Charting candidates
Two choices are open on this page, and both are made here rather than argued. The first is the series palette: which six colors a chart takes, and in what order. The second is the library: four candidates, the same three charts, the same numbers, the same card.
The series are synthetic. Nothing on this page is a measurement of any part.
Palette candidates
Section titled “Palette candidates”--helia-chart-1 through --helia-chart-6 used to alias the advisory tones,
so the first two series were the green and the blue that a callout uses to mean
success and information, and a chart wore the status vocabulary whether or not
it meant any of it. The shipped palette below replaces all six: a light modern
blue, the page ink, and a slate ramp. It is what the tokens resolve to today,
and the gallery charts and the React chart follow it without being touched.
The three candidates after it are kept for the record. They are the other directions the palette could take, drawn the same way so the shipped one can be read against them. None of the four uses a green, and none uses a blue that could be read as a status color.
Every step is derived from the brand scale in the package sheets: the slate
ramp is the OKLCH hue and chroma of --helia-accent-slate with the lightness
stepped, and the chromatic steps take the hue and chroma of
--helia-accent-cyan, --helia-accent-orange, --helia-accent-purple and
--helia-accent-pink at whatever lightness the ground calls for. The two
grounds stand side by side because the page is only ever in one of them and a
palette has to survive both.
Under each palette are the same two charts: a two-series line and a grouped
bar, drawn by the shipped Chart part with --helia-chart-1 through
--helia-chart-6 set on a wrapper. Six declarations is the whole of a palette
change, so nothing on this page is a mock-up of one. The shipped block sets the
same values the sheet does, and the smoke suite reads them back against the
document to keep the two from drifting apart.
Each palette also carries the smallest CIEDE2000 distance between any two of its six steps, which is the number that decides whether a reader can tell one series from another. Roughly: around 1 is the just-noticeable difference for two patches touching each other, and two marks separated across a chart need a good deal more than that. It is a floor, not an average, so a palette with five well-spread steps and one near-duplicate scores as the near-duplicate.
--helia-product-accent resolves to --helia-accent-slate on this site, so
the product accent in A and B is a slate step, and C’s highlighted series is
amber rather than the accent: a monochrome palette whose one highlight is
another slate is not a highlight. On a product surface that sets the dial to a
product hue, the accent steps move with it.
Shipped. Blue and ink
Section titled “Shipped. Blue and ink”| Step | Role | Dark | Light |
|---|---|---|---|
--helia-chart-1 | Blue | #7fb0ff | #4f8ff7 |
--helia-chart-2 | Page ink | #ffffff | #17181c |
--helia-chart-3 | Slate 1 | #9ba5ba | #4e586c |
--helia-chart-4 | Slate 2 | #828ca2 | #656f84 |
--helia-chart-5 | Slate 3 | #6a758a | #768096 |
--helia-chart-6 | Slate 4 | #596378 | #8892a8 |
Closest pair, CIEDE2000: 6.9 on dark, between steps 5 and 6, 6.4 on light, between steps 5 and 6.
Dark
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
Light
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
A. Ink-led
Section titled “A. Ink-led”| Step | Role | Dark | Light |
|---|---|---|---|
--helia-chart-1 | Page ink | #f0f1f2 | #111318 |
--helia-chart-2 | Product accent | #a1abc0 | #687287 |
--helia-chart-3 | Slate mid | #768096 | #394255 |
--helia-chart-4 | Slate far | #4e586c | #949fb4 |
--helia-chart-5 | Amber | #fd9146 | #c46101 |
--helia-chart-6 | Violet | #8e84ff | #624bd6 |
Closest pair, CIEDE2000: 14.1 on dark, between steps 2 and 3, 16.1 on light, between steps 2 and 4.
Dark
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
Light
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
B. Accent-led
Section titled “B. Accent-led”| Step | Role | Dark | Light |
|---|---|---|---|
--helia-chart-1 | Product accent | #8892a8 | #687287 |
--helia-chart-2 | Teal | #02a5b3 | #01818c |
--helia-chart-3 | Amber | #da721e | #ae5500 |
--helia-chart-4 | Violet | #887cff | #6a55e1 |
--helia-chart-5 | Rose | #e35b8e | #bd376e |
--helia-chart-6 | Page ink | #f0f1f2 | #111318 |
Closest pair, CIEDE2000: 15.2 on dark, between steps 1 and 4, 16.5 on light, between steps 1 and 4.
Dark
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
Light
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
C. Monochrome plus one
Section titled “C. Monochrome plus one”| Step | Role | Dark | Light |
|---|---|---|---|
--helia-chart-1 | Highlight | #fd9146 | #c46101 |
--helia-chart-2 | Slate 1 | #d5def0 | #131a2b |
--helia-chart-3 | Slate 2 | #aeb8cc | #2f384a |
--helia-chart-4 | Slate 3 | #8892a8 | #4e586c |
--helia-chart-5 | Slate 4 | #687287 | #707a90 |
--helia-chart-6 | Slate 5 | #4c5569 | #949fb4 |
Closest pair, CIEDE2000: 9.4 on dark, between steps 2 and 3, 9.5 on light, between steps 2 and 3.
Dark
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
Light
Inference latency
Milliseconds per inference against input length, in tokens
- heliaRT
- heliaAOT
Energy per inference
Microjoules per inference against deployment target
- heliaRT
- Reference
Library candidates
Section titled “Library candidates”Both questions below are now settled: Observable Plot is the static layer and
Apache ECharts is the interactive one, so Chart, ChartGroup and ChartPlot
draw with Plot and ChartInteractive draws with ECharts wherever hovering,
zooming, toggling or selecting is the point.
The comparison stays here because it is the evidence for that split rather than an open question: the rows below are what each library costs in island bytes, what it can render on the server, how it takes a theme, and which interactions it has without being written by hand.
Every chart carries a title and a subtitle, and the subtitle says what is
plotted against what, so no axis is titled. Gridlines are horizontal only and
drawn in the hairline token. All four rows take their series from
--helia-chart-1 through --helia-chart-3, the same shipped palette the
section above draws, so a difference between two rows is the library and never
the colors.
The facts
Section titled “The facts”| Library | License | Version | Island JavaScript | Server render | Theming |
|---|---|---|---|---|---|
MUI X Charts@mui/x-charts | MIT | 9.14.0 | 487 KB | In part. The surface and the legend are in the HTML; the marks wait to be measured. | Colors are props, gridlines need an sx rule, and the rest is MUI theme defaults. |
Rechartsrecharts | MIT | 3.8.0 | 429 KB | No. ResponsiveContainer has nothing to measure, so the HTML holds an empty container. | Every color is a prop on the element that draws it, and there is no theme to override. |
Observable Plot@observablehq/plot | ISC | 0.6.17 | 297 KB | No. Plot.plot builds against a document, so the island is client only. | One color scale and one style object per figure. |
Apache EChartsecharts | Apache-2.0 | 6.1.0 | 667 KB | Yes. echarts.init(null, ...) with ssr lays the figure out with no document, so the SVG is in the HTML; the island then replaces it with a live instance, and the interactions arrive with that. | A JSON theme registered once. Every value in it is a var(), so the SVG follows the theme toggle. |
License and version are the installed packages’, read from node_modules. The
byte figures are minified JavaScript, not compressed, summed over the chunks
this page loads for that island and with the shared React runtime taken out,
because the page pays for that once whichever library wins. The four overlap:
around 79 KB of d3 is common to the first three, so this page loads less than
the column adds up to. Recharts is already a peer dependency of the package, so
its bytes are a cost the React layer pays anyway; the other three would be new.
ECharts is imported through its tree-shaking entry points rather than its
barrel, so its figure is the one a site would actually ship.
The interactions
Section titled “The interactions”| Interaction | MUI X Charts | Recharts | Observable Plot | Apache ECharts |
|---|---|---|---|---|
| Tooltip A value readout that follows the pointer. | Native ChartsTooltip, on by default. | Native The Tooltip element. | Native The tip mark under a pointer transform, and crosshair. | Native tooltip, on the item or the axis. |
| Zoom Narrowing the domain by wheel, drag or slider. | No Not in the community package: the zoom plugin is x-charts-pro. | Native The Brush element gives a range slider under the plot. | No No zoom, and no interaction to build on. | Native dataZoom, as a slider under the plot or on the wheel. |
| Legend toggle Clicking a legend entry removes that series. | Native hiddenItems and onHiddenItemsChange on the chart. | By hand Legend onClick plus the series' own hide prop, held in state. | By hand Plot legends are static swatches, so the island filters the data and rebuilds. | Native The legend toggles the series it names. |
| Selection Dragging over marks selects them and reports which. | No The brush gestures and overlay are internals with no public props. | No Brush narrows the domain; it does not report a set of marks. | No The pointer transform picks one mark, never a set. | Native brush returns the selected marks as an event. |
| Animation Marks are drawn in rather than appearing. | Native On unless skipAnimation is set. | Native isAnimationActive, on by default. | No A figure is built and appended; there is nothing to animate. | Native On by default, and configurable. |
| Streaming Appending points without rebuilding the chart. | By hand Re-render with new series; there is no append. | By hand New data prop; the animation replays each time. | By hand Rebuild the whole figure and swap the node. | Native appendData adds points without a rebuild. |
Native means the library does it from options or props. By hand means the
island writes it and the library only gets out of the way. No means neither.
Each cell was checked against the installed package’s own type declarations,
not its documentation.
MUI X Charts
Section titled “MUI X Charts”The community charts package is MIT. It needs @mui/material as a peer, and
although the @emotion peers are marked optional in this major, MUI’s styling
engine loads @emotion/react at runtime and the server render fails without
it, so @emotion/react and @emotion/styled came in as well. @mui/system
arrives with @mui/material. Four new packages, then, and MUI’s styling layer
alongside its charts.
- heliaRT
- heliaAOT
- heliaRT
- Reference
- int8
- fp16
Recharts
Section titled “Recharts”Already a peer dependency and already wrapped by the package’s shadcn chart component. Here it is used through its own primitives rather than the wrapper, so the comparison is between libraries and not between a library and a wrapper.
Observable Plot
Section titled “Observable Plot”Not a React library at all: Plot.plot builds a figure element against a DOM
and hands it back, so an island owns the node and the width. That makes it the
cheapest to drop into a page that is not React, and the one that needs an
adapter in a page that is.
This row is hydrated, and it is Plot at its ceiling rather than at its default.
The tooltips are the tip mark under a pointer transform and the crosshair
interaction, both of which cost a line each. The legend is not Plot’s: Plot’s
own legend is a static swatch strip, so what is above each figure is a set of
buttons, and a click filters the data and rebuilds the whole figure. There is
no zoom and no selection below, because there is nothing in Plot to build
either one on.
Apache ECharts
Section titled “Apache ECharts”Apache-2.0, and the only candidate here that treats a chart as something a reader operates. The row below has a tooltip on hover, a legend that toggles the series it names, a zoom on the line as both a slider and the wheel, a brush over the scatter that selects the marks inside it, and an entrance animation. None of that is written here; all of it is options.
The figures in the HTML are ECharts’ own server render. echarts.init(null, ...) with ssr lays a chart out with no document at all, so the build writes
the SVG into the page and the island’s job on mount is to replace a picture
with an instance. The theme is a JSON object registered once, and every color
in it is a var() rather than a hue, which is what lets a figure rendered at
build follow the theme toggle — the same trick the Astro Chart part uses, and
the reason the SVG renderer is used on the client as well as on the server.