Skip to content

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 + notice

Declarative 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.

Chart engines
NeedEngineWhyRuntime
General analytical chartsApache ECharts 6.1Line, bar, scatter, heatmap, hierarchy, flow, radial and gauge from one modular API. Apache-2.0.now
Financial price chartsLightweight Charts 5.2Candles, volume, panes and crosshair are native. Apache-2.0 with a visible attribution notice.now
SparklinesHand-rolled SVGEight to sixty points do not justify an engine instance per table row.now
Bespoke visualisationvisxReserved for shapes neither engine can express. Not installed — nothing needed it this round.deferred
Ultra-dense streaminguPlot / ChartGPUStudied 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.

Luc UI v1.0Homedark theme