Charts · Architecture
How the chart system is put together
Two engines, one experience. The rendering engines are infrastructure: they draw pixels, and everything the reader actually perceives — tooltips, legends, ranges, annotations, synchronisation, fullscreen — is owned by Luc UI.
Layers
Nothing above the adapter layer knows which engine is drawing.
Layer diagram
LUC chart system
Chart chrome ......... ChartCard, ChartHeader, ChartToolbar, ChartLegend,
RangeSelector, ChartSkeleton, ChartEmptyState,
ChartErrorState, FullscreenResearchView
Config primitives .... ChartSpec, SeriesSpec, AxisSpec, AnnotationSpec,
ChartFormat, ChartRangeSpec, syncGroup
Adapters ............. adapters/echarts (general)
adapters/lightweight (financial)
Engines .............. Apache ECharts 6.1 Apache-2.0
TradingView Lightweight Charts 5.2 Apache-2.0 + noticeDeclarative spec
defineChartSpec({
id: "btc-close-1d",
family: "line",
xKey: "time",
x: { type: "time" },
axes: [{ id: "primary", format: { kind: "currency" } }],
series: [
{ key: "close", label: "Close", tone: "accent" },
{ key: "sma50", label: "SMA 50", tone: "info", lineType: "dashed" },
],
annotations: { referenceLines: [{ value: 100000, label: "100k" }] },
interaction: { zoom: "inside" },
ranges: { ranges: ["1m", "1y", "all"], default: "1y" },
syncGroup: "research-stack",
})The token bridge
Canvas engines cannot read CSS variables. The adapter resolves the chart tokens once per theme, and normalises every colour through a canvas round-trip because the engines' own colour parsers do not understand oklch or color-mix.
/* 1. tokens.css defines semantic chart tokens per theme. */
:root { --chart-series-1: oklch(0.545 0.130 213); }
[data-theme] { --chart-series-1: oklch(0.780 0.115 205); }
/* 2. The resolver reads computed values and normalises them. */
const theme = resolveChartTheme();
// oklch() and color-mix() resolve through a canvas round-trip, because the
// engines' own colour parsers understand hex and rgb() only.
/* 3. Adapters build options from the resolved theme, never a literal. */
chart.setOption({ color: theme.palette, ...baseOption(theme, motion) });A theme flip invalidates the cached theme object, rebuilds each option and re-applies it in place — no remount, no lost zoom, no flash.
Engine decisions
Recorded so the choice stays deliberate.
| Need | Engine | Why | Runtime |
|---|---|---|---|
| General analytical charts | Apache ECharts 6.1 | Line, bar, scatter, heatmap, hierarchy, flow, radial and gauge from one modular API. Apache-2.0. | now |
| Financial price charts | Lightweight Charts 5.2 | Candles, volume, panes and crosshair are native. Apache-2.0 with a visible attribution notice. | now |
| Sparklines | Hand-rolled SVG | Eight to sixty points do not justify an engine instance per table row. | now |
| Bespoke visualisation | visx | Reserved for shapes neither engine can express. Not installed — nothing needed it this round. | deferred |
| Ultra-dense streaming | uPlot / ChartGPU | Studied for cursor synchronisation and WebGPU ceilings. Neither is justified while ECharts holds 10k points. | reference |
Escape hatch
Raw engine configuration is available, and clearly marked as advanced.
The escape hatch is a last resort. If a page needs it for something a spec could express, the spec is missing a field and should gain one instead.
// 80% path: specs and components.
<LineChart data={rows} xKey="time" series={series} interaction={{ zoom: "inside" }} />
// 20% path: reach the engine deliberately.
<LineChart extendOption={(option) => { /* the assembled ECharts option */ }} />
// Or hold the live instance for export and custom actions.
<LineChart onReady={(chart) => (chartRef.current = chart)} />Bundle discipline
Modular registration, lazy loading, split by route.
ECharts registers from echarts/core with only the chart types and components the system uses. The full bundle is never imported.
Both engines load through a dynamic import inside an effect, so neither touches window during SSR and neither is in the server bundle.
Lightweight Charts loads only on routes that render a financial chart, so an analytics-only page never pays for it.
What LUC owns
If it appears on screen, it is ours.
Tooltips — markup, typography, swatches, alignment, edge avoidance.
Legends — colour, label, visibility toggle, keyboard reachable.
Range controls — which ranges exist and what they mean.
Annotations — reference lines, bands and event markers.
Synchronisation — normalised cursor and range across engines.
Fullscreen — a research view with real chrome.
Loading, empty and error states — no engine is initialised for them.
Accessibility — a label, a description and keyboard-reachable controls.