C CoolAdmin v3.3.0

Charts

CoolAdmin uses Chart.js 4.5.1 with a thin factory layer. Charts are guarded by withCanvas() so main-vanilla.js stays safe to include on every page, and the dashboard's Refresh button re-runs every registered chart through a single registry.

Last updated May 22, 2026

CoolAdmin ships Chart.js 4.5.1 as the only chart library. It’s loaded as a UMD bundle (vendor/chartjs/chart.umd.js-4.5.1.min.js) from a classic <script> tag — not an ES module — and exposes the global Chart.

All chart definitions live in js/main-vanilla.js. The file is included on every page and stays cheap to ship because every chart is gated by withCanvas(id, fn) — when the target canvas doesn’t exist on the current page, the chart factory short-circuits.

The contract

Add a chart by dropping a <canvas> with an id:

<canvas id="my-chart" width="600" height="240"></canvas>

Register a factory in main-vanilla.js:

withCanvas('my-chart', (ctx) => {
  new Chart(ctx, {
    type: 'line',
    data: { /* ... */ },
    options: { /* ... */ }
  });
});

That’s it. The chart renders on pages with #my-chart and is skipped on pages without it.

withCanvas — the chart factory guard

const __chartRegistry = [];

function withCanvas(id, render) {
  __chartRegistry.push({ id, render });
  const ctx = document.getElementById(id);
  if (!ctx) return;
  render(ctx);
}

Two responsibilities:

  1. Page-safe execution. If document.getElementById(id) returns null, the render function never runs. No console errors, no thrown exceptions on pages without that canvas.
  2. Registry for the Refresh button. Every chart pushes itself into __chartRegistry so the dashboards’ Refresh button can re-render the entire set via window.__coolReinit().

The pattern lets main-vanilla.js be included on every page — pricing, login, error, etc. — without complaining about missing chart canvases.

The dashboards’ “Refresh” button

Every dashboard has a Refresh button that swaps KPI cards and the primary chart to skeleton placeholders for ~1.2 seconds, then re-renders.

The mechanism leans on the registry:

window.__coolReinit = function () {
  __chartRegistry.forEach(({ id, render }) => {
    const ctx = document.getElementById(id);
    if (!ctx) return;
    // …destroy existing Chart instance via Chart.getChart(ctx), then re-run render(ctx)
  });
};

The skeleton CSS lives in src/scss/app/_skeleton.scss — a shimmer pulse on placeholder divs that takes over the chart’s container during the 1.2s window. The refresh button toggles a class on the dashboard wrapper, waits, then calls __coolReinit() to repaint the actual charts.

This is more of a polish detail than a critical feature, but it’s why every chart has to push itself into the registry — without that, Refresh can’t find them.

Shared option helpers

Three helpers reused across chart definitions:

sparklineOptions({ tooltip })

The inline mini-chart used in dashboard widget cards. Hidden axes, no legend, transparent grids, near-zero point radius:

function sparklineOptions({ tooltip = false } = {}) {
  return {
    maintainAspectRatio: false,
    responsive: true,
    plugins: { legend: { display: false } },
    scales: {
      x: { grid: { color: 'transparent' }, ticks: { font: { size: 2 }, color: 'transparent' } },
      y: { display: false, ticks: { display: false } }
    },
    elements: {
      line: { borderWidth: 0 },
      point: { radius: 0, hitRadius: 10, hoverRadius: 4 }
    }
    // …`tooltip: true` adds the lightTooltip block too
  };
}

Use this for any tiny inline chart that should look like a sparkline — no axes, no grid, no legend.

lightTooltip

A reusable tooltip style for larger dashboard line charts:

const lightTooltip = {
  backgroundColor: '#fff',
  titleColor: '#333',
  bodyColor: '#666',
  borderColor: '#ddd',
  borderWidth: 1,
  titleFont: { family: 'Poppins' },
  bodyFont:  { family: 'Poppins' }
};

Spread it into options.plugins.tooltip for consistent tooltip styling:

new Chart(ctx, {
  type: 'line',
  data: { /* ... */ },
  options: { plugins: { tooltip: lightTooltip } }
});

percentTooltip()

Wraps lightTooltip with a label callback that appends %:

function percentTooltip() {
  return {
    ...lightTooltip,
    callbacks: {
      label: (item) => `${item.chart.data.labels[item.dataIndex]}: ${item.dataset.data[item.dataIndex]}%`
    }
  };
}

Use it on charts where every Y value is a percentage.

Months arrays

Two pre-baked label arrays for time-series charts:

const MONTHS_SHORT = ['January', 'February', 'March', 'April', 'May', 'June'];
const MONTHS_LONG  = [...MONTHS_SHORT, 'July', 'August', 'September', 'October', 'November', 'December'];

Yes — the “short” array is the first six months and the “long” is twelve. Names are about array length, not abbreviated month names. Use whichever fits your chart’s timeline.

Chart types in use across the template

Every chart in main-vanilla.js uses one of these Chart.js types:

TypeExample uses
lineDashboard revenue, KPI sparklines, recent activity charts
barSales pipeline stages, marketing conversion bars
doughnutRegion breakdown, browser breakdown, traffic source split
pie(less common) — small share-of-total visualizations

For Chart.js’s full type list and option schema, see the Chart.js 4 docs.

Adding a new chart — full example

Suppose you want a bar chart on the marketing dashboard showing campaign performance.

1. Add the canvas

In production/index3.html (or its Pug source src/pug/partials/content/index3.html):

<div class="card">
  <div class="card-header">
    <h3>Campaign performance</h3>
  </div>
  <div class="card-body">
    <canvas id="campaign-perf" style="height: 280px;"></canvas>
  </div>
</div>

2. Register the chart factory

Append to main-vanilla.js:

withCanvas('campaign-perf', (ctx) => {
  new Chart(ctx, {
    type: 'bar',
    data: {
      labels: ['Search', 'Display', 'Social', 'Email', 'Referral'],
      datasets: [{
        label: 'Conversions',
        data: [128, 92, 156, 78, 64],
        backgroundColor: '#4272d7',
        borderRadius: 4
      }]
    },
    options: {
      maintainAspectRatio: false,
      responsive: true,
      plugins: {
        legend: { display: false },
        tooltip: lightTooltip
      },
      scales: {
        y: { beginAtZero: true, grid: { color: '#f1f3f5' } },
        x: { grid: { display: false } }
      }
    }
  });
});

3. Build

npm run build:pug      # only if you edited the Pug source

If you edited the HTML directly (not the Pug source), no build step is needed — refresh the browser.

Why Chart.js and not ECharts or D3

Chart.js was chosen because:

  • Bootstrap-friendly API — opinionated defaults that look reasonable next to Bootstrap cards without per-chart styling work.
  • UMD bundle works without a module loader. CoolAdmin loads scripts as classic <script> tags; ES module-only libraries (newer ECharts builds, modern D3) don’t fit without a bundler.
  • Familiar. Most users have touched Chart.js before. Reading existing charts and writing new ones doesn’t require a learning curve.

If you need a chart type Chart.js doesn’t support cleanly (geographical, network graphs, sankey, treemap), drop in ECharts or D3 selectively as additional vendored scripts on the specific page — vendor/echarts/echarts.min.js + a per-page inline init script. No need to swap out Chart.js wholesale.

A note on the UMD bundle

Chart.js must stay on the UMD buildvendor/chartjs/chart.umd.js-4.5.1.min.js. The ES module build (chart.esm.js) throws:

Uncaught SyntaxError: Cannot use import statement outside a module

…because pages load Chart.js via plain <script>, not <script type="module">. This was a documented fix in v2.1; don’t swap back.

See also