A Adminator v4.1.4

Charts

Adminator ships Chart.js 4.5 integrated with the design token system. Seven seeded chart examples come pre-themed, and adding a new chart is one seed function plus one canvas element. All charts re-render automatically on theme toggle.

Last updated May 21, 2026

The 2026 redesign wraps real Chart.js 4.5 with a thin layer that:

  • Reads CSS variables for colors instead of hard-coded hex
  • Re-instantiates every chart when the theme changes
  • Lets you declare new charts as <canvas data-chart-key="..."> plus one seed function

The integration lives in src/assets/scripts/2026/charts.js — about 260 lines, including all six demo seeds.

How a chart gets on the page

Two pieces. First, the HTML:

<div class="chart-canvas-wrap">
  <canvas data-chart-key="revenue-line"></canvas>
</div>

Then, a seed function in charts.js:

export const SEEDS = {
  'revenue-line': (t) => ({
    type: 'line',
    data: {
      labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
      datasets: [{
        label: 'Revenue',
        data: [42, 56, 50, 78, 88, 96],
        borderColor: t.primary,
        backgroundColor: `${t.primary}20`,
        tension: 0.35,
        fill: true,
        pointRadius: 0,
        pointHoverRadius: 5,
        borderWidth: 2.5,
      }],
    },
    options: { /* ... */ },
  }),
  // ...
};

When the page loads, the charts module:

  1. Reads CSS variables via tokens() → returns { primary, success, danger, text, border, ... }
  2. Calls Chart.defaults.font.family = 'Inter', etc. — sets shared defaults
  3. Finds every <canvas data-chart-key="..."> element
  4. Looks up the matching seed in SEEDS, calls it with the tokens
  5. Instantiates a new Chart with the returned config

On theme change, a MutationObserver destroys all instances and runs the whole sequence again with the new tokens.

The tokens() function

The bridge between CSS variables and Chart.js:

function tokens() {
  const cs = getComputedStyle(document.documentElement);
  return {
    primary: cs.getPropertyValue('--primary').trim(),
    success: cs.getPropertyValue('--success').trim(),
    danger:  cs.getPropertyValue('--danger').trim(),
    warning: cs.getPropertyValue('--warning').trim(),
    info:    cs.getPropertyValue('--info').trim(),
    purple:  cs.getPropertyValue('--purple').trim(),
    pink:    cs.getPropertyValue('--pink').trim(),
    orange:  cs.getPropertyValue('--orange').trim(),
    teal:    cs.getPropertyValue('--teal').trim(),
    text:    cs.getPropertyValue('--t-base').trim(),
    muted:   cs.getPropertyValue('--t-muted').trim(),
    light:   cs.getPropertyValue('--t-light').trim(),
    border:  cs.getPropertyValue('--border').trim(),
    soft:    cs.getPropertyValue('--border-soft').trim(),
    bg:      cs.getPropertyValue('--bg-card').trim(),
  };
}

If you add a custom palette token in _tokens.scss (see Theming) and want it in your charts, add it here too.

Transparency without rgba()

Chart.js expects hex colors for borderColor but rgba for backgroundColor (so the fill is translucent). The pattern in the seeds:

borderColor: t.primary,
backgroundColor: `${t.primary}20`,   // 20 = ~12% opacity in 2-digit hex

The 8-character hex format (#RRGGBBAA) lets you append an alpha without converting to rgba(). Cheat sheet:

  • 10 ≈ 6%
  • 20 ≈ 12%
  • 40 ≈ 25%
  • 80 ≈ 50%
  • C0 ≈ 75%
  • FF = 100%

Most modern browsers support this (Chrome 62+, Firefox 49+, Safari 9.1+).

Adding a new chart

Three steps:

1. Add a canvas to your page

<div class="card">
  <div class="card-head">
    <h3>Active sessions</h3>
  </div>
  <div class="chart-canvas-wrap">
    <canvas data-chart-key="sessions-bar"></canvas>
  </div>
</div>

.chart-canvas-wrap gives the canvas a sensible aspect ratio and minimum height — defined in _charts.scss.

2. Add a seed

In charts.js, append to SEEDS:

'weekly-sessions': (t) => ({
  type: 'bar',
  data: {
    labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    datasets: [
      {
        label: 'Desktop',
        data: [124, 168, 192, 184, 210, 156, 142],
        backgroundColor: t.primary,
        borderRadius: 4,
        borderSkipped: false,
      },
      {
        label: 'Mobile',
        data: [82, 96, 118, 124, 140, 168, 174],
        backgroundColor: t.teal,
        borderRadius: 4,
        borderSkipped: false,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    scales: {
      x: { grid: { display: false }, ticks: { color: t.muted } },
      y: { grid: { color: t.soft }, ticks: { color: t.muted } },
    },
    plugins: {
      legend: { display: true },
    },
  },
}),

3. That’s it

Refresh the page. The canvas picks up the seed by its data-chart-key. Theme toggle works automatically — the init code observes data-theme on <html> via MutationObserver, then destroys and re-instantiates every Chart instance with fresh tokens when it changes.

Re-using a seed across multiple canvases

Seed functions take a single argument — the tokens object — so the same key drives every canvas with that data-chart-key. Two canvases pointing at data-chart-key="revenue-line" will render the same chart twice.

If you need per-instance variation (different data, different color), the simplest pattern is one seed per variant:

'kpi-revenue':  (t) => ({ type: 'line', data: { datasets: [{ data: [42, 56, 50, 78], borderColor: t.primary }] }, options: { /* ... */ } }),
'kpi-users':    (t) => ({ type: 'line', data: { datasets: [{ data: [12, 14, 18, 22], borderColor: t.success }] }, options: { /* ... */ } }),

This trades a little duplication for clarity — each seed is self-contained, and the init code stays simple. If you have many KPI sparklines and want a factory pattern instead, that’s a fork-and-customize from the existing initCharts() in charts.js.

Chart types included

All six demos live in src/charts.html, plus the monthly line chart on the dashboard. Use them as starting points.

ChartSeed keyUse case
Line + area fill (revenue)revenue-lineTrend over time, comparison vs. previous period
Bar (grouped — channels)channels-barCategorical comparison across channels
Doughnut (devices)devices-doughnutPercentage breakdown of a small set (3–5 slices)
Radar (traffic sources)sources-radarMulti-axis comparison
Stacked bar (MRR breakdown)mrr-stackedPart-to-whole over time
Area filled (sessions)sessions-areaSmooth continuous trends
Dashboard monthly linedashboard-monthlyThe featured chart on index.html

Default styling

applyDefaults() in charts.js sets globally:

Chart.defaults.font.family = "'Inter', system-ui, sans-serif";
Chart.defaults.font.size = 12;
Chart.defaults.color = t.muted;
Chart.defaults.borderColor = t.soft;
Chart.defaults.plugins.legend.position = 'bottom';
Chart.defaults.plugins.legend.labels.usePointStyle = true;
Chart.defaults.plugins.legend.labels.padding = 16;
Chart.defaults.plugins.tooltip.backgroundColor = t.text;
Chart.defaults.plugins.tooltip.titleColor = t.bg;
Chart.defaults.plugins.tooltip.bodyColor = t.bg;
Chart.defaults.plugins.tooltip.cornerRadius = 6;
Chart.defaults.plugins.tooltip.displayColors = false;

This is what gives every chart in the template the same legend style, the same tooltip look, the same grid line color. If you want a different default — say a top-positioned legend — change it here once instead of per-seed.

A note on bundle size

Chart.js plus its registerables (all controllers + scales + elements registered) adds ~180 KB minified to the bundle. Adminator’s webpack config splits it into vendor-chartjs.js so pages without charts can lazy-load it.

If you’re tight on bundle size and only use one or two chart types, you can import just those controllers instead of registerables:

// Smaller bundle — line and bar only
import { Chart, LineController, BarController, LineElement, BarElement,
         PointElement, CategoryScale, LinearScale, Tooltip, Legend, Filler } from 'chart.js';

Chart.register(LineController, BarController, LineElement, BarElement,
               PointElement, CategoryScale, LinearScale, Tooltip, Legend, Filler);

This drops ~80 KB.