A technical companion

Under the hood of the UK Energy Dashboard

A plain-English walkthrough of how the dashboard is built and how every part actually works — the languages, the data, the charts, and the search — written for anyone curious about the engineering behind it.

1 HTML file 3 languages 0 build tools 1 runtime dependency 179 events 9 charts 7 categories ~8,300 lines
01

The whole thing is one file

The entire dashboard — every event, every chart, every interaction, all the styling — lives inside a single self-contained .html file. There is no server, no database, no build step, and no installation. You double-click the file and it opens in any modern browser.

That decision shapes everything else. Because there's no separate database, the data travels inside the file as JavaScript. Because there's no build tool (no Webpack, no compiler), what you write is exactly what runs. And because there's no server, the file can be emailed, dropped on a shared drive, or published as a static page, and it behaves identically everywhere. It's the same philosophy as a self-contained spreadsheet: one artefact, fully portable, that does its whole job on its own.

It leans on exactly one external library, Chart.js, loaded from a public CDN to draw the graphs, plus three web fonts from Google Fonts. Everything else — the timeline, the modals, the filtering, the search engine — is written by hand. Click the layers below to see how the file is organised.

InteractiveAnatomy of the file

The split between data (script ①) and logic (script ②) is deliberate: the facts can be edited without touching the machinery, and the machinery never has to change just because a fact does.

02

Three languages, one page

A web page is built from three languages that each do one job. The dashboard uses all three, and the cleanest way to understand any part of it is to ask: which of the three is responsible here?

  • HTML is the structure — the nouns. It says "there is a heading here, a button there, a box for a chart." It's the skeleton, with no opinion about colour or behaviour.
  • CSS is the presentation — the adjectives. It says "headings are in this serif at this size, the accent colour is burnt orange, cards have a soft shadow." Change the CSS and the page looks different without any structure changing.
  • JavaScript is the behaviour — the verbs. It says "when this tab is clicked, redraw the chart" and "when someone types in the search box, rank the events." It's the only one of the three that can react, calculate, or change the page after it loads.

So when you click a chart tab and the graph changes, three things cooperated: HTML provided the tab and the canvas, CSS made the active tab look active, and JavaScript heard the click and redrew the chart. Keeping these roles separate is what makes a page this size maintainable — a styling fix never risks breaking the data, and a data fix never risks breaking the layout.

Throughout this document, the code blocks are taken directly from the dashboard's source, lightly trimmed for clarity.

03

The design system & how dark mode works

Every colour, font, and spacing value in the dashboard is defined once, in a single place, as a named CSS variable (also called a custom property). Nothing in the dashboard hard-codes a colour like #c6630b; instead it refers to the variable var(--accent). This is what gives the whole thing a consistent, deliberate look — the "warm paper" aesthetic with a burnt-orange accent, a serif display face, and a faint grid background.

CSS — the design tokens
:root {
  /* one definition each — referenced everywhere */
  --paper-bg: #f5efe0;   /* the warm page background  */
  --ink:      #1a1714;   /* primary text             */
  --accent:   #c6630b;   /* burnt-orange highlight   */
  --card:     #fbf7ea;   /* panel background         */
  --line:     #c8bfab;   /* hairline borders         */
  --font-display: 'Fraunces', serif;        /* headings */
  --font-body:    'Inter Tight', sans-serif;  /* text     */
  --font-mono:    'JetBrains Mono', monospace; /* code/data */
}

Dark mode is then almost free. There's a second block that re-points the very same variable names to dark values, and it only applies when the page's root element carries the attribute data-theme="dark":

CSS — dark theme override
[data-theme="dark"] {
  --paper-bg: #141210;   /* same names, dark values */
  --ink:      #f1ebdb;
  --accent:   #e88830;
  --card:     #1b1814;
  --line:     #3a342a;
}

Toggling the theme is therefore a one-line action in JavaScript — flip that one attribute on, and because everything reads from the variables, the entire interface recolours at once. There's no need to restyle each component individually.

JavaScript — the toggle
// one attribute change recolours the whole page
document.documentElement.setAttribute('data-theme', 'dark');

The swatches below are read live from this page's own variables. Use the Dark toggle at the top — every swatch updates instantly, for exactly the reason just described.

LiveThe active palette (reads the real variables)
04

The data layer: one source of truth

The heart of the dashboard is a single list — an array — of 179 event objects. Everything you see is derived from this list: the timeline dots, the detail cards, the markers on the charts, and the search results. Edit an event here and it updates everywhere automatically, because there's only one copy of the truth.

Each event is an object — a labelled bundle of fields. Click any field name below to see what it does and where it surfaces in the dashboard.

InteractiveAn event, annotated — click a field
{ id: 'eon-npower-2019', date: '2019-11-29', year: 2019, yearFrac: 0.91, category: 'ma', title: 'E.ON takes over npower…', description: 'E.ON gained control of npower…', suppliers: ['E.ON', 'npower'], spImplication: 'Removed a legacy Big Six brand…', sources: [ {label, url}, … ], spRelevant: false, forecast: false, defaultView: true, }
Click a field name
Each labelled field feeds a specific part of the dashboard.

Two more data structures sit alongside the events. Categories are a short list that gives the timeline its seven horizontal lanes and gives every event a colour and a home:

JavaScript — the seven categories (lanes)
const LANES = [
  {cat: 'regulation',  name: 'Regulation and Policy'},
  {cat: 'networks',    name: 'Networks and Infrastructure'},
  {cat: 'cap',         name: 'Price Cap'},
  {cat: 'ma',          name: 'Mergers and Acquisitions'},
  {cat: 'strategic',   name: 'Strategic Moves'},
  {cat: 'failure',     name: 'Failures and SoLR'},
  {cat: 'geopolitics', name: 'Geopolitics and Macro'},
];

Themes are curated storylines — for example "Energy Crisis" — that group related events into a narrative arc. A theme doesn't copy the events; it just lists their ids, so the events stay in one place and a theme is simply a reading order through them. This is also why a single event can appear in several stories at once.

05

The timeline & the event cards

The timeline turns the data list into something you can scan. The code walks each of the seven categories, filters the events that belong to it, and places a dot for each one at a horizontal position that corresponds to its date.

That position is pure arithmetic. The timeline spans 2000–2030, and each event carries a year plus a yearFrac (how far through the year it falls, 0–1). The code converts those into a percentage across the track:

JavaScript — placing a dot on the timeline
const YEAR_START = 2000, YEAR_RANGE = 31; // 2000…2030

function eventX(event) {
  const yearFrac = event.yearFrac ?? 0.5;
  const frac = (event.year - YEAR_START + yearFrac) / YEAR_RANGE;
  return frac * 100; // → a left-offset percentage
}

Using a percentage rather than a fixed pixel value means the timeline stretches and shrinks gracefully with the window. Where several events would land on top of one another, the code nudges them into stacked slots so the dots don't collide.

Clicking a dot opens the detail card (a modal). The card isn't pre-written in the HTML — it's assembled on the spot from the event's fields: the date and category pills, the title and description, a "Why this matters" note, the narrative badges, related events, and the source links. The HTML holds just one empty modal shell; JavaScript fills it with whichever event you clicked and reveals it.

06

The charts: data made interactive

The graphs are drawn by Chart.js, the one external library. Chart.js takes a description of what you want — the numbers, the chart type, the axis settings, the colours — and renders it onto an HTML <canvas>, a drawing surface in the page.

There are nine charts, and each has its own small function that returns its configuration. A single dispatcher picks the right one by name — a clean pattern that keeps each chart's setup self-contained:

JavaScript — choosing a chart's configuration
function configFor(chartType, colors) {
  if (chartType === 'cap')          return capChartConfig(colors);
  if (chartType === 'market-share') return marketShareChartConfig(colors);
  if (chartType === 'wholesale')    return wholesaleChartConfig(colors);
  // …six more, one per chart…
}

Switching charts follows one repeatable rhythm: throw away the old chart, build the new configuration, and create a fresh chart. This "tear down and rebuild" approach keeps the state simple — there's never a stale half-updated chart lying around.

JavaScript — rendering the active chart
function renderChart(chartType) {
  if (chartInstance) chartInstance.destroy();      // 1. tear down
  const ctx = document.getElementById('mainChart').getContext('2d');
  const colors = getThemeColors();                  // 2. light or dark?
  const config = configFor(chartType, colors);      // 3. build config
  chartInstance = new Chart(ctx, config);          // 4. draw
}

One subtlety worth knowing: Chart.js can't read the CSS variables directly, so the chart's text and gridline colours are fetched in JavaScript first (that's getThemeColors()) and handed in. That's why the charts also respond correctly when you flip to dark mode — they're redrawn with the dark palette.

The event-line plugin

The dashed vertical "event lines" you can toggle onto a chart — marking, say, the Ukraine invasion on the price-cap graph — aren't a built-in Chart.js feature. They're a small custom plugin: a piece of code that hooks into Chart.js's drawing process and, after the chart is painted, draws the extra lines on top at the right dates. The demo below is a faithful miniature of that mechanism.

LiveA Chart.js chart with the event-line plugin
illustrative price-cap path, £/yr

Toggling the lines flips a single true/false flag and redraws — the same state-then-render idea used everywhere in the dashboard (§08). The dashed lines and labels are drawn by the plugin in burnt orange, and they recolour with the theme.

08

Interactivity without a framework

Many modern web apps are built with a framework like React. The dashboard deliberately isn't — it's plain ("vanilla") JavaScript. For a self-contained file with no build step, that's a feature: nothing to install, nothing to compile, and the code is exactly what runs.

The model is simple to hold in your head. The current situation is kept in ordinary variables — which chart is showing, whether event lines are on, which companies are toggled. When something changes, the code updates a variable and then calls a render function that rebuilds the affected part of the page. There's no hidden magic reconciling the screen; the redraw is explicit.

JavaScript — the state-then-render pattern
let chartEventLinesOn = false;        // a piece of state

eventsButton.addEventListener('click', () => {
  chartEventLinesOn = !chartEventLinesOn;  // 1. change the state
  renderChart(currentChart);               // 2. redraw from it
});

Interactions are wired with event listeners — "when this is clicked, run this function." That single pattern, repeated, powers the tabs, the filters, the timeline dots, the modal, and the search box. It's a small amount of vocabulary doing a lot of work, which is exactly why the whole dashboard fits in one readable file.

09

The Compare view: small multiples

The Compare feature shows several charts at once as small side-by-side "mini" charts — a technique called small multiples, which lets you read trends across different measures together rather than flipping between them.

It reuses the very same configuration functions as the main charts, just rendered smaller and into their own canvases (tracked in a lookup so each can be cleaned up independently). Each mini chart can even carry its own little view toggle — electricity vs gas market share, for instance — without disturbing the main view.

That last point needs care: because the chart configs read from shared state, the Compare code snapshots that state, swaps in the mini chart's own choice, builds the config, then restores the snapshot. The main chart never notices it happened. It's a tidy way to let two views of the same machinery coexist without one stepping on the other.

10

How it's built — and kept correct

A single file with 179 events and thousands of lines of logic could easily drift into inconsistency. It's kept honest by a disciplined workflow rather than by chance.

  • Versioning. Every change produces a new numbered version (the current one is v118), so any edit can be compared against the last good copy and rolled back if needed.
  • Automated checks. Before a version ships, scripts pull the two inline scripts out of the file and confirm they parse without error, that there are exactly the expected number of events with no duplicate ids, that the events are in date order, that every chart marker and every theme points at a real event, and that the page's tags are balanced. If any check fails, the version isn't shipped.
  • One change at a time. Edits are made surgically and re-validated, so a small content fix can't quietly break the search index or the charts.

None of this is visible in the dashboard itself — it's the equivalent of a spell-check and a test suite running behind the scenes. It's what lets a file this dense stay accurate as it grows.

11

Glossary

A quick reference for the terms used above.

HTML
The structural language of a web page — defines the elements that exist (headings, buttons, boxes).
CSS
The styling language — controls how elements look: colour, type, spacing, layout.
JavaScript
The behaviour language — the only one that can react to clicks, calculate, and change the page after load.
CSS variable
A named, reusable value (e.g. --accent). Define once, reference everywhere; the basis of theming.
Array
An ordered list of items — here, the list of 179 events.
Object
A labelled bundle of values — here, a single event with its title, date, and so on.
Library
Pre-written code you bring in to do a job — here, Chart.js for drawing graphs.
CDN
A public network that hosts common libraries so a page can load them over the internet.
Canvas
A blank drawing surface element in the page; Chart.js paints the graphs onto it.
Plugin
A small add-on that hooks into a library — here, to draw the dashed event lines Chart.js doesn't provide.
Modal
A pop-over panel layered over the page — here, the event detail card.
Event listener
An instruction of the form "when X happens, run this" — how every interaction is wired.
Index (search)
Text prepared in advance so searching is fast — built once when the page loads.
Framework
A large structure (like React) for building apps. The dashboard deliberately uses none.