ReactorD3 分两层,你选择在哪一层上创作。高层 DSL —— LineChart、BarChart、AreaChart、PieChart,外加 TreeChart 和 ForceGraph —— 覆盖了仪表盘反复要的那些场景:一个系列、一个 x 访问器、一个 y 访问器,以及一个由库替你接好的、有主张的视觉表面(坐标轴、网格、调色板、键盘导航、屏幕阅读器摘要)。当有主张的表面不合身时 —— 你想把某个扇区的百分比渲染在扇区内部、你想要一张热力图、你想要一个定制的平行坐标视图 —— 同一个库又暴露了构建出那些高层形状的 D3 基元:LinearScale、BandScale、D3Canvas、D3LinePath<T>、D3ArcPath,以及高层图表上的 *LabelView 逃生舱。这与 Recharts 和 Observable Plot 所画的取舍相同 —— 便利优先,逃生舱在下面 —— 只不过每个 D3 基元都渲染为 WinUI 形状,且用着与高层图表相同的那块 Canvas,因此降一层绝不意味着换渲染器。默认挑高层;只有当高层表面拒绝了你要的东西时才下探到 D3。
图表¶
ReactorD3 把数据可视化带进 Microsoft.UI.Reactor(Reactor)。图表 DSL 提供 LineChart、BarChart、AreaChart 和 PieChart 等高层工厂方法,产出标准的 Reactor 元素。你绑定数据,用流畅 API 配置外观,图表就渲染为 Canvas 上的原生 WinUI 形状。
把 DSL 与标准 Reactor 工厂方法一并导入。图表由可选的 Microsoft.UI.Reactor.Advanced 包提供(spec 062 §7),所以先加上它的包引用:
折线图¶
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、#RRGGBB、rgb(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 元素,因此你只需要改状态。用 UseState、UseReducer 或任何 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(AxisLabel、LabelAccessor、DataLabel)。只有当纯文本不够时才去够 *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 暴露该扇区的 Index、Value、Fraction、CentroidX/CentroidY、StartAngle/EndAngle、InnerRadius/OuterRadius,以及解析出的调色板 Color,因此一个标签可以呼应扇区几何而无须重算它。
坐标轴刻度标签¶
XTickLabelView 和 YTickLabelView 把数字刻度标签替换成任意 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)—— 创建一个绘制表面D3Rect、D3Circle、D3Line、D3Path—— 基元形状Text、TextRight、TextCenter—— 定位文本D3LinePath<T>、D3AreaPath<T>、D3ArcPath—— 生成器辅助D3Axes、D3Grid、D3Legend—— 坐标轴/图例组合件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() 和定义域/值域设置器的工作方式完全相同。D3Axes 和 D3Grid 返回 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 的动画修饰符 —— 它们的工作方式与用在 Border 或 TextBlock 上时完全一致。
提示¶
从高层 DSL 开始。 LineChart、BarChart、AreaChart 和 PieChart 覆盖了大多数仪表盘需求,无需触碰比例尺或生成器。
为可读性启用 .ShowGrid(true)。 网格线让从图表上读数值容易得多,折线图和面积图尤其如此。
把数据集控制在 1000 个点以内。 每个数据点都在 Canvas 上创建 WinUI 形状。更大的数据集请先聚合或采样再绘图。
实时数据只需改状态。 图表的差异比对很高效 —— 更新状态会触发协调器只打补丁变化的部分。需要 60fps 逃生舱时,用 OnReady 直接访问底层 Canvas。
比例型数据用环形图(InnerRadius > 0)。 中心那块空间很适合放一个总计或标签。