In Modules 01 and 02 you learned to draw bars and lines on a canvas. But the data was hard-coded. In a real dashboard, data comes from a reporting API — and it never arrives in the exact shape your chart needs. This module teaches the critical middle step: fetching data and transforming it into chart-ready arrays.
Try it: Open fetch-and-chart.html in a browser. It loads sample-data.json via fetch(), transforms the data, and renders a bar chart — no server required.
Every dashboard chart follows the same pipeline:
Keeping these steps separate makes your code testable and reusable. The transform layer is a pure function — it has no side effects, takes JSON in, and returns arrays out.
fetch()The fetch() API returns a Promise. Pair it with async/await for clean, readable code:
async function loadReport() {
// 1. Make the HTTP request
const response = await fetch('/api/report?range=30d');
// 2. Check for HTTP errors (fetch doesn't throw on 4xx/5xx)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// 3. Parse the JSON body
const json = await response.json();
// 4. Verify the application-level status
if (!json.success) {
throw new Error(json.error || 'API returned success: false');
}
return json.data;
}
response.ok? Unlike XMLHttpRequest, fetch() only rejects on network failures. A 404 or 500 response resolves successfully — you must check response.ok (or response.status) yourself.
Our reporting API returns a standardized envelope. The data property contains pre-aggregated results from SQL GROUP BY queries:
{
"success": true,
"data": {
"byDay": [
{ "date": "2026-01-01", "pageviews": 245, "sessions": 98 },
{ "date": "2026-01-02", "pageviews": 312, "sessions": 121 },
... // ~30 rows for a 30-day range
],
"topPages": [
{ "page": "/home", "views": 1250 },
{ "page": "/about", "views": 430 },
... // top 8 pages
],
"browsers": {
"Chrome": 5200,
"Safari": 2100,
"Firefox": 890,
"Edge": 450,
"Other": 160
},
"summary": {
"totalPageviews": 8800,
"totalSessions": 3200,
"avgSessionDuration": 185
}
}
}
Notice the structure: the server has already done the heavy lifting. Each key (byDay, topPages, browsers) is ready for a different chart type.
A well-designed analytics stack splits work between server and client:
| Layer | Responsibility | Handles |
|---|---|---|
| Database | Storage, indexing | Millions of raw events |
| Reporting API | SQL GROUP BY, filtering, authentication | Heavy aggregation, access control |
| Browser | Sort, format, colorize, render | ~30 pre-aggregated rows |
GROUP BY in SQL, sending ~30 rows, not 100,000. Sending raw logs wastes bandwidth, exposes PII, and makes the browser do work the database is optimized for.
Even pre-aggregated data needs shaping before a chart can use it. Common transforms include:
Given an array of objects, group them by a computed key using Array.reduce():
// Group pageviews by day-of-week
function groupBy(array, keyFn) {
return array.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) groups[key] = [];
groups[key].push(item);
return groups;
}, {});
}
// Usage: group daily data by weekday name
const byWeekday = groupBy(data.byDay, row => {
const d = new Date(row.date + 'T00:00:00');
return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()];
});
// → { Mon: [{...},{...},...], Tue: [{...},...], ... }
reduce()? It processes every element in a single pass, building up the result object as it goes. This is the standard pattern for grouping in JavaScript — equivalent to Python's itertools.groupby or SQL's GROUP BY.
// Sort pages descending by views (do NOT mutate the original)
function sortDesc(array, valueFn) {
return [...array].sort((a, b) => valueFn(b) - valueFn(a));
}
const ranked = sortDesc(data.topPages, p => p.views);
// → [{page:'/home', views:1250}, {page:'/about', views:430}, ...]
// "2026-01-15" → "Jan 15"
function formatDate(isoString) {
const d = new Date(isoString + 'T00:00:00');
const months = ['Jan','Feb','Mar','Apr','May','Jun',
'Jul','Aug','Sep','Oct','Nov','Dec'];
return `${months[d.getMonth()]} ${d.getDate()}`;
}
// Map over daily data to build axis labels
const labels = data.byDay.map(row => formatDate(row.date));
// → ["Jan 1", "Jan 2", "Jan 3", ...]
new Date('2026-01-15') parses as UTC midnight, which can shift to the previous day in western time zones. Adding 'T00:00:00' forces local-time parsing and avoids the off-by-one bug.
A typical transform function chains these utilities into a single pipeline:
function prepareTopPagesChart(apiData) {
// 1. Sort descending by views
const sorted = sortDesc(apiData.topPages, p => p.views);
// 2. Take top 6
const top6 = sorted.slice(0, 6);
// 3. Split into parallel arrays for the chart
return {
labels: top6.map(p => p.page),
values: top6.map(p => p.views)
};
}
function prepareDailyChart(apiData) {
return {
labels: apiData.byDay.map(row => formatDate(row.date)),
pageviews: apiData.byDay.map(row => row.pageviews),
sessions: apiData.byDay.map(row => row.sessions)
};
}
The browsers field arrives as an object (not an array). Convert it for charting:
// Object → array of {name, count} sorted descending
function prepareBrowserChart(browsers) {
const entries = Object.entries(browsers); // [['Chrome',5200], ...]
const sorted = entries.sort((a, b) => b[1] - a[1]);
return {
labels: sorted.map(([name]) => name),
values: sorted.map(([, count]) => count)
};
}
// → { labels: ['Chrome','Safari','Firefox','Edge','Other'],
// values: [5200, 2100, 890, 450, 160] }
With clean data, the render step is straightforward. Here is the full pipeline wired together:
async function main() {
// FETCH
const response = await fetch('sample-data.json');
const json = await response.json();
// TRANSFORM
const { labels, values } = prepareTopPagesChart(json.data);
// RENDER (reusing Module 01 / 02 canvas skills)
const ctx = document.getElementById('chart').getContext('2d');
const maxV = Math.max(...values);
const barW = 600 / labels.length;
labels.forEach((label, i) => {
const barH = (values[i] / maxV) * 300;
ctx.fillStyle = '#16a085';
ctx.fillRect(80 + i * barW, 350 - barH, barW - 10, barH);
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(label, 80 + i * barW + barW / 2, 368);
ctx.fillText(values[i], 80 + i * barW + barW / 2, 345 - barH);
});
}
main();
sample-data.jsonDuring development you often do not have a running reporting API. A static JSON file lets you build and test charts without a server:
// In development: load from static file
const API_URL = 'sample-data.json';
// In production: switch to the real API
// const API_URL = '/api/report?range=30d';
const response = await fetch(API_URL);
const json = await response.json();
The sample-data.json file included in this module mimics the exact shape the reporting API returns. Your transform code works identically against either source — that is the whole point of keeping fetch and transform separate.
fetch() fails (e.g., opened via file:// without a server), it renders from the embedded data instead. This is a useful resilience pattern for demos and error states.
The data-transforms.js file exports six utility functions you can import into any page:
| Function | Purpose | Example |
|---|---|---|
groupBy(arr, keyFn) |
Group elements by key | groupBy(rows, r => r.browser) |
sortDesc(arr, valFn) |
Sort descending by value | sortDesc(pages, p => p.views) |
formatDate(iso) |
ISO string to "Jan 15" | formatDate('2026-01-15') |
parseBrowser(ua) |
Extract browser name | parseBrowser(navigator.userAgent) |
sumBy(arr, valFn) |
Sum numeric values | sumBy(rows, r => r.pageviews) |
topN(arr, n, valFn) |
Top N items by value | topN(pages, 5, p => p.views) |
Import them as ES modules:
<script type="module">
import { sortDesc, topN, formatDate } from './data-transforms.js';
const ranked = sortDesc(data.topPages, p => p.views);
const labels = data.byDay.map(r => formatDate(r.date));
const top5 = topN(data.topPages, 5, p => p.views);
</script>
Production dashboards must handle failures gracefully. Wrap your pipeline in try/catch:
async function main() {
try {
const data = await fetchReport();
const shaped = transformForChart(data);
renderChart(shaped);
} catch (err) {
console.error('Dashboard error:', err);
showErrorState('Failed to load data. Please try again.');
}
}
function showErrorState(message) {
const ctx = document.getElementById('chart').getContext('2d');
ctx.fillStyle = '#999';
ctx.font = '16px sans-serif';
ctx.textAlign = 'center';
ctx.fillText(message, 400, 200);
}
fetch() + async/await; always check response.okreduce(), sort(), and map()sample-data.json) for offline development