G Gentelella v4.0.0

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:

FactoryDescription
dashboardNetworkStacked area: sessions + pageviews per day
revenueLineSmooth line chart — single metric over time
salesBarVertical bar chart with rounded tops
trafficDonutDonut: traffic sources
deviceUsageDonut: device breakdown
browsersDonut: browser breakdown
stackedAreaStacked area chart, three series
horizontalBarSideways bar chart for category rankings
mixedBarLineBar + line on dual axes (revenue + conversions)
radarMulti-axis comparison radar
gaugeSingle-value gauge, 0–100
scatterXY scatter with size encoding
heatmapDay-hour heatmap
funnelConversion funnel
candlestickOHLC financial chart
treemapHierarchical treemap
sankeySankey flow diagram
calendarHeatmapGitHub-style contribution calendar
ganttGantt timeline using ECharts’ custom series
polarBarPolar/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 module
  • el — the host <div data-chart> element
  • t — 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 the tokens() 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 themechange event that triggers chart rebuild
  • Architecture — the lazy-load gate that keeps ECharts off pages without [data-chart]