← Back to Blog

Project 02 — SA Fuel Price Tracker

Building a live South African fuel price dashboard with vanilla JS and Chart.js — evolving from a static JSON file to a proper REST API backend, with interactive chart navigation and a browser-based price editor.

The Problem

South Africa has no free public API for fuel prices. The Department of Mineral Resources and Energy publishes new retail prices on the first Wednesday of every month — as a gazette, not a data feed. Every existing tracker either scrapes a website or requires a paid API.

The goal was a genuinely useful, zero-cost dashboard that stays current with minimal maintenance effort.

Version 1 — Static JSON

The first version loaded from a local prices.json file — 29 months of official FIASA data seeded manually. Simple, fast, zero dependencies beyond Chart.js.

const DATA_URL = './data/prices.json';

It proved the UI worked. But updating it required editing a file, committing and pushing every month. That is not a real system.

Version 2 — Live from the SA Fuel Price API

Project 04 built a proper REST API backed by PostgreSQL on Railway. The tracker was then updated to pull from it directly:

const DATA_URL = 'https://sa-fuel-api-production.up.railway.app/v1/prices?limit=36';

The API returns a different shape than the flat JSON the dashboard was built against — nested prices.petrol.p95Inland instead of flat p95i. A normalise function bridges the gap at load time:

function normalizeApiRow(row) {
  return {
    month: row.monthLabel,
    p95i:  row.prices.petrol.p95Inland,
    p95c:  row.prices.petrol.p95Coastal,
    p93i:  row.prices.petrol.p93Inland,
    d005i: row.prices.diesel.d005Inland,
    d005c: row.prices.diesel.d005Coastal,
  };
}

The API also returns newest-first. The tracker expects chronological order for slice(-12) and month-on-month change calculations, so the array is reversed after normalisation:

priceData = rows.map(normalizeApiRow).reverse();

Async JS and Error Handling

The fetch flow covers three UI states — loading, success, and failure — managed by toggling a hidden class. Checking res.ok before parsing matters because a 404 or 403 still resolves the fetch() promise without throwing.

Visualisation with Chart.js

Chart.js handles the price history line. Every visual is overridden to match the MD Works dark theme — grid colour, tick colour, tooltip background, font families. The gradient fill is created from the canvas context before the chart initialises. Calling chart.destroy() before rebuilding prevents stacking canvas instances.

Interactive Chart Navigation

With 36 months of data a fixed 12-month window hides most of the history. A sliding window with ← Older and Newer → navigation was added:

const end        = data.length - (chartOffset * WINDOW);
const start      = Math.max(0, end - WINDOW);
const windowData = data.slice(start, end);

chartOffset tracks how many windows back the view is. The nav buttons disable automatically at the boundaries. A range label shows the exact months in view.

Month-on-Month Change Indicators

Each price card compares the current month against the previous. A 0.005 threshold avoids flagging floating point noise as a change.

Admin Price Editor

A password-protected admin.html page authenticates via the API key and exposes two modes — Add New Month and Correct Existing. The month label auto-fills from the date picker. All five price fields map directly to the API’s POST and PUT endpoints. No terminal, no code change, no redeploy needed.

What This Project Demonstrates

  • fetch() with async/await, res.ok checking, and UI state management
  • Consuming a versioned REST API with response normalisation
  • Chart.js with full custom dark theme, gradient fill, tooltip styling
  • Sliding window chart navigation with offset state
  • Connecting two of your own projects — tracker as a consumer of the API

Live: sa-fuel-tracker.pages.dev GitHub: sa-fuel-tracker


Next: Project 03 — SA Incident Tracker. Community safety reporting with Google Apps Script, moderation queues, live CCTV feeds and a canvas-based photo blur tool.