ChartAI.Blazor
1.2.0
dotnet add package ChartAI.Blazor --version 1.2.0
NuGet\Install-Package ChartAI.Blazor -Version 1.2.0
<PackageReference Include="ChartAI.Blazor" Version="1.2.0" />
<PackageVersion Include="ChartAI.Blazor" Version="1.2.0" />
<PackageReference Include="ChartAI.Blazor" />
paket add ChartAI.Blazor --version 1.2.0
#r "nuget: ChartAI.Blazor, 1.2.0"
#:package ChartAI.Blazor@1.2.0
#addin nuget:?package=ChartAI.Blazor&version=1.2.0
#tool nuget:?package=ChartAI.Blazor&version=1.2.0
ChartAI.Blazor
A Blazor component for chartai, a tiny WebGPU chart engine that renders millions of points across thousands of series off the main thread. The engine ships bundled inside the package; there is nothing to install on the JS side. The bundled build is upstream chartai 1.1.0 plus the changes listed in the changelog: in-place data patching, gaps, multiple y axes, anti-aliased lines, GPU hover highlight and axis panning.
Requirements
- .NET 10 (Blazor WebAssembly or an interactive Server render mode; the component uses JS interop, so it does not render under static SSR).
- A browser with WebGPU (current Chrome, Edge, Safari and Firefox). Without a WebGPU adapter
the host element shows a notice (
Chart.UnavailableText) instead of an empty box.
Install
dotnet add package ChartAI.Blazor
Quick start
No service registration and no <script> tag is needed. The component imports its JS module
on first render. The host element must have a height; the default Style gives it min-height: 300px.
@using ChartAI.Blazor.Components
@using ChartAI.Blazor.Models
<Chart Config="config" Series="series" Plugins="ChartPlugins.Crosshair | ChartPlugins.Minimap" />
@code {
private readonly ChartConfig config = new()
{
Type = ChartType.Line,
FormatX = AxisFormat.Index,
FormatY = AxisFormat.Number,
Legend = new LegendConfig { DefaultOpen = true },
};
private readonly ChartSeries[] series =
[
new() { Label = "Signal", Color = "#3b82f6", X = [0, 1, 2, 3, 4], Y = [1, 3, 2, 5, 4] },
new() { Label = "Noise", Color = "#f97316", X = [0, 1, 2, 3, 4], Y = [2, 1, 3, 2, 3] },
];
}
Chart types
ChartType: Line, Area, Scatter, Bar, Candlestick, Ohlc, Step, Histogram,
Heatmap, Bubble, BaselineArea, ErrorBand, Waterfall.
Renderer-specific channels live on ChartSeries (Open/High/Low for candlesticks and OHLC,
Lo/Hi for error bands, R for bubbles, Value for heatmaps). ChartSeries.Waterfall(...)
builds a waterfall series from a list of deltas. Any other numeric channel can be passed through Extra.
Plugins
Hover tooltips, zoom, legend, annotations, thresholds and watermark are always available and
switch on through their ChartConfig properties. Interactive per-chart plugins are attached with
the Plugins flags parameter:
ChartPlugins.Crosshair | Stats | Ruler | TooltipPin | Minimap | RangeSelector
Annotation labels are clickable: AnnotationClicked receives the Id of the annotation whose
label was clicked. A vertical line's label sits in the bottom margin, or just inside the top of
the plot with LabelPosition = AnnotationLabelPosition.Top; neighbouring labels stack in lanes.
Charts on one page can be linked: the module's setSyncViews(mode) mirrors a zoom or pan to every
other chart - true or "both" on both axes, "x" or "y" on one. A trend page links the time
axis and leaves each plot its own scale. Linked charts share the visible data range, so charts
whose data covers different spans still show the same window. ViewChanged is raised for every
chart whose view moved: the one the user dragged, the charts that followed it, and a view moved by
the minimap, the range selector or ResetViewAsync().
Updating a chart
Configchanges are detected by comparing its serialized JSON with what was last sent: a new instance with the same content costs nothing, and an object changed in place is picked up the next time the parent renders.RefreshConfigAsync()on a@refsends it right away.Serieschanges are detected by reference: assign a new collection to send new data.RefreshAsync()resendsConfigandSeries.SetDataAsync(series)replaces the data;ResetViewAsync()animates back to fit-to-data.- The methods can be called as soon as the
@refis set, inOnAfterRenderAsync(firstRender)say: a call made while the chart is still initialising runs once it is up.Readycompletes with true then, or with false when the chart never comes up (no WebGPU, or it was disposed); the methods then do nothing andPatchDataAsyncreturns 0. AwaitReadybefore starting a timer that feeds the chart. - Changing
Pluginsrecreates the chart; the data it shows is kept, including data fromSetDataAsyncandPatchDataAsync.Idis read once, when the chart is created. - A missing sample is
double.NaNin any channel and renders as a gap. The series cross JS interop as binary float64 (a shared x once), not as JSON text. NaN or an infinity in a config value, a bound or an annotation serializes as null: the value counts as unset. - While the pointer is on a line that series is drawn on top and the others fade
(
ChartConfig.HighlightHover); the tooltip leads with it. Dragging an axis gutter pans that axis, the wheel over it zooms it. ChartConfig.BgColoris the opaque background the axis margins are painted in. By default they are a gradient that fades the data out toward the borders;BgFade = falsepaints them as plain strips with a hard edge and starts the home view at that edge.
Live data
A chart that follows a stream should not be rebuilt per tick. PatchDataAsync writes columns
in place: only the patched columns cross JS interop, and the engine writes them straight into
the existing GPU buffers. Every series of such a chart shares the x axis, ascending.
<Chart @ref="chart" Config="config" Series="series" ViewChanged="OnViewChanged" />
@code {
// A window of 500 columns with room for as many again, see "What a tick costs" below.
private readonly ChartConfig config = new() { Type = ChartType.Line, Capacity = 1000 };
private Task Tick(double x, double a, double b) => chart.PatchDataAsync(new ChartPatch
{
X = [x], // one new column
Series = [new() { Y = [a] }, new() { Y = [b] }], // one entry per series
DropBefore = x - 500, // ring buffer: drop what left the window
Bounds = new ChartBounds { MinX = x - 500, MaxX = x },
});
// The visible range after the user zoomed or panned: fetch that window.
private Task OnViewChanged(ChartViewRange r) => LoadAsync(r.MinX, r.MaxX);
}
Offsetrewrites from a column on (the newest sample refreshed); null appends.Drop/DropBeforediscard the oldest columns first, so a window of fixed length never grows.Boundsmoves the data window with the patch;SetBoundsAsyncdoes only that when nothing new arrived. A side left null keeps its value.ResetViewputs the view back to its home transform, so the moved window is what is shown.SetDataAsync(series, capacity, bounds)reloads everything with the buffers sized forcapacity. Withoutboundsthe window fits the data (orDefaultBounds) again.- With several y axes a patch keeps the secondary axes' ranges; reload with
SetDataAsyncto rescale them.
What a tick costs. A patch crosses interop as one small binary block, and its columns are
copied into the chart's column store and written into the GPU buffers; nothing else is uploaded.
A dropped column is only skipped: the chart draws from the first column still in the window.
When the buffers are full, the columns of the window are moved back to their start and uploaded
once (a full-window write, without recreating anything), or the buffers grow to twice their size
(one full upload) when that would free less than a quarter of them. With Capacity twice the
window that happens once per window of ticks. What does not shrink with the delta is the GPU's
work: after a patch it reruns its per-pixel reduction over the visible columns of every series,
so a frame costs the window times the series, as for any redraw. Compute the bounds you send from
the samples you have (double.NaN makes Min() and Max() NaN).
Dark mode
ChartConfig.IsDark = true switches the engine to its dark theme, and setting it back to false
switches to light. The theme is global: every chart on the page follows it, whichever chart
set it, so set it on one chart (or on all of them alike). A chart that leaves IsDark false does
not touch the theme. To switch every chart at once from your own JS interop:
var mod = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/ChartAI.Blazor/chartai-blazor.js");
await mod.InvokeVoidAsync("setTheme", isDark);
When the engine initialises it reads a dark class on <html>, unless setTheme was already
called.
Multiple Y axes
Configure axes via ChartConfig.YAxes and bind each series with ChartSeries.YAxis.
Axes on the same side stack outward without overlapping; YAxisConfig.Width and
ChartConfig.YAxisGap control the horizontal distance. The tooltip and legend
combine the series of all axes, each value formatted with its own axis format.
var config = new ChartConfig
{
Type = ChartType.Line,
YAxisGap = 8, // px between stacked axes (optional, default 6)
YAxes = new()
{
new YAxisConfig { Id = "price", Side = AxisSide.Left, Format = AxisFormat.Price },
new YAxisConfig { Id = "temp", Side = AxisSide.Right, Format = AxisFormat.Degree,
Color = "#f97316", Width = 60, Min = -20, Max = 40 },
},
};
var series = new[]
{
new ChartSeries { Label = "Price", X = x, Y = prices, YAxis = "price" },
new ChartSeries { Label = "Temperature", X = x, Y = temps, YAxis = "temp" },
};
The first axis is the primary axis: thresholds, annotations and the ruler measure
in its units, and unassigned series (YAxis == null) fall back to it. Min/Max
are optional exact bounds; otherwise each axis auto-scales to its own series.
Demo
dotnet run --project ChartAI.Blazor.Demo
Building the chart engine
ChartAI.Blazor/wwwroot/chartai.js is generated. Its sources are the TypeScript files under
ChartAI.Blazor/engine/src: the chartai engine
(upstream 1.1.0) plus this project's changes (in-place data patching, several Y axes, hover
highlighting, gap handling, and so on). engine/build.ts bundles them with
Bun into that one file, GPU worker inlined, the same way upstream builds its
own dist/chart-library.js.
dotnet buildruns the bundler automatically when Bun is on the PATH and a source file is newer than the bundle. Without Bun the committed bundle is used and a warning is logged.-p:ChartAiSkipEngineBuild=truedisables the step.The bundle is built with the Bun version CI pins (1.4.2,
ChartAiBunVersionin the csproj). With another version on the PATH the build keeps the committed bundle and warns, since a different Bun can emit a different file;-p:ChartAiAnyBunVersion=truebuilds with it anyway.To build by hand, run in
ChartAI.Blazor/engine:bun run build.tsbun installthenbun run typechecktype-checks the sources (needs nothing else).The bundle is committed so that consumers and CI can build without Bun. CI rebuilds it with a pinned Bun version and fails when the committed file is stale, so rebuild and commit
wwwroot/chartai.jstogether with any change underengine/src.
Releasing
Releases are published by the Release GitHub Actions workflow. Push a tag vX.Y.Z and the
workflow packs ChartAI.Blazor X.Y.Z, pushes it to nuget.org and attaches the package to a GitHub
release. It authenticates with nuget.org Trusted Publishing, so no API key is stored; the
nuget.org account needs a trusted publishing policy for this repository and workflow file.
The tagged commit must be on master. A version that nuget.org already has fails the run, and
the GitHub release is only created once nuget.org accepted the package, so a re-pushed tag never
attaches a package that differs from the published one.
git tag v1.0.0 && git push origin v1.0.0
To build a package locally:
dotnet pack ChartAI.Blazor/ChartAI.Blazor.csproj -c Release -o artifacts
License
MIT. The bundled chartai engine is released into the public domain by its author;
see THIRD-PARTY-NOTICES.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.