Skip to main content
All projects

Case study

Role
Sole developer
Organisation
Personal project

The Contour Atlas

A field guide to the twelve regions of the UK, covering food, festivals, landmarks and live weather, drawn as an Ordnance Survey sheet. Live readings arrive on a station-style split-flap board that only flips when something has actually changed.

The Contour Atlas, Personal project

01 / Overview

What I did.

Built to get streaming right at small scale: a real producer, a real change filter, a real transport, and an abstraction thin enough that the streaming layer could be swapped for a broker without touching the application. It also runs entirely without an API key, which was a constraint I set deliberately: the design had to survive a free, rate-limited upstream.

02 / Architecture

Layers listed outermost first.

How it's put together.

A producer-consumer pipeline with three deliberate seams: one between the upstream API and the producer, one between the producer and the transport, and one between raw coordinates and anything drawn on screen. Each seam exists because that is where I expected the implementation to change.

  1. Upstream

    Where the weather comes from.

    Open-Meteo, queried without an API key. All twelve regions are fetched in a single request rather than twelve, which is what keeps the whole thing inside a free tier at a 60-second cadence.

    • Open-Meteo
  2. Producer

    One poller for the whole map.

    A server-side loop polling once every 60 seconds. It runs on the server, not in each visitor's browser, so a hundred concurrent visitors cost exactly the same upstream traffic as one.

    • Next.js
    • TypeScript
  3. Change filter

    Only publish news.

    Each reading is compared with the last published value for that region and dropped if it has not moved. The board flips because something changed, not because a minute passed.

    • TypeScript
  4. Event bus

    The seam a broker would slot into.

    Publishing goes through an EventBus interface with an in-process implementation. Moving to a Kafka-compatible broker means writing one more implementation of that interface, not reworking the application around it.

    • EventBus interface
  5. Transport

    Server to browser.

    Server-Sent Events. Changed readings are pushed down an open HTTP response as they are published; the browser's EventSource handles reconnection itself.

    • Server-Sent Events
  6. Client

    The sheet and the board.

    React components rendering the map and the split-flap board, subscribed to the event stream and animating only the characters that actually differ.

    • React
    • Tailwind
  7. Projection

    One coordinate system for everything drawn.

    Coastline, regional borders and location pins all pass through the same coordinate-to-SVG function, so a pin cannot land in the sea because two pieces of geometry disagreed.

    • SVG
    • TypeScript

03 / Decisions

Each of these had a credible alternative. The trade-off is stated, not hidden.

Choices, and what they cost.

  1. Server-Sent Events, not WebSocket

    Chose
    SSE over a long-lived HTTP response
    Instead of
    • WebSocket
    • Client-side polling on an interval
    Why
    The data only ever travels one way, server to browser. WebSocket buys a return channel this application has no use for, in exchange for a protocol upgrade, its own reconnection logic and more hostile behaviour through proxies. SSE is plain HTTP, and EventSource reconnects on its own with no code from me.
    Trade-off
    There is no client-to-server channel, so anything interactive later would need a separate mechanism. Browsers also cap concurrent SSE connections per origin, which is fine for one stream and would not be for many.
  2. One batched upstream poll, server-side

    Chose
    A single server-side request covering all twelve regions every 60 seconds
    Instead of
    • One request per region
    • Each browser fetching the weather itself
    Why
    Twelve requests a minute per visitor would have needed a paid key almost immediately; one request a minute for everybody does not. Polling on the server also decouples upstream cost from traffic entirely, since the load is the same whether one person is watching or a thousand.
    Trade-off
    A single request is a single point of failure: one bad response affects all twelve regions at once, so the failure has to degrade to stale-but-labelled data rather than an empty map.
  3. Filtering unchanged readings at the producer

    Chose
    Comparing against the last published value and publishing only differences
    Instead of
    • Publishing every reading on every tick
    • Letting the client decide what changed
    Why
    A split-flap board that flips every 60 seconds regardless is noise, and it trains you to ignore it. Filtering at the producer means every animation on screen carries information, and it keeps the stream quiet, so a change is visible in the network tab as well as on the board.
    Trade-off
    Silence becomes ambiguous: a client cannot tell 'nothing changed' from 'the producer died'. That needs a heartbeat to resolve, which is a cost the naive approach does not have.
  4. An EventBus interface over calling a broker directly

    Chose
    An interface with a single in-process implementation
    Instead of
    • Publishing straight to the transport
    • Running Kafka or Redpanda from the start
    Why
    A broker in Docker for a twelve-region weather feed would be infrastructure with nothing to justify it, and it would put a container between me and every deployment. The interface costs almost nothing and means the decision stays open: if this needed durability or multiple consumers, it is one new implementation rather than a rewrite.
    Trade-off
    It is an abstraction with exactly one implementation today, which is the classic shape of speculative generality. It earns its place only because it is genuinely thin: a handful of methods, no configuration, no framework.
  5. A single shared projection function

    Chose
    One coordinate-to-SVG transform used by coastline, borders and pins
    Instead of
    • Per-feature coordinate handling
    • Pre-baked SVG paths with hard-coded pin positions
    Why
    The failure mode of drawing geography is quiet: a pin two pixels into the sea, a border that does not meet the coast. Those happen when two pieces of code convert coordinates slightly differently. Routing everything through one function makes registration structural, because the features cannot disagree, because there is only one conversion.
    Trade-off
    Everything now depends on that one function being right, and changing the projection changes every drawn element at once. That is the intended property, but it means the function needs the most care in the codebase.

04 / Stack

Grouped by the job they do rather than listed as a keyword run.

Technologies.

Application
  • Next.js
  • React
  • TypeScript
Styling
  • Tailwind
Streaming
  • Server-Sent Events
  • EventBus interface
Data
  • Open-Meteo
  • No API key
Drawing
  • SVG
  • Shared projection
Hosting
  • Vercel

05 / Outcome

Where it landed.

A live streaming pipeline with no key, no broker and no per-visitor upstream cost. The two things I set out to prove both held up: that a change filter makes an interface calmer and more informative at the same time, and that one seam in the right place keeps a decision open.