Lesson 35 of 36
Worked Scenario: Design an Analytics Dashboard (Datadog / Amplitude)
A full worked answer to a Senior-FE prompt — dozens of charts on one page, time-range control, drill-down, and queries that take seconds to resolve without the dashboard freezing.
An analytics dashboard — Datadog, Grafana, Mixpanel, Amplitude, Looker — is one of the dense-UI prompts that separates "I can build a chart" from "I can build a product people stare at for hours". The naive implementation collapses the first time a user opens 20 charts with a 7-day range.
Clarifying requirements first
Before proposing anything, the questions worth asking out loud: How many charts on a typical dashboard, worst case? Average query latency at the backend — hundreds of milliseconds or seconds? Time-range control expected (last 1h, last 7d, custom)? Drill-down (click a chart to filter other charts)? Dashboards saved and shared via URL? Real-time updates expected? Mobile view important? For this answer, assume: up to ~30 charts on a dense dashboard, backend query latency 1–4s for the worst queries, first-class time-range control with URL persistence, click-to-drill-down, dashboards shareable by URL, real-time updates on charts explicitly flagged as 'live', desktop-first with a reduced-density mobile view.
Rendering strategy: SSR shell, CSR charts
Dashboards are behind a login — no indexing, no SEO. SSR the shell (header, sidebar, dashboard title, filter chrome) with cookies attached for a fast visually-complete first paint. The charts themselves are CSR — their queries are too variable to pre-render.
Progressive loading, not parallel-everything
A naive 'fetch all 30 queries on mount in parallel' is a disaster: the browser's connection pool caps at ~6 concurrent per host, the backend suddenly gets a 30-query burst, and the user watches a frozen page while their request queue drains.
- Step 1
- Step 2
- Step 3
- Step 4
The user sees the dashboard laid out immediately, their visible charts start loading, and off-screen charts never execute if they never scroll there. Both the browser and the backend are kinder for it.
Query state: cancellable, keyed, cached
QueryKey = (chartId, timeRange, filters)
The cache key. A time-range change invalidates every cached result; a chart config change invalidates one.
In-flight tracker
Keyed by QueryKey, holds the AbortController. On range change, abort every entry; on remount with same key, don't re-fire.
Result cache (bounded LRU)
Short TTL (~30s on a non-live chart). Repeat views are instant; stale-while-revalidate refreshes in the background.
The library you reach for here (TanStack Query, SWR, your own) matters less than owning the three primitives yourself: cancellation, keyed caching, scoped invalidation on filter change.
Chart rendering: SVG, canvas, or WebGL
There is no universal 'always use canvas' answer. Each primitive has a honest place:
- SVG — up to ~1 000 points per chart. Accessible by default, hit-testable, rendered as DOM. Good for stat tiles, sparklines, small-multiple grids.
- Canvas — 1 000 to ~100 000 points. One drawing surface, no DOM per point, redraws cleanly on resize. The common choice for dense time-series.
- WebGL — hundreds of thousands of points on a single chart. Rare, and when you need it you know. The accessibility story requires a parallel ARIA table.
A dashboard mixing all three is normal. The right question in interview is "what's the point density per chart" and the honest answer about which primitive each one needs.
Accessibility is non-negotiable: a chart rendered to canvas or WebGL ships
with a parallel <table> or <figure> + <figcaption> + aria description
summarising the data. Screen reader users should not be shown an empty
canvas.
Time range as the dashboard's single source of truth
Time range (last 24h, last 7d, custom from…to) lives in the URL (search
params). Every chart's query derives its range from the URL, not from React
state. This buys three things:
- Shareability: a URL IS the dashboard view.
- Back-button navigation across range changes.
- Correct invalidation: a URL change naturally triggers a re-render across every subscribing component.
On range change: cancel every in-flight query, surface a 'loading for new range' state across all visible charts, re-dispatch the visible-chart queries, defer off-screen ones.
Drill-down: filters as another URL parameter
Clicking a chart to filter the whole dashboard (click 'US' on a regions chart, every other chart re-queries filtered to US) is the dense-dashboard UX. The filter lives in the URL alongside the time range. The pipeline is identical: URL change cancels in-flight queries, re-dispatches visible ones.
Real-time charts: WebSocket plus incremental append
Charts explicitly flagged as 'live' subscribe to a WebSocket channel for their query key. The server pushes incremental updates (new data points appended, aggregates refreshed). The frontend's job is to apply updates idempotently to the chart's data array and trigger a redraw. The common bug is unbounded growth — a chart receiving a point per second over a day is 86 400 points by evening. Trim the series to a reasonable visible window.
Export and share: the dashboard's second product
A dashboard that cannot be exported as a PDF or shared by URL is half a product. URL sharing is already there (time-range in URL). PDF export is usually a server-side headless-browser render against the URL — the dashboard doesn't need special code, just a stable URL representation of itself.
What's explicitly out of scope, and why
Not solved in this answer: the query engine itself (a backend concern); alerting rules and notification delivery (a separate product surface); dashboard-template / variable system (a next-level feature with its own state model); user permissions enforcement (backend concern — the frontend reads what the server exposes). Scope out is a strength.
What to remember
- Progressive loading beats parallel-everything. Render placeholders, mount queries on IntersectionObserver, cap concurrency.
- Query state has three primitives you should own: cancellation (AbortController per query), keyed caching (chart + range + filters), scoped invalidation on range or filter change.
- SVG for small, canvas for mid-to-high, WebGL for the extreme — each with an accessible table or figure caption.
- Time range and filters live in the URL. The URL IS the dashboard. Everything else derives from it.
- Range change cancels in-flight queries; the UX reads as 'loading for the new range', not 'half the charts are stale'.
- Live charts need bounded growth — trim the data array to a visible window, or you leak memory.
Check yourself
3 questions · pass 3/3 to unlock Worked Scenario: Design an Infinite Canvas (Figma / Miro / Excalidraw)
1.A dashboard has 30 charts, each hitting a different query that takes 1–4 seconds to resolve. The naive 'fetch everything on mount' approach freezes the page and burns the server. What's the right loading pattern?
2.A user drags the time-range control from 'Last 24h' to 'Last 7d'. The dashboard has 15 queries in flight for the old range. What should happen?
3.Each chart renders ~5 000 data points. A dashboard with 15 visible charts is 75 000 points on screen at once. SVG renders cleanly but input lag appears on hover. What's the right rendering primitive, and when do you switch?
3 left to answer
Discussion
Sign in to postNo comments yet. Be the first to say something.