Charts
Four charting libraries — Recharts, ApexCharts, ECharts, and Chart.js — each demoed on its own page, all pulling colors from the token-aware useChartTokens() hook.
Luminaux intentionally demos four chart libraries rather than committing to one, so a buyer can adopt
whichever they already know or prefer. Every chart pulls its colors from useChartTokens(), a hook
that reads the app's raw --* design tokens off <html> and re-reads on theme / skin / OLED change, so
all charts re-skin live in light and dark. Chart pages are React.lazy-loaded so they split out of
the main bundle.
| Library | Packages | Route | Strengths |
|---|---|---|---|
| Recharts | recharts | /charts/recharts | React-first, declarative (charts are JSX). Great default for most dashboards. |
| ApexCharts | apexcharts + react-apexcharts | /charts/apexcharts | Config-object API, rich built-in interactivity, easy gradients/donuts/heatmaps. |
| ECharts | echarts + echarts-for-react | /charts/echarts | The most powerful/flexible for large or unusual visualizations. |
| Chart.js | chart.js + react-chartjs-2 | /charts/chartjs | Canvas-based, small and fast. Also powers cards in the widgets library. |
All four demo the same core dataset (a 12-month revenue series, a last-year comparison, a 4-slice traffic
breakdown) as six panels each: an area/line revenue chart, a grouped bar chart, a two-series line
comparison, a donut/pie, plus two library-specific extras (Recharts: radar + radial bar; ApexCharts:
radial bar + heatmap; ECharts: radar + gauge; Chart.js: radar + polar area). Month labels are derived from
the active language via monthsShort(i18n.language) — never hardcoded.
You don't have to keep all four
Pick one library and delete the other demo pages/deps for a leaner bundle. If you keep only one, Chart.js is already a dependency of the widgets library.
Architecture & files
| Path | Responsibility |
|---|---|
src/hooks/useChartTokens.ts | Reads raw --* tokens off <html>; exposes semantic colors + a skin-aware series[]; re-reads on theme/skin/OLED change. Shared by all chart pages & widgets. |
src/pages/charts/RechartsPage.tsx | Recharts demo (area / bar / line compare / donut / radar / radial bar). |
src/pages/charts/ApexChartsPage.tsx | ApexCharts demo (area / bar / line / donut / radialBar / heatmap). |
src/pages/charts/EChartsPage.tsx | ECharts demo (line-area / bar / line compare / pie / radar / gauge). |
src/pages/charts/ChartjsPage.tsx | Chart.js demo (line-area / bar / line compare / doughnut / radar / polar area) — includes the ChartJS.register(…) setup. |
src/lib/dates.ts | monthsShort(lang) — locale-derived month labels (dates never get i18n keys). |
src/routes.tsx | All four chart routes are React.lazy-loaded. |
Each page renders a <PageHeader> and wraps every chart in a Panel (with fill + mandatory
title/subtitle) inside a Stagger/StaggerItem entrance, on a responsive grid-cols-1 lg:grid-cols-2
grid.
useChartTokens()
The single most important pattern: never hardcode chart colors — read them from useChartTokens() so
the chart follows the theme, dark mode, and every color skin.
export interface ChartTokens {
primary: string
success: string
warning: string
danger: string
info: string
foreground: string
mutedForeground: string
border: string
surface: string
surfaceMuted: string
background: string
/** Categorical series palette derived from the tokens above. */
series: string[]
}Key behaviors:
- Reads raw
--*vars, not the Tailwind--color-*aliases (which aren't emitted as real CSS variables). This matches the project's SCSS/token rule. seriesis skin-aware (built bybuildSeries): if the active skin defines a categorical--c1..--c6palette (as Prism does) those values are used verbatim; otherwise it derives a distinct multi-hue set anchored on the skin's primary hue (the first entry is the exact--primary, the rest are hue-rotations with normalized saturation/lightness, lightened in dark mode); if--primaryisn't a parseable hex it falls back to[primary, success, warning, info, danger].- Re-reads on change. A
MutationObserveron<html>watches theclass(dark mode),data-skin, anddata-oledattributes, so charts recolor live — no reload. - SSR-safe fallback. When
documentis undefined the initial state is a staticEMPTYpalette.
Feed axis/grid strokes from tk.mutedForeground / tk.border, series fills from tk.primary
(single-series) or tk.series (categorical), tooltip surfaces from tk.surface / tk.foreground. For
two-series comparisons the demo pages pair tk.primary + tk.info — the two tokens every skin
overrides — rather than dipping into success/warning/danger.
Per-library APIs & examples
Charts are JSX components (AreaChart, BarChart, LineChart, PieChart, RadarChart,
RadialBarChart, plus Area/Bar/Line/Pie/Cell/Radar/RadialBar, axes, grids, Tooltip,
Legend) wrapped in ResponsiveContainer. Colors are passed as props: stroke={tk.primary}, fill,
contentStyle for the tooltip.
export function RevenueArea({lang}: {lang: string}) {
const tk = useChartTokens()
const revenue = monthsShort(lang).map((m, i) => ({m, v: values[i]}))
const tooltip = {background: tk.surface, border: `1px solid ${tk.border}`, borderRadius: 10, color: tk.foreground}
return (
<ResponsiveContainer width="100%" height={280}>
<AreaChart data={revenue}>
<defs>
<linearGradient id="rcArea" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={tk.primary} stopOpacity={0.35} />
<stop offset="100%" stopColor={tk.primary} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke={tk.border} vertical={false} />
<XAxis dataKey="m" stroke={tk.mutedForeground} fontSize={12} tickLine={false} axisLine={false} />
<YAxis stroke={tk.mutedForeground} fontSize={12} tickLine={false} axisLine={false} />
<Tooltip contentStyle={tooltip} cursor={{stroke: tk.border}} />
<Area type="monotone" dataKey="v" stroke={tk.primary} strokeWidth={2} fill="url(#rcArea)" />
</AreaChart>
</ResponsiveContainer>
)
}For a categorical pie, cycle the derived palette: <Cell fill={tk.series[i % tk.series.length]} />.
Recharts needs a ResponsiveContainer with an explicit height and a width-bearing parent.
Configuration & customization
The rule across all four libraries is identical: source every color from tk. A few patterns worth
calling out:
- Single-series accent →
tk.primary; second series →tk.info. - Categorical (pie/donut/multi-series) →
tk.series, cycled withi % tk.series.length. - Axes / grid lines →
tk.mutedForeground(labels) andtk.border(grid strokes). - Tooltip surface →
tk.surfacebackground +tk.foregroundtext +tk.borderoutline. (The Chart.js page inverts this for a high-contrast tooltip; both are token-pure.) - Transparent chart background so the
Panelsurface shows through — Apex useschart.background: 'transparent', ECharts usesbackgroundColor: 'transparent'; Recharts and Chart.js are transparent by default.
Lazy-loading for bundle splitting
Charting libraries are heavy, so every chart page is React.lazy-loaded in src/routes.tsx. Each library
then lands in its own Vite chunk that's only fetched when the user visits that page. While a lazy page
loads it shows the shared PageLoader Suspense fallback (gated by config.routeLoader).
Best practices
- Always read colors from
useChartTokens()— never hardcode hex. Non-negotiable for Chart.js: a canvas cannot resolve CSS variables. - Use
tk.seriesfor categorical data, cycling with the modulo index. Pairtk.primary+tk.infofor two-series compares. - Keep chart backgrounds transparent so the
Panelsurface shows through in both light and dark. - Set
fontFamily: 'inherit'(Apex) so charts use the app's skin-aware font. - Register Chart.js pieces at module scope — one
ChartJS.register(…)per module, listing exactly what you use. - Derive date labels from the locale (
monthsShortinsrc/lib/dates.ts) — dates never get i18n keys. - Lazy-load chart pages — these libs are heavy; keep them out of the main bundle.
- Make charts responsive — Recharts via
ResponsiveContainer; Apex/ECharts fill their container and take aheight; Chart.js needsmaintainAspectRatio: falseinside a fixed-height wrapper.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Chart colors don't change with dark mode / skin | Colors were hardcoded. Route every color through useChartTokens(). |
| Colors look wrong / transparent when read in plain CSS | You read a --color-* alias. Use the raw tokens (--primary, --surface, …). |
Chart.js throws "category" is not a registered scale | Missing registration — add the scale/element/controller to ChartJS.register(…). |
| Chart.js chart stretches / wrong height | Set responsive: true + maintainAspectRatio: false and give it a fixed-height wrapper div. |
| ApexCharts tooltip/text stays light in dark mode | Pass theme: {mode} and tooltip: {theme} derived from the .dark class. |
| Chart shows a solid box behind it | Chart background isn't transparent — set background/backgroundColor: 'transparent'. |
| Series colors all look the same on a custom skin | The derived palette anchors on --primary. Accept the hue-rotated set, or give the skin its own --c1..--c6 (Prism pattern). |
| Recharts chart has zero size | Wrap it in ResponsiveContainer with an explicit height and a width-bearing parent. |
FAQ
Why ship four chart libraries? So a buyer can choose the one they prefer. Each is demoed with the same six-panel layout and dataset for easy comparison.
Which should I use? Recharts by default (declarative React). ApexCharts for rich built-in interactivity. ECharts for the most powerful/complex visualizations. Chart.js for a small, fast canvas renderer — and it's what the widgets library already uses.
How is the categorical palette built? buildSeries in useChartTokens.ts: skin-provided --c1..--c6
if present (Prism), otherwise the exact primary plus five hue-rotations of it, falling back to
[primary, success, warning, info, danger] when the primary isn't a parseable hex.
Where else is Chart.js used? The widgets library — DonutWidget, GaugeWidget, and the MiniBarChart
primitive are react-chartjs-2 charts, colored from the same useChartTokens() hook.
Related
Tables & Data Grid
Shares useChartTokens and the lazy-load pattern (AG Grid theme from the same tokens).
Widgets
Its donut/gauge/mini-bar cards are Chart.js via the same token hook.
Design tokens & dark mode
The --* tokens the charts read.
Design skins
How skins swap the tokens charts pick up, and Prism's categorical palette.
Motion & effects
The Stagger/StaggerItem entrance used on chart pages.
Was this page helpful?
