Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ~71 min· topic 7 of 7

Topic 14.7

Build: The Dashboard in React

In one line

Move the whole Lumen dashboard into React without losing a feature: the rides line is D3 maths with JSX marks, the stations chart is Recharts, and every earlier D3 chart is mounted unchanged through one small wrapper. It answers Arjun: the dashboard can now live inside the company's React app.

Before you start

You will learn

  • Add React and Recharts to the Vite project and switch index.html to React mount points
  • Write a generic <D3Box> that mounts any plain D3 chart, and use it for every chart you already built
  • Rewrite RidesLine.jsx (D3 maths, JSX marks, brush and presets kept) and StationsBar.jsx (Recharts) as the two showcases

You write code in

In your Vite project lumen-dashboard/, now with React (run npm run dev), and in the editor on this page

Files you touch

  • vite.config.js
  • package.json
  • index.html
  • src/main.jsx
  • src/App.jsx
  • src/charts/RidesLine.jsx
  • src/charts/StationsBar.jsx
  • src/parts/D3Box.jsx
  • src/parts/map-with-trips.js
  • src/utils/theme.js
  • src/utils/useSize.js
  • src/data/extra.js

Think of it like this

Moving a shop into a bigger building. The shelves, the stock and the till all come with you. Two display cases are rebuilt to fit the new walls, and everything else is carried in on a trolley and put in place as it was. Nothing is left behind in the old shop.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

mount point
The one element, <div id="root">, that React draws into.
D3Box
Our wrapper in src/parts/D3Box.jsx: it hands one empty <div> to a plain D3 chart and cleans up after it.
adapter
A small function that makes a chart with a different shape look like draw(container, data, options), as map-with-trips.js does.
portal
createPortal(children, element): React draws children inside an element that is outside its own root, such as #kpis.
useLoad
Our small hook in App.jsx: it runs a loader once and reports loading, ready or error.
state per box
Each box decides on its own what to show: loading text, an error, or the chart.

Step by step

01Add React to the project

These commands were run for real in a scratch copy of the Phase 13 project (Node 24, npm 11, Vite 8.3.4). The versions npm installed are listed below.

terminal
$ npm install react react-dom recharts && npm install -D @vitejs/plugin-react
── expected output ──
added 30 packages, and audited 109 packages in 14s
 
found 0 vulnerabilities
added 1 package, and audited 110 packages in 3s
 
found 0 vulnerabilities
Installed: vite 8.3.4, react and react-dom 19.3.1, recharts 3.10.1, d3 7.9.0, @vitejs/plugin-react 6.1.2. This page's preview runs React 19.2.8. Your project gets the newest versions, which can be newer than the ones shown here.

02Tell Vite about React, and give React places

vite.config.js loads the plugin. index.html changes in three places: its script is now /src/main.jsx; the rides line and stations boxes become <div id="root">; and there are two new places, <div id="notices"> for the loading and error notices and <div id="more"> at the end of the grid for the later boxes. Both divs use display: contents, so the grid treats the boxes inside them as its own children.

03D3Box: the wrapper that keeps every chart

D3Box takes draw, data and options. One effect creates the chart and returns a cleanup (stop watching the width, call the chart's destroy if it has one, empty the <div>). A second effect calls update(data) when the data changes. StrictMode runs the first effect twice in development, so the box has to start clean the second time, and it does.

using itwhole filejsx
<D3Box draw={drawStationsStacked} data={data} />
<D3Box draw={drawJuneHeatmap} data={june} />
<D3Box draw={drawFleetTreemap} data={fleet} />
<D3Box draw={drawMapWithTrips} data={mapData} options={{ onSelect }} />

04App.jsx: one hook, one state per box

useLoad(load) runs a loader once and returns { status, value, error }. Box shows its heading and then loading text, the loader's own error message, or its chart. The key numbers and notices reach their old places through portals. The picked date range is one useState here, and selectRange clamps it to the data (a reversed, out-of-range or empty range, as in Phase 8).

05RidesLine.jsx and StationsBar.jsx: the two showcases

RidesLine builds its scales, path and notes in one useMemo, draws axes and the brush into refs, and keeps the crosshair in state. StationsBar sorts a copy, leaves out rows without a number, shows a sentence for none, and gives the Recharts container a height that grows with the number of stations, so ResponsiveContainer never measures 0. It takes its colours from the same CSS tokens (var(--chart-accent)), so the dark theme works.

06Run it

npm run dev and open the address it prints. Then try everything: drag across the line, press Summer, press Tab and the arrow keys, click a station on the map, switch the theme. The last example below runs the same code on this page, with the files joined into one.

terminal
$ npm run build
── expected output ──
vite v8.3.4 building client environment for production...
transforming...
✓ 979 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html 6.42 kB │ gzip: 2.29 kB
dist/assets/index-BLqODWnK.css 4.08 kB │ gzip: 1.24 kB
dist/assets/index-DWyn0aa8.js 687.88 kB │ gzip: 212.43 kB
 
✓ built in 533ms
Vite also warns that some chunks are larger than 500 kB (React, D3 and Recharts together). Phase 16 shows how to split the bundle and import only the D3 parts you use. Your numbers will differ a little.

Try it yourself

  1. 1

    Break one file

    In the example's stand-in loaders change "station-month.csv" to "stations-month.csv". The stations box and the stacked months box each show 404 Not Found, and the console prints stations bars in July: 0 and stacked 0. The rides line, heatmap and treemap still work: that is the point of one state per box.

  2. 2

    Make every month empty

    In StationsBox change d.rides > 0 to d.rides > 1e9. The console prints stations bars in July: 0 and the box shows No stations had any rides in this period. instead of an empty frame.

  3. 3

    Mount one more chart in one line

    The heatmap box is <D3Box draw={drawJuneHeatmap} data={june.value} />. Write the same line for another plain chart you built and it is on the page: D3Box asks only for draw(container, data, options).

Code & diagrams

The Lumen dashboard as a React app (the files joined into one) New tab

The project's imports of local files can't run in the preview, so the code of `D3Box`, `RidesLine`, `StationsBar`, `App` and the plain stacked, heatmap and treemap charts is joined into one example, and the loaders are short stand-ins that read the same data. The station map and its trips layer (about 400 lines in four more files) are not joined in: its box shows a one-line stand-in. In your project each file stays on its own, and the map works. Drag across the line, press Summer, or change the month.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

key numbers: 365 | 933,651 | 18 Jul 2025 (4,380) | 14 Aug 2025 (439) | 0
rides line: 1 line, axes drawn | Line chart of rides per day, 365 days fr
stations bars in July: 25
plain D3 charts mounted: stacked 1 heatmap 1 treemap 1
after Summer: 1 Jun to 31 Aug: 334,321 rides
brush drawn: true | stations note: Station numbers are monthly, so they add up every month the selection touches (Jun, Jul, Aug).

Your project files

Your lumen-dashboard project after this topic. Click a file to see it in full.

vite.config.jsjs

The whole file after this topic. If yours differs, you can copy this one and carry on.

// Vite's settings. The React plugin turns JSX (the HTML-looking code in .jsx files) into plain JavaScript calls.
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget to empty the box in the cleanup

In D3Box delete the line el.replaceChildren(); from the cleanup.

terminal
$ Console
── what you'll see ──
plain D3 charts mounted: stacked 2 heatmap 2 treemap 2
Every plain chart is drawn twice, one on top of the other. The other lines of the output stay the same, so only a reader who counts notices.

Break #2

Give the stations chart a parent with no height

In StationsBar.jsx change height in the parent's style to height: "auto".

terminal
$ Console
── what you'll see ──
stations bars in July: 0
No error and no warning. The box shows the month selector and nothing below it.

Myth vs fact

Myth

Moving to React means rewriting every chart.

Fact

Here two charts were rewritten as showcases. The other six, the loaders, the cleaning, the formatting, the themes and the map's hit-testing moved unchanged through D3Box.

Myth

A plain D3 chart cannot live inside React.

Fact

It can, if React gives it one empty element and nothing else touches what is inside, and if the effect cleans up. That is all D3Box does.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Keep data loading out of the chart components, as here: App.jsx loads and the charts only draw. Later you can swap useLoad for TanStack Query or your app's own data layer without touching a chart.

  • ▸

    In a bigger app, mount the dashboard lazily with React.lazy, so the D3, Recharts and map code is downloaded only when someone opens the page.

  • ▸

    D3Box creates the chart once per draw function and ignores later changes to options. That is deliberate: options such as onSelect are read when the chart is made. If you need a changing callback, keep it in a ref (as RidesLine does with onBrush) or call a method on the chart's handle.

  • ▸

    A chart's update should be cheap and safe to call with the same data twice. D3Box only calls it when the data prop's identity changes, so build data with useMemo in the parent, as MapBox does.

Remember this

  1. 1

    Arjun's need is met. The dashboard runs as React components, mounted by src/main.jsx. It can be dropped into the company's app as one <App />, shares its React and its login, and its parts can take props from the app's own state. The company's pages did not have to learn D3, and the team did not have to rewrite eight charts.

  2. 2

    Starting point: the Phase 13 dashboard. This is the newest committed complete build: the Vite project with the loaders (src/data/load.js, clean.js), format.js, config.js, the themes (theme.css, charts.css, resize.js), the rides line with crosshair, brush and presets, the stations chart, the stacked months, the June heatmap, the station map with hit-testing and its trips layer, and the fleet treemap. Every one of those files stays exactly as Phase 13 left it, except src/main.js, which index.html no longer loads (delete it; main.jsx and App.jsx replace it).

  3. 3

    The pattern: D3 owns the DOM inside a ref. src/parts/D3Box.jsx renders one empty <div> and never looks inside it. In an effect it calls the chart's own draw(container, data, options), remembers the { update, resize } it returns, calls update(data) when the data prop changes, calls resize(width) when the box changes width, and in its cleanup removes everything the chart drew. That is the one-owner rule of Topic 14.1 and the cleanup rule of Topic 14.2, written once. The stacked months, the heatmap and the treemap are one line each: <D3Box draw={drawStationsStacked} data={data} />.

  4. 4

    The map is the same wrapper with a small adapter. The station map and its trips layer need each other and need redrawing after a theme change, so src/parts/map-with-trips.js draws both and returns one { update, resize, destroy }. src/utils/theme.js now holds the theme button's code and lets a chart listen for theme changes, because the old main.js kept that in a variable that React cannot reach. The hit-testing, the keyboard arrows and the click handler inside the map are untouched.

  5. 5

    Two charts are rewritten, as showcases. RidesLine.jsx is Topics 14.1 and 14.2 together: scales and path text in useMemo, JSX marks, the axes and the brush drawn by D3 into empty <g> refs with cleanup, and the crosshair as React state. It keeps the keyboard reading, the tap tooltip, the quietest-day note, the gaps, the reveal of the first draw, the small-screen layout, and the brush and the June, Summer and Whole year buttons. StationsBar.jsx is Recharts (Topic 14.4) with the shared maximum and the summed months kept. The picked dates are one range state in App.jsx that the brush, the buttons and the stations chart all read.

  6. 6

    What the dashboard can do now that it could not before: run inside any React app, re-render from props and state, show each chart's loading, error and empty state separately (a failed stations file no longer hides the rides chart), and be driven by the app's own state. Everything it could do before still works: the key numbers, loaders and cleaning, all seven charts, the brush and presets, motion, the light, dark and automatic themes, redraw on resize, the map's hit-testing and keyboard use, and the hand-made week and low-stations pictures.

  7. 7

    Still missing: the table twin and full accessibility work (Phase 15), a live feed, filters and tests (Phase 16), and the polish of the story pages (Phase 17).

Explain it without notes

01

Which files stayed the same in the move to React, and why was that possible?

02

Why does each box have its own loading and error state?

03

How does D3Box follow the one-owner rule, and what does its cleanup do?

Practice

01

Mount the drawFleetTreemap chart in a new box that loads fleet.json with useLoad(loadFleet), with a loading state and an error state.

02

Add a third box that shows a Loading… state and then Busiest day from daily.csv, using useLoad.

Trade-offs

  • ↔

    A React dashboard is easier to embed and to drive from app state, and it costs a React runtime and a build step the plain version did not need (the bundle grew from the plain project's size to 687.88 kB, 212.43 kB gzipped).

  • ↔

    D3Box keeps old charts working at no cost, and leaves them outside React's rendering: their output is not server-rendered and React tools do not see inside them. Rewrite a chart in React only when you need that.

Done when you can

  • I can add React and Recharts to a Vite project and switch index.html to mount points.

  • I can write D3Box and explain why its cleanup empties the box.

  • I can mount an existing plain D3 chart with one line, and an adapter when two charts depend on each other.

  • I can write a loader hook and give each box its own state.

  • I can run npm run build and read its size line.