07. Live Data

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.

Connection to the Dashboard Project: The patterns in this module are exactly what you need for the Dashboard project phase. Your dashboard will fetch data from the Reporting API, wire it to Chart.js instances, and update charts when the user changes the date range. Everything here applies directly.

Demo Files

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.

The Chart Lifecycle

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.

Create: 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.

Update: 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();
Do not recreate the chart: A common mistake is calling 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.

Destroy: 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.

Chart Lifecycle ═══════════════ new Chart(ctx, config) chart.update() chart.destroy() ────────────────────► ◄──────────────────► ────────────────────► CREATE UPDATE DESTROY (repeat as needed) CREATE: Binds to canvas, registers listeners, first render UPDATE: Mutate .data, call .update() — re-renders with animation DESTROY: Unbinds listeners, releases canvas, frees memory

The Update Pattern in Detail

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' });
}
Offline development: During development, you can point 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.

Updating Multiple Datasets

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();

Date Range Wiring

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.

Loading States

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';
}

Building KPI Cards

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.

┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │ Total Pageviews │ │ Unique Sessions │ │ Bounce Rate │ │ │ │ │ │ │ │ 8,847 │ │ 3,201 │ │ 42.3% │ │ ▲ 12.3% │ │ ▲ 5.7% │ │ ▼ 8.4% │ │ ╱╲ ╱╲╱╲ │ │ ╱╲╱╲ ╱╲ │ │ ╲╱╲╱ ╲╱ │ │ ╱ ╲╱ ╲ │ │ ╱ ╲╱ ╲ │ │ ╱ ╲╱ ╲ │ └──────────────────┘ └──────────────────┘ └──────────────────┘ Green trend = good Green trend = good Green trend = good (up is good) (up is good) (down is good!)

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.

KPI Card Structure

<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.

Sparklines with Chart.js

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.

Connecting to the Reporting API

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'];
}
Architecture reminder: The DataViz Overview describes the hybrid architecture where the server does heavy aggregation (SQL GROUP BY) and the client handles rendering. The reporting API returns pre-aggregated data; the client just maps it to chart labels and values.

Reusable Chart Helpers

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

Putting It All Together

A typical dashboard page combines everything from this module into a single flow:

Page Load ═════════ 1. Set default date range (current month) 2. Fetch data for that range 3. Create charts (new Chart() for each) 4. Build KPI cards with sparklines 5. Wire date inputs to onChange handler Date Change ═══════════ 1. Show loading indicator 2. Fetch new data 3. updateChart() for each chart 4. Update KPI values and sparklines 5. Hide loading indicator

The live-dashboard.html demo implements this exact flow. Study it, then adapt the pattern for your own dashboard project.

Common Mistakes

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

Summary