Skip to content

ReactorD3 分两层,你选择在哪一层上创作。高层 DSL —— LineChartBarChartAreaChartPieChart,外加 TreeChartForceGraph —— 覆盖了仪表盘反复要的那些场景:一个系列、一个 x 访问器、一个 y 访问器,以及一个由库替你接好的、有主张的视觉表面(坐标轴、网格、调色板、键盘导航、屏幕阅读器摘要)。当有主张的表面不合身时 —— 你想把某个扇区的百分比渲染在扇区内部、你想要一张热力图、你想要一个定制的平行坐标视图 —— 同一个库又暴露了构建出那些高层形状的 D3 基元:LinearScaleBandScaleD3CanvasD3LinePath<T>D3ArcPath,以及高层图表上的 *LabelView 逃生舱。这与 Recharts 和 Observable Plot 所画的取舍相同 —— 便利优先,逃生舱在下面 —— 只不过每个 D3 基元都渲染为 WinUI 形状,且用着与高层图表相同的那块 Canvas,因此降一层绝不意味着换渲染器。默认挑高层;只有当高层表面拒绝了你要的东西时才下探到 D3。

图表

ReactorD3 把数据可视化带进 Microsoft.UI.Reactor(Reactor)。图表 DSL 提供 LineChartBarChartAreaChartPieChart 等高层工厂方法,产出标准的 Reactor 元素。你绑定数据,用流畅 API 配置外观,图表就渲染为 Canvas 上的原生 WinUI 形状。

把 DSL 与标准 Reactor 工厂方法一并导入。图表由可选的 Microsoft.UI.Reactor.Advanced 包提供(spec 062 §7),所以先加上它的包引用:

<PackageReference Include="Microsoft.UI.Reactor.Advanced" Version="0.1.0-preview.15" />
using static Microsoft.UI.Reactor.Factories;
using static Microsoft.UI.Reactor.Charting.Charts;

折线图

LineChart 在你的数据点上画一条连续折线。传入一个集合以及 X 和 Y 的访问器函数:

class LineChartDemo : Component
{
    private static readonly SalesPoint[] Data =
    [
        new(1, 120), new(2, 180), new(3, 150),
        new(4, 220), new(5, 310), new(6, 280),
        new(7, 350), new(8, 400), new(9, 380),
        new(10, 420), new(11, 460), new(12, 510)
    ];

    public override Element Render()
    {
        return VStack(12,
            SubHeading("Line Chart"),
            LineChart(Data, d => d.Month, d => d.Revenue)
                .Title("Monthly Revenue — Line")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .AxisLabel(ChartAxisType.X, "Month")
                .AxisLabel(ChartAxisType.Y, "Revenue (USD)")
                .Width(600).Height(250)
                .Stroke("#0078D4").StrokeWidth(2.5)
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

折线图

图表会自动缩放坐标轴以适配你的数据,并做 .Nice() 取整。用 .ShowGrid() 开关网格线,用 .ShowAxes() 开关坐标轴标签。

条形图

BarChart 渲染竖直条。每个数据点变成一根条,其高度映射到 Y 值:

class BarChartDemo : Component
{
    private static readonly SalesPoint[] Data =
    [
        new(1, 340), new(2, 420), new(3, 510), new(4, 380)
    ];

    public override Element Render()
    {
        return VStack(12,
            SubHeading("Bar Chart"),
            BarChart(Data, d => d.Month, d => d.Revenue)
                .Title("Quarterly Revenue — Bar")
                .SeriesName("Revenue")
                .Units("quarters", "USD")
                .AxisLabel(ChartAxisType.X, "Quarter")
                .AxisLabel(ChartAxisType.Y, "Revenue (USD)")
                .Width(600).Height(250)
                .Fill("#50C878")
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

条形图

条宽根据数据点数量自动计算。用 .Fill() 设置条形颜色,用 .FillOpacity() 控制透明度。

面积图

AreaChart 填充数据折线与基线之间的区域。它把填充区域与一条折线叠加起来:

class AreaChartDemo : Component
{
    private static readonly SalesPoint[] Data =
    [
        new(1, 50), new(2, 120), new(3, 200),
        new(4, 350), new(5, 480), new(6, 600),
        new(7, 720), new(8, 850), new(9, 1020),
        new(10, 1150), new(11, 1300), new(12, 1500)
    ];

    public override Element Render()
    {
        return VStack(12,
            SubHeading("Area Chart"),
            AreaChart(Data, d => d.Month, d => d.Revenue)
                .Title("Monthly Revenue — Area")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .AxisLabel(ChartAxisType.X, "Month")
                .AxisLabel(ChartAxisType.Y, "Revenue (USD)")
                .Width(600).Height(250)
                .Stroke("#9B59B6").Fill("#9B59B6")
                .FillOpacity(0.2)
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

面积图

.FillOpacity() 控制区域透明度。取一个较低的值(0.15--0.3),既让网格线透出来,又能填满形状。

饼图

PieChart 把数据分成成比例的弧。把 .InnerRadius() 设为大于 0 就得到环形图:

class PieChartDemo : Component
{
    private static readonly CategoryData[] Data =
    [
        new("Engineering", 42),
        new("Marketing", 18),
        new("Sales", 25),
        new("Support", 15)
    ];

    public override Element Render()
    {
        return VStack(12,
            SubHeading("Pie Chart"),
            PieChart(Data, d => d.Value, d => d.Name)
                .Title("Team Distribution")
                .Description("Pie chart showing team size across Engineering, Marketing, Sales, and Support.")
                .Width(300).Height(300)
                .InnerRadius(60)
                .PadAngle(0.03)
        ).Padding(24);
    }
}

饼图

传入一个标签访问器即可在每条弧的重心处显示文本。颜色默认在 Category10 调色板里循环 —— 用 .SetColors() 覆盖。

图表配置

所有图表类型共享一组通用的构造器方法:

方法 默认值 用途
.Width(n) 400 画布宽度(像素)
.Height(n) 300 画布高度(像素)
.Margin(l, t, r, b) 40, 20, 20, 30 坐标轴/标签边距,顺序为 left, top, right, bottom
.Stroke(color) #4285f4 线条/边框颜色
.Fill(color) #4285f4 填充颜色
.StrokeWidth(n) 2 线条粗细
.FillOpacity(n) 0.3 填充透明度(0--1)
.ShowAxes(bool) true 显示坐标轴线与标签
.ShowGrid(bool) true 显示横向网格线

颜色接受任何 CSS 风格的字符串:#RGB#RRGGBBrgb(r,g,b),或 steelblue 这类具名颜色。想要能随深色模式自适应的调色板感知颜色,参见 styling 和下面的 .Palette(...) 无障碍修饰符

把数据绑定到状态

图表是标准 Reactor 元素。状态变化时,图表用新数据重新渲染:

class CombinedChartDemo : Component
{
    private static readonly SalesPoint[] Data2024 =
    [
        new(1, 100), new(2, 140), new(3, 180),
        new(4, 200), new(5, 260), new(6, 300)
    ];

    private static readonly SalesPoint[] Data2025 =
    [
        new(1, 160), new(2, 220), new(3, 280),
        new(4, 320), new(5, 390), new(6, 450)
    ];

    private static readonly string[] Years = ["2024", "2025"];

    public override Element Render()
    {
        var (year, setYear) = UseState(0);
        var data = year == 0 ? Data2024 : Data2025;

        return VStack(12,
            SubHeading("Interactive Chart"),
            ComboBox(Years, year, setYear),
            AreaChart(data, d => d.Month, d => d.Revenue)
                .Title("Revenue by Year")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .Interactive()
                .Width(600).Height(250)
                .Stroke("#0078D4").Fill("#0078D4")
                .FillOpacity(0.15)
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

交互式图表

hooks 里的 UseState 之类来驱动数据选择。图表在每次渲染时重建 —— 为保持交互流畅,请把数据集控制在约 1000 个点以内。

动态数据更新

图表是原生 Reactor 元素,因此你只需要改状态。用 UseStateUseReducer 或任何 Hook 来更新数据 —— 协调器会对新旧元素树做差异比对,只打补丁变化的部分:

class DynamicDataDemo : Component
{
    private static readonly List<SalesPoint> InitialPoints =
        Enumerable.Range(1, 8)
            .Select(i => new SalesPoint(i, Random.Shared.Next(50, 500)))
            .ToList();

    public override Element Render()
    {
        var (points, updatePoints) = UseReducer(InitialPoints);

        return VStack(12,
            SubHeading("Dynamic Data"),
            Button("Randomize", () => updatePoints(_ =>
                Enumerable.Range(1, 8)
                    .Select(i => new SalesPoint(i, Random.Shared.Next(50, 500)))
                    .ToList())),
            BarChart<SalesPoint>(points, d => d.Month, d => d.Revenue)
                .Title("Dynamic Revenue Data")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .Width(600).Height(250)
                .Fill("#E74C3C")
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

动态数据

对于高频更新(60fps 流式),用 OnReady 拿到一个句柄,它暴露底层 Canvas 供直接操作。这与动画管线用的是同一个合成器属性逃生舱 —— 静态几何通过 Reactor 渲染,活的值则直接改写。

自定义标签元素

大多数图表只需要基于字符串的标签 API(AxisLabelLabelAccessorDataLabel)。只有当纯文本不够时才去够 *View 扩展 —— 比如图标加文本的刻度、混合排版的多行标签,或把某个扇区的百分比渲染在扇区内部。

饼图扇区标签

LabelView 替换每个饼图扇区上内置的文本文本标签。委托收到该扇区的数据项以及一个描述其几何的 PieSliceLayout,并返回任意 Element:

// 百分比渲染在扇区内部。字符串标签访问器仍然传入,
// 以便屏幕阅读器描述该扇区。
PieChart(Data, d => d.Value, d => d.Name)
    .Title("Team Distribution")
    .Width(300).Height(300)
    .InnerRadius(50).PadAngle(0.02)
    .LabelView((d, layout) =>
        TextBlock($"{layout.Fraction:P0}")
            .FontSize(12).Bold().Foreground(Theme.AccentText))

在每个扇区内部渲染百分比的饼图

PieSliceLayout 暴露该扇区的 IndexValueFractionCentroidX/CentroidYStartAngle/EndAngleInnerRadius/OuterRadius,以及解析出的调色板 Color,因此一个标签可以呼应扇区几何而无须重算它。

坐标轴刻度标签

XTickLabelViewYTickLabelView 把数字刻度标签替换成任意 Element。每个委托收到该刻度的定义域值:

// X 轴刻度:为每个刻度渲染月份名加一个小标题。
LineChart(Data, d => d.Month, d => d.Revenue)
    .Title("Revenue by Month")
    .SeriesName("Revenue")
    .Width(600).Height(220)
    .Stroke("#0078D4").StrokeWidth(2.5)
    .ShowGrid(true).ShowAxes(true)
    .XTickLabelView(t => VStack(2,
        TextBlock(Months[Math.Clamp((int)t - 1, 0, Months.Length - 1)])
            .FontSize(11).SemiBold(),
        TextBlock("month").FontSize(8).Opacity(0.6)))

带月份名坐标轴刻度的折线图

方法 用途
PieChartElement<T>.LabelView(Func<T, PieSliceLayout, Element>, Func<T, string>? name = null, bool interactive = false) 用任意 Element 替换内置扇区文本,锚定在扇区重心上
ChartElement<T>.XTickLabelView(Func<double, Element>, Func<double, string>? name = null, bool interactive = false) 替换 X 轴刻度标签,在刻度上水平居中
ChartElement<T>.YTickLabelView(Func<double, Element>, Func<double, string>? name = null, bool interactive = false) 替换 Y 轴刻度标签,右对齐到坐标轴边缘

默认情况下,你返回的元素以非交互方式渲染:图表会对整个已实现子树(而不只是外层包装器)强制施加 AccessibilityView.Raw,并清除每个内部控件的 IsTabStop,因此没有任何内部 peer 暴露给辅助技术,标签内部也不会有任何东西进入键盘 Tab 序。图表那套结构化无障碍描述(见下)因此始终是唯一真相来源。该元素也会自动锚定 —— 构造时你不需要已知尺寸,图表会在布局时重新定位。

两个可选参数对此做细化:

  • name 为这种情形提供一个可访问名称投影:你在没有字符串 LabelAccessor(饼图)/ DataLabel(折线/条形/面积图)的情况下使用了 *View 覆盖。对于饼图扇区,该投影喂给图表自己的扇区描述符,于是播报出的名字与可见标签一致,而不是退回到 Slice {n}。对于坐标轴刻度,它在已实现的刻度元素上设置 AutomationName(刻度没有逐刻度描述符)。有字符串 LabelAccessor/DataLabel 时优先用它;只有当视觉完全住在 *View 里时才去够 name
  • interactive: true 是一个可选启用的逃生舱:它保留子树的 peer 与 tab stop,使你标签内的可聚焦子元素保持可达。一旦你设置了它,就要为标签的无障碍负责 —— 图表不再隐藏它。在运行时切换 interactive 会重挂载标签,让隐藏/取消隐藏立即生效。

图表无障碍

图表开箱即完全可访问。加 .Title().SeriesName() 供屏幕阅读器识别,加 .Units() 做坐标轴注解,加 .Interactive() 开启键盘导航:

/// <summary>
/// Canonical accessible chart pattern — demonstrates all recommended accessibility
/// modifiers for both static and interactive charts. Follow this pattern to ensure
/// charts are fully accessible to screen readers, keyboard users, and users who
/// need forced-colors or reduced-motion.
/// </summary>
class AccessibleChartDemo : Component
{
    private static readonly SalesPoint[] Data =
    [
        new(1, 120), new(2, 180), new(3, 150),
        new(4, 220), new(5, 310), new(6, 280)
    ];

    public override Element Render()
    {
        return VStack(12,
            SubHeading("Accessible Chart"),

            // 静态可访问图表:Title + SeriesName + Units
            LineChart(Data, d => d.Month, d => d.Revenue)
                .Title("Monthly Revenue 2024")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .AxisLabel(ChartAxisType.X, "Month")
                .AxisLabel(ChartAxisType.Y, "Revenue (USD)")
                .Width(600).Height(250)
                .Stroke("#0078D4").StrokeWidth(2.5)
                .ShowGrid(true).ShowAxes(true),

            // 交互式可访问图表:加上键盘导航与点调用
            BarChart(Data, d => d.Month, d => d.Revenue)
                .Title("Monthly Revenue — Interactive")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .Interactive()
                .Width(600).Height(250)
                .Fill("#50C878")
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

可访问图表

无障碍修饰符

方法 用途
.Title(str) 可见标题与可访问名称
.Description(str) 覆盖自动生成的摘要
.SeriesName(str) 为数据系列命名
.Units(x, y) 坐标轴单位注解(如 "months"、"USD")
.AxisLabel(axis, str) 显式坐标轴标签
.Interactive() 启用键盘导航与虚拟焦点
.OnPointInvoke(handler) 在某个点上按下 Enter/Space 时的回调
.AlternateView(element) 在图表与数据表之间切换(T 键)
.Palette(palette) 经筛选的色盲安全调色板
.SeriesColors(colors) 自定义系列颜色(经扫描器校验)
.SetColors(colors) 自定义饼图扇区颜色(经扫描器校验;是 .SeriesColors 在饼图上的对应物)
.RawColors(colors) 原始系列颜色 —— 逃生舱,不做校验(Tier 4)
.ChartBackground(color) 声明所渲染的背景,使调色板对比度(A11Y_CHART_011)针对这唯一的实际背景做检查(warning 级),而不是针对固定的浅色/深色背景之一(info 级)
.SeriesShapes(shapes) 用于双重编码的标记形状
.SeriesDashes(dashes) 用于双重编码的虚线样式

屏幕阅读器会播报图表类型、系列数量、数据范围和各个数据点的值。键盘用户用方向键导航,用 Enter 调用数据点,用 T 切换到备用的数据表视图。框架完整的 a11y 故事在 accessibility;图表特有的契约是:颜色永远不是唯一的编码 —— 任何发给强制颜色用户的多系列图表,都要把 .Palette(...).SeriesShapes(...).SeriesDashes(...) 组合起来。

底层绘制

对于自定义可视化,ReactorD3 提供形状生成器和一套 Canvas DSL。导入 using static Microsoft.UI.Reactor.Charting.D3Charts; 可获得:

  • D3Canvas(w, h, children) —— 创建一个绘制表面
  • D3RectD3CircleD3LineD3Path —— 基元形状
  • TextTextRightTextCenter —— 定位文本
  • D3LinePath<T>D3AreaPath<T>D3ArcPath —— 生成器辅助
  • D3AxesD3GridD3Legend —— 坐标轴/图例组合件
  • Brush(color)Palette —— 画笔与默认分类调色板

这三个是位置型的:Text(x, y, text, …) 接受画布坐标,与核心的 TextBlock(...) 工厂方法无关。Reactor 没有核心 Text(...) 元素工厂 —— 别处存在的那些 Text(...) 成员是 DataGrid 的单元格渲染器与编辑器辅助器,它们返回 Func<object, Element>,而不是 Element

比例尺是类型,不是 D3Charts 的成员 —— 它们住在更深一层命名空间里。加 using Microsoft.UI.Reactor.Charting.D3; 才能碰到它们。

比例尺类型

每个比例尺都暴露 .Map(value) 把定义域值投影到值域空间 —— 比例尺是对象而非委托,所以 scale(v) 编译不过。用 new LinearScale(domain, range) 构造一个,或用 .SetDomain(...) / .SetRange(...) 流畅地配置。

比例尺 用途
LinearScale 连续数值映射,带 .Nice().Ticks()
BandScale<T> 分类型映射,带带宽(条形图)
LogScale 面向指数数据的对数映射
PowScale 幂/平方根映射
OrdinalScale<T> 离散到离散的映射

注意:Render() 里重建数据数组,后果自负。图表在快路径上按引用比较传入的数据 —— 每次渲染一个新数组,意味着图表要重新推导比例尺、重跑饼图布局、并每帧重新分配 WinUI 形状池,即使什么都没变化。症状是:一个无关状态更新时图表闪烁。修法与 Reactor 到处在用的形状一样:用 UseMemo(() => …, deps) 把数据包起来,键取真正会变的那个东西。这个模式在 PieChart 上最明显 —— 每次渲染都 new[] { … } 会让每个扇区重挂载,对那些根本没动的扇区也触发 .Transition() 的进入/退出,于是"每往一个无关的 TextBox 里敲一个键,图表就自己重画一遍"。

模式

来自滴答数据源的实时更新图表

流式图表 —— 一个价格推送、一个传感器读数 —— 驱动一个滑动窗口的数据点。把窗口放在 UseReducer 里(每次滴答从前一个窗口推导出下一个),滴答时追加,一旦 Count 超过窗口大小就丢掉头部。UseEffect 接受同步的 Action/Func<Action>,绝不接受 async lambda,所以从副作用里启动那个泵,并从返回的清理函数里取消它:

record Sample(DateTime Time, double Value);

class LiveChartDemo : Component
{
    public override Element Render()
    {
        var (samples, updateSamples) = UseReducer<IReadOnlyList<Sample>>(Array.Empty<Sample>());

        // UseEffect 接受同步的 Action/Func<Action> —— 绝不接受 async lambda。
        // 从副作用里启动泵,从返回的清理函数里取消它。
        UseEffect(() =>
        {
            var cts = new CancellationTokenSource();
            _ = PumpAsync(cts.Token);
            return () => { cts.Cancel(); };
        }, Array.Empty<object>());

        return VStack(12,
            SubHeading("Live feed"),
            LineChart(samples, s => s.Time.Ticks, s => s.Value)
                .Title("Live feed")
                .SeriesName("Value")
                .Width(600).Height(220)
                .Stroke("#0078D4")
        ).Padding(24);

        async Task PumpAsync(CancellationToken token)
        {
            using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(200));
            try
            {
                while (await timer.WaitForNextTickAsync(token))
                {
                    updateSamples(prev =>
                    {
                        var next = prev.Append(new Sample(DateTime.Now, Random.Shared.Next(0, 100))).ToList();
                        return next.Count > 60 ? next.Skip(next.Count - 60).ToList() : next;
                    });
                }
            }
            catch (OperationCanceledException)
            {
                // 卸载时的预期行为。
            }
        }
    }
}

图表在每次滴答时重画。对于低于 100ms 的节奏,下探到 OnReady 直接改写 Canvas —— 协调器很快,但不是免费的。定时器的清理模式见 effects-scheduling

切换图表类型而不丢数据绑定

同一份数据既能喂折线图、条形图,也能喂面积图 —— 让用户挑形状。把 ComboBox 选中的索引存在状态里,并据此切换工厂调用:

class ChartSwitchDemo : Component
{
    private static readonly SalesPoint[] Data =
    [
        new(1, 120), new(2, 180), new(3, 150), new(4, 220), new(5, 310), new(6, 280)
    ];

    public override Element Render()
    {
        // ComboBox 上报的是选中的 *索引*,所以持有一个 int 并在其上分支。
        var (kind, setKind) = UseState(0);
        var kinds = new[] { "line", "bar", "area" };

        // 每个工厂方法都返回 ChartElement<T>,因此这个 switch 有共同的
        // 类型,下面的修饰符链是共享的。
        ChartElement<SalesPoint> chart = kind switch
        {
            0 => LineChart(Data, d => d.Month, d => d.Revenue),
            1 => BarChart(Data, d => d.Month, d => d.Revenue),
            _ => AreaChart(Data, d => d.Month, d => d.Revenue),
        };

        return VStack(8,
            SubHeading("Switch chart type"),
            ComboBox(kinds, kind, setKind),
            chart
                .Title("Revenue")
                .SeriesName("Revenue")
                .Width(600).Height(250)
                .Stroke("#0078D4")
        ).Padding(24);
    }
}

因为每个工厂方法都返回一个 ChartElement<T>,修饰符链是完全相同的 —— 唯一有分支的是构造函数。

为自定义形状下探到 D3

当高层表面不合身时(热力图、平行坐标、蜂群图),直接渲染 D3Canvas 并组合 D3 形状生成器。LinearScale / BandScale 类型与高层图表内部使用的是同一批,因此行为一致 —— .Nice().Ticks() 和定义域/值域设置器的工作方式完全相同。D3AxesD3Grid 返回 Element[],所以用 .. 把它们展开进画布子元素:

class D3CustomDemo : Component
{
    public override Element Render()
    {
        const double w = 600, h = 240;
        const double left = 50, top = 20, right = 20, bottom = 40;
        double plotW = w - left - right, plotH = h - top - bottom;

        var data = Enumerable.Range(0, 40)
            .Select(i => (x: (double)i, y: 40 + 30 * Math.Sin(i / 4.0) + i))
            .ToArray();

        var (yMin, yMax) = D3Extent.Extent(data.Select(d => d.y));

        // 比例尺是普通对象:Set* 做流畅改写,Map 做值投影。
        var xs = new LinearScale([0, 39], [left, left + plotW]);
        var ys = new LinearScale([yMax, yMin], [top, top + plotH]).Nice();

        var line = D3Charts.Brush("#0078D4");

        return VStack(12,
            SubHeading("Custom D3 canvas"),
            D3Charts.D3Canvas(w, h,
                [.. D3Charts.D3Grid(ys, left, plotW),
                 .. D3Charts.D3Axes(xs, ys, left, top, plotW, plotH),
                 D3Charts.D3LinePath(data, x: d => xs.Map(d.x), y: d => ys.Map(d.y),
                     stroke: line, strokeWidth: 2),
                 .. data.Select(d => (Element)(D3Charts.D3Circle(xs.Map(d.x), ys.Map(d.y), 3)
                     with { Fill = line }))])
        ).Padding(24);
    }
}

这与 Observable Plot 用的是同一套组合策略 —— 几何基元置于比例尺之上 —— 只不过那些基元渲染为 WinUI 形状,而不是 SVG。要做热力图,把 D3LinePath 换成嵌套的 D3Rect;要做平行坐标,为每个轴重复调用一次 line-path。

常见错误

在 Render 里构造数据数组

// 不要这样:
public override Element Render()
{
    var data = new SalesPoint[] { new(1, 120), new(2, 180), /* … */ };
    return LineChart(data, d => d.Month, d => d.Revenue);
}
class DynamicDataDemo : Component
{
    private static readonly List<SalesPoint> InitialPoints =
        Enumerable.Range(1, 8)
            .Select(i => new SalesPoint(i, Random.Shared.Next(50, 500)))
            .ToList();

    public override Element Render()
    {
        var (points, updatePoints) = UseReducer(InitialPoints);

        return VStack(12,
            SubHeading("Dynamic Data"),
            Button("Randomize", () => updatePoints(_ =>
                Enumerable.Range(1, 8)
                    .Select(i => new SalesPoint(i, Random.Shared.Next(50, 500)))
                    .ToList())),
            BarChart<SalesPoint>(points, d => d.Month, d => d.Revenue)
                .Title("Dynamic Revenue Data")
                .SeriesName("Revenue")
                .Units("months", "USD")
                .Width(600).Height(250)
                .Fill("#E74C3C")
                .ShowGrid(true).ShowAxes(true)
        ).Padding(24);
    }
}

每次渲染都分配一个新数组,于是图表的引用相等检查失败,比例尺重新推导,形状池重置 —— 尽管可见上什么都没变。把静态数据提升为 static readonly 字段,或用 UseMemo(() => BuildData(...), deps) 把动态数据包起来。

把颜色当作唯一的系列编码

// 不要这样:
LineChart(serverA, ...).Stroke("red"),
LineChart(serverB, ...).Stroke("green"),
LineChart(serverC, ...).Stroke("blue")

在强制颜色模式下(Windows 高对比度),每一条描边都坍缩成当前的强调色,用户分不清这几个系列。请把颜色与 .SeriesShapes(MarkerShape.Circle, MarkerShape.Square, MarkerShape.Triangle).SeriesDashes(DashStyle.Solid, DashStyle.Dash4_2, DashStyle.Dash2_2) 配对,这样即便没有颜色,系列依然可辨。与 WCAG 2.2 SC 1.4.1 里对图表的原则一致。

用手逐个动画 D3 形状

// 不要这样:
D3Circle(xs.Map(d.X), ys.Map(d.Y), 3)
    .Set(c =>
    {
        var anim = c.Compositor.CreateScalarKeyFrameAnimation();
        anim.InsertKeyFrame(0f, 0.0f);
        anim.InsertKeyFrame(1f, 1.0f);
        anim.Target = "Opacity";
        c.StartAnimation(anim);
    })

内联的合成动画重复了 .OpacityTransition().Transition(Transition.Fade) 已经做的事,而且绕过了 Reactor 在动画管线层面所尊重的"减少动态效果"偏好。请在 D3 形状上使用 Reactor 的动画修饰符 —— 它们的工作方式与用在 BorderTextBlock 上时完全一致。

提示

从高层 DSL 开始。 LineChartBarChartAreaChartPieChart 覆盖了大多数仪表盘需求,无需触碰比例尺或生成器。

为可读性启用 .ShowGrid(true) 网格线让从图表上读数值容易得多,折线图和面积图尤其如此。

把数据集控制在 1000 个点以内。 每个数据点都在 Canvas 上创建 WinUI 形状。更大的数据集请先聚合或采样再绘图。

实时数据只需改状态。 图表的差异比对很高效 —— 更新状态会触发协调器只打补丁变化的部分。需要 60fps 逃生舱时,用 OnReady 直接访问底层 Canvas。

比例型数据用环形图(InnerRadius > 0)。 中心那块空间很适合放一个总计或标签。

下一步

  • 动画 —— 上一篇:过渡、关键帧、交互状态
  • 高级模式 —— 下一篇:错误边界、记忆化与性能调优
  • 集合 —— 从虚拟化列表与可观察集合绑定图表数据
  • 副作用与生命周期 —— 用 UseEffect 异步加载图表数据
  • 样式与主题 —— 用主题令牌作为图表颜色,以适配深色模式
  • 无障碍 —— 屏幕阅读器、键盘导航与强制颜色指南