ECharts
Gentelella ships 20 ECharts factory functions covering every common chart type. They auto-resize on viewport change, re-render on theme toggle, read all colors from CSS custom properties, and lazy-load ECharts itself only on pages that need it.
Last updated May 22, 2026
ECharts is the heaviest dependency in Gentelella — ~400 KB of charting library. The integration in src/v4/charts.js keeps that cost off pages that don’t use charts via dynamic import, while exposing 20 named factory functions for the chart types the template covers.
The contract
Add a chart by dropping a div with a data-chart="<factory-name>" attribute:
<div data-chart="revenue-line" style="width:100%;height:300px"></div>
That’s it. initCharts() from main-v4.js finds every matching element, dynamic-imports ECharts core + the chart types it needs, and instantiates each chart with tokens read from :root CSS custom properties.
The 20 factories
All exported in the charts map at the bottom of charts.js:
| Factory | Description |
|---|---|
dashboardNetwork | Stacked area: sessions + pageviews per day |
revenueLine | Smooth line chart — single metric over time |
salesBar | Vertical bar chart with rounded tops |
trafficDonut | Donut: traffic sources |
deviceUsage | Donut: device breakdown |
browsers | Donut: browser breakdown |
stackedArea | Stacked area chart, three series |
horizontalBar | Sideways bar chart for category rankings |
mixedBarLine | Bar + line on dual axes (revenue + conversions) |
radar | Multi-axis comparison radar |
gauge | Single-value gauge, 0–100 |
scatter | XY scatter with size encoding |
heatmap | Day-hour heatmap |
funnel | Conversion funnel |
candlestick | OHLC financial chart |
treemap | Hierarchical treemap |
sankey | Sankey flow diagram |
calendarHeatmap | GitHub-style contribution calendar |
gantt | Gantt timeline using ECharts’ custom series |
polarBar | Polar/radial bar chart |
Use data-chart="revenue-line" (hyphenated) — the map keys are kebab-case, the function names are camelCase.
Anatomy of a factory
Every factory has the same signature: (echarts, el, t) => instance.
echarts— the imported ECharts core moduleel— the host<div data-chart>elementt— the tokens object (see below)
A condensed example:
function revenueLine(echarts, el, t) {
const chart = echarts.init(el);
chart.setOption({
...baseOption(t),
legend: { show: false },
xAxis: {
type: 'category',
data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
axisLine: { lineStyle: { color: t.borderLight } },
axisLabel: { color: t.textMuted, fontSize: 10 }
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: t.borderLight, type: [4, 3] } },
axisLabel: { color: t.textMuted, fontSize: 10 }
},
series: [{
type: 'line',
data: [42, 56, 50, 78, 88, 96],
smooth: true,
lineStyle: { color: t.primary, width: 2 },
itemStyle: { color: t.primary },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: hexToRgba(t.primary, 0.20) },
{ offset: 1, color: hexToRgba(t.primary, 0.02) }
])
},
symbol: 'circle',
symbolSize: 6
}]
});
return chart;
}
Two things to notice:
- No hex literals. Every color is
t.primary,t.textMuted,t.borderLight, etc. — read from CSS custom properties via thetokens()function. baseOption(t)is the shared defaults — font family (Inter), font sizes, grid margins, tooltip styling. Used by every chart for consistency.
The tokens() function
const tokens = () => {
const cs = getComputedStyle(document.documentElement);
return {
primary: cs.getPropertyValue('--primary').trim(),
primaryDk: cs.getPropertyValue('--primary-dk').trim(),
azure: cs.getPropertyValue('--azure').trim(),
blue: cs.getPropertyValue('--blue').trim(),
yellow: cs.getPropertyValue('--yellow').trim(),
green: cs.getPropertyValue('--green').trim(),
red: cs.getPropertyValue('--red').trim(),
purple: cs.getPropertyValue('--purple').trim(),
text: cs.getPropertyValue('--text').trim(),
textMuted: cs.getPropertyValue('--text-muted').trim(),
borderLight: cs.getPropertyValue('--border-color-light').trim(),
bgSurface: cs.getPropertyValue('--bg-surface').trim()
};
};
If you add a new token in _tokens.scss that charts should know about (say, --accent-coral), add it here too:
return {
// ...existing tokens...
accentCoral: cs.getPropertyValue('--accent-coral').trim()
};
tokens() is called fresh at every render, so theme changes are picked up automatically.
Theme reactivity
A MutationObserver watches <html> for data-theme attribute changes:
const themeObserver = new MutationObserver((records) => {
if (records.some((r) => r.attributeName === 'data-theme')) rebuild();
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
document.documentElement.addEventListener('themechange', rebuild);
rebuild() calls instance.dispose() on every chart and re-instantiates it with fresh tokens. The full dispose-and-rebuild is needed because ECharts caches some styles internally — a plain setOption doesn’t pick up CSS-derived color changes cleanly.
The custom themechange event is what the theme generator dispatches — same handler, no observer needed for that path.
Lazy loading
initCharts() returns early if no [data-chart] element exists:
export async function initCharts() {
const elements = document.querySelectorAll('[data-chart]');
if (!elements.length) return;
// ...dynamic import of ECharts modules
}
This means pages without charts never load ECharts at all. The dashboard page pulls it in; the inbox page doesn’t.
The import itself is modular — only the chart types and components the template uses get pulled in, instead of the full ECharts barrel:
const [
echartsCore,
{ LineChart, BarChart, PieChart, RadarChart, GaugeChart, ScatterChart,
HeatmapChart, FunnelChart, CandlestickChart, TreemapChart, SankeyChart,
CustomChart },
{ GridComponent, TooltipComponent, LegendComponent,
VisualMapComponent, PolarComponent, CalendarComponent },
{ CanvasRenderer }
] = await Promise.all([
import('echarts/core'),
import('echarts/charts'),
import('echarts/components'),
import('echarts/renderers')
]);
echartsCore.use([
LineChart, BarChart, PieChart, RadarChart, GaugeChart, ScatterChart,
HeatmapChart, FunnelChart, CandlestickChart, TreemapChart, SankeyChart,
CustomChart, GridComponent, TooltipComponent, LegendComponent,
VisualMapComponent, PolarComponent, CalendarComponent, CanvasRenderer
]);
If you drop a chart type (say you never use SankeyChart), remove it from both the destructure and the use() call. Vite tree-shakes the rest.
Skeleton placeholders
While ECharts is loading, every [data-chart] element gets a chart-skeleton class that renders a subtle pulsing gradient placeholder:
elements.forEach((el) => {
if (!el.children.length && !el.classList.contains('skeleton')) {
el.classList.add('skeleton', 'chart-skeleton');
}
});
Once the chart instance mounts into the element, the skeleton class is removed. This prevents the layout shift you’d otherwise get from an empty div suddenly becoming 300px tall.
Adding a new chart type
Three steps:
1. Write the factory
In charts.js, append a function with the standard signature:
function quickSpark(echarts, el, t) {
const chart = echarts.init(el);
chart.setOption({
...baseOption(t),
grid: { left: 0, right: 0, top: 4, bottom: 0 },
xAxis: { show: false, type: 'category' },
yAxis: { show: false, type: 'value' },
series: [{
type: 'line',
data: [12, 14, 18, 22, 28, 32, 40],
smooth: true,
lineStyle: { color: t.primary, width: 2 },
itemStyle: { color: t.primary },
symbol: 'none',
areaStyle: { color: hexToRgba(t.primary, 0.15) }
}]
});
return chart;
}
2. Register it
Add a key to the charts map:
const charts = {
// ...existing factories
'quick-spark': quickSpark
};
3. Use it on a page
<div data-chart="quick-spark" style="width:120px;height:32px"></div>
Restart the dev server — the chart picks up automatically because initCharts() discovers it via the [data-chart] query selector.
Per-instance data
The current pattern bakes data into each factory. If you need the same chart type with different data per instance, two options:
Read data attributes from the host element:
function quickSpark(echarts, el, t) {
const data = (el.dataset.points || '12,14,18,22').split(',').map(Number);
const colorKey = el.dataset.color || 'primary';
const color = t[colorKey];
const chart = echarts.init(el);
chart.setOption({ /* use data + color */ });
return chart;
}
<div data-chart="quick-spark" data-color="success" data-points="100,80,90,120,140"></div>
Or fetch data from your backend after init:
function quickSpark(echarts, el, t) {
const chart = echarts.init(el);
chart.showLoading();
fetch(el.dataset.url)
.then((r) => r.json())
.then((data) => {
chart.hideLoading();
chart.setOption({ /* render with fetched data */ });
});
return chart;
}
ECharts’ showLoading() displays a built-in spinner during the fetch.
Charts vs. SVG charts
The template has a separate production/other_charts.html page that demos a few smaller chart types as inline SVG — no library, just hand-written paths. These are good for sparklines or single-value widgets where pulling in ECharts is overkill.
The two coexist: pick ECharts for anything complex (axes, tooltips, multiple series); pick inline SVG for visual flourishes in a card header. The bundle stays small because the SVG charts have zero JS cost.
See also
- Theming — the tokens every chart reads
- Theme generator — the
themechangeevent that triggers chart rebuild - Architecture — the lazy-load gate that keeps ECharts off pages without
[data-chart]