The previous modules taught you how to create charts with Chart.js using static configuration objects. In a real analytics dashboard, the data changes constantly — users pick a date range, toggle a metric, or the page auto-refreshes. This module teaches the patterns that connect Chart.js to a live data source: the chart lifecycle, the update pattern, date-range wiring, and KPI cards with sparklines.
Run: Open the HTML files in a browser. No server required — they use the local sample-data.json file via fetch(), so you may need to serve them from a local server (e.g., npx serve .) or use a browser that allows local file fetch.
Every Chart.js instance follows a three-phase lifecycle: create, update, and destroy. Understanding this lifecycle is essential because mismanaging it causes the most common bugs in Chart.js dashboards — memory leaks, ghost tooltips, and canvas reuse errors.
new Chart()You create a chart by passing a canvas element (or its ID) and a configuration object. Chart.js takes ownership of the canvas and renders immediately.
const ctx = document.getElementById('myChart').getContext('2d');
const chart = new Chart(ctx, {
type: 'bar',
data: {
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'],
datasets: [{
label: 'Pageviews',
data: [120, 190, 300, 250, 180],
backgroundColor: '#16a085'
}]
},
options: {
responsive: true,
plugins: { legend: { display: false } }
}
});
The new Chart() call returns a chart instance. You must hold onto this reference — it is the handle you use for updates and cleanup.
chart.update()When new data arrives, you mutate the chart's existing data properties and call chart.update(). This is the single most important pattern in this module. Do not recreate the chart on every data change.
// New data arrives (e.g., from a fetch() call) const newLabels = ['Sat', 'Sun', 'Mon', 'Tue', 'Wed']; const newData = [95, 80, 310, 275, 290]; // Mutate the existing chart's data chart.data.labels = newLabels; chart.data.datasets[0].data = newData; // Re-render with animation chart.update();
new Chart() again on the same canvas when data changes. This creates a new chart instance on top of the old one. The old chart still exists in memory, its event listeners are still attached, and you get ghost tooltips and rendering artifacts. Always use chart.update() instead.
chart.destroy()When you need to remove a chart entirely — for example, switching from a bar chart to a line chart, or unmounting a component — call chart.destroy(). This unregisters all event listeners, removes the chart from the internal registry, and releases the canvas.
// Clean up before creating a different chart type on the same canvas
chart.destroy();
// Now it's safe to create a new chart on the same canvas
const newChart = new Chart(ctx, { type: 'line', ... });
If you only need to swap data (same chart type, same structure), use update(). Use destroy() only when you need to fundamentally change the chart configuration.
The update pattern has three steps: fetch data, mutate chart properties, call update. Here is the complete flow you will use in your dashboard:
// Step 1: Fetch data from the API (or local JSON for dev)
async function loadData(startDate, endDate) {
const response = await fetch(`/api/stats?start=${startDate}&end=${endDate}`);
const json = await response.json();
return json.data;
}
// Step 2: Wire it to the chart
async function refreshChart(chart, startDate, endDate) {
const data = await loadData(startDate, endDate);
// Extract labels and values from the response
const labels = data.byDay.map(d => formatDate(d.date));
const values = data.byDay.map(d => d.pageviews);
// Mutate chart data
chart.data.labels = labels;
chart.data.datasets[0].data = values;
// Re-render
chart.update();
}
// Helper: format "2026-01-15" → "Jan 15"
function formatDate(iso) {
const d = new Date(iso + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
fetch() at a local sample-data.json file instead of a live API. The demo files in this module use this approach. When you wire up the real reporting API, you just change the URL.
If your chart has multiple datasets (e.g., pageviews and sessions on the same chart), update each dataset's data array individually:
// Two datasets on one chart chart.data.labels = newLabels; chart.data.datasets[0].data = pageviewData; // first dataset chart.data.datasets[1].data = sessionData; // second dataset chart.update();
Analytics dashboards almost always have a date range picker. The pattern is straightforward: two <input type="date"> elements, an event listener on each, and a function that fetches data and updates the chart.
<!-- Date range inputs -->
<label>From: <input type="date" id="startDate" value="2026-01-01"></label>
<label>To: <input type="date" id="endDate" value="2026-01-31"></label>
<canvas id="chart"></canvas>
<script>
const startInput = document.getElementById('startDate');
const endInput = document.getElementById('endDate');
// Listen for changes on either date input
startInput.addEventListener('change', onDateChange);
endInput.addEventListener('change', onDateChange);
async function onDateChange() {
const start = startInput.value; // "2026-01-01"
const end = endInput.value; // "2026-01-31"
if (!start || !end || start > end) return; // basic validation
await refreshChart(chart, start, end);
}
</script>
In the live-dashboard.html demo, the date inputs filter the local sample-data.json by month. In your real dashboard, they will set query parameters on the reporting API request.
When fetching data takes time, you should show a loading indicator so the user knows something is happening. A simple approach is to overlay a message on the chart area:
async function onDateChange() {
showLoading(true);
try {
await refreshChart(chart, startInput.value, endInput.value);
} finally {
showLoading(false);
}
}
function showLoading(visible) {
document.getElementById('loading').style.display =
visible ? 'flex' : 'none';
}
A KPI (Key Performance Indicator) card is a compact display showing a single metric with context: the current value, the trend direction, and optionally a sparkline showing recent history. Most analytics dashboards lead with a row of KPI cards above the detailed charts.
Notice the subtlety: for bounce rate, a decrease is good, so the downward arrow is colored green. Your code needs to handle this per-metric polarity.
<div class="kpi-card">
<div class="kpi-label">Total Pageviews</div>
<div class="kpi-value">8,847</div>
<div class="kpi-trend up good">▲ 12.3%</div>
<canvas class="kpi-sparkline" width="120" height="40"></canvas>
</div>
The CSS classes up/down set the arrow direction, and good/bad set the color. A metric where "up is good" (like pageviews) gets up good. A metric where "down is good" (like bounce rate) gets down good.
A sparkline is a tiny line chart with no axes, no legend, no gridlines — just the line and optionally a fill. Chart.js handles this by disabling all plugins and scales:
function createSparkline(canvasId, data, color) {
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: data.map((_, i) => i), // dummy labels
datasets: [{
data: data,
borderColor: color,
borderWidth: 2,
fill: true,
backgroundColor: color + '20', // 12% opacity
pointRadius: 0, // no dots
tension: 0.4 // smooth curve
}]
},
options: {
responsive: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: {
x: { display: false },
y: { display: false }
},
elements: { line: { borderWidth: 2 } }
}
});
}
See the kpi-cards.html demo for the complete implementation with four KPI cards and sparklines.
In development, the demos in this module use a local sample-data.json file. In production, your dashboard will fetch from the Reporting API. The swap is minimal because the data shape is the same:
// Development: local JSON
const API_BASE = './sample-data.json';
// Production: real API
// const API_BASE = '/api/stats';
async function fetchData(month) {
const response = await fetch(API_BASE);
const json = await response.json();
// In dev, select the month from the local file
// In production, pass the date range as query params
return json[month] || json['2026-01'];
}
The chart-helpers.js file provides a set of helper functions that wrap common Chart.js patterns. Instead of writing the full configuration object every time, you call a function:
// Using the helpers
const bar = createBarChart('barCanvas', labels, data, {
label: 'Pageviews',
color: '#16a085'
});
const line = createLineChart('lineCanvas', labels, data, {
label: 'Sessions',
color: '#2E86C1'
});
// Later, when data changes:
updateChart(bar, newLabels, newData);
// When tearing down:
destroyChart(bar);
These helpers are not required — they are convenience wrappers. For your dashboard project you can use them directly, adapt them, or write your own.
| Helper Function | Purpose |
|---|---|
createBarChart() |
Creates a styled bar chart with sensible defaults |
createLineChart() |
Creates a styled line chart with fill and tension |
createDoughnutChart() |
Creates a styled doughnut chart |
updateChart() |
Updates labels and data on an existing chart |
destroyChart() |
Safely destroys a chart instance (null-safe) |
createSparkline() |
Creates a tiny line chart for KPI cards |
A typical dashboard page combines everything from this module into a single flow:
The live-dashboard.html demo implements this exact flow. Study it, then adapt the pattern for your own dashboard project.
These are the bugs students hit most often when wiring Chart.js to live data:
| Mistake | Symptom | Fix |
|---|---|---|
Calling new Chart() on every data change |
Ghost tooltips, flickering, memory leak | Use chart.update() instead |
Forgetting to call chart.update() |
Chart does not visually change | Always call update() after mutating data |
| Not destroying before re-creating | "Canvas is already in use" error | Call chart.destroy() first |
Replacing chart.data with a new object |
Chart does not react to changes | Mutate chart.data.labels and chart.data.datasets[0].data directly |
| No loading state during fetch | UI feels broken while waiting | Show a spinner or "Loading..." overlay |
new Chart(ctx, config) and save the instancechart.data.labels and chart.data.datasets[0].data, then calling chart.update() — never recreate the chartchart.destroy() only when you need to fundamentally change the chart type or tear down the pageonChange handler that fetches data and calls updateChart()