Design a Dashboard Analytics UI
Frontend system design for analytics dashboards — layout, query UX, charts, caching, and realtime refresh.
- system-design
- interview
- architecture
- dashboard
Scope the problem
In scope (frontend):
- Dashboard layout (grid of widgets)
- Global filters (date range, workspace, segment)
- Chart/table widgets with loading/error/empty
- Drill-down navigation
- Performance with many widgets
- Optional realtime/polling refresh
Out of scope: OLAP engine design, warehouse modeling (mention only as API constraints).
Assumptions: B2B analytics; users compare ranges; permissions per widget/data set.
Requirements & metrics
| Type | Examples |
|---|---|
| Functional | filter, visualize, export CSV, share link |
| Non-functional | interactive filters < 300ms UI; query may be seconds |
| Product | time-to-first-widget, error rate, saveable views |
High-level UI architecture
┌──────────────────────────────────────────────────────────┐
│ Dashboard shell: title, saved views, share, refresh │
├──────────────────────────────────────────────────────────┤
│ Global filter bar: date, compare, project, granularity │
├────────────────────────┬─────────────────────────────────┤
│ Widget grid │ Side peek / drilldown drawer │
│ KPI · timeseries · │ │
│ table · pie │ │
└────────────────────────┴─────────────────────────────────┘
Component model
DashboardPage
├── FilterBar (controlled filter state ↔ URL)
├── WidgetGrid (responsive layout)
│ └── WidgetContainer
│ ├── WidgetHeader (title, menu, last updated)
│ └── WidgetBody → Chart | Table | KPI
└── QueryClient / useWidgetQuery
Widget contract
type WidgetId = string;
type GlobalFilters = {
range: { start: string; end: string }; // ISO dates
compareRange?: { start: string; end: string };
timezone: string;
projectId: string;
granularity: "hour" | "day" | "week";
};
type WidgetConfig = {
id: WidgetId;
type: "kpi" | "timeseries" | "table" | "breakdown";
title: string;
queryKey: string; // server query template id
visualization: Record<string, unknown>;
};
type WidgetDataResponse = {
series?: { t: string; v: number }[];
rows?: Record<string, string | number>[];
value?: number;
asOf: string;
};
Data fetching & cache
| Layer | Strategy |
|---|---|
| URL | filters + dashboard id for shareable state |
| React Query | key: [widgetId, filtersHash] |
| HTTP | Cache-Control for immutable past ranges; no-store for “live” |
| Memo | expensive chart transforms |
const key = ["widget", widget.id, filters];
useQuery({
queryKey: key,
queryFn: ({ signal }) => fetchWidget(widget.id, filters, signal),
staleTime: isLive(filters) ? 30_000 : 5 * 60_000,
placeholderData: keepPreviousData, // smooth filter tweaks
});
Parallelism: widgets fetch independently with concurrency limit (e.g. 4–6) to avoid stampeding the API when a dashboard with 20 widgets mounts.
Abort: filter changes abort in-flight queries.
Rendering & charts
- Lazy-load chart library per widget type (
import("echarts")or similar) - Reserve height to avoid CLS
- Downsample large series on server; client shouldn’t plot 100k points
- Tables: virtualize long breakdowns
- Color tokens from design system (colorblind-safe palettes)
Cross-filtering
Click bar → updates global filter or local highlight. Decide:
- Global filter update (URL changes; all widgets refetch)
- Local brush (fast; only one chart)
Senior answer: support local highlight immediately + optional “apply as filter”.
Realtime / refresh
| Mode | Mechanism |
|---|---|
| Manual | refresh button invalidates queries |
| Polling | interval while tab visible |
| Push | websocket invalidation message |
Pause when document.hidden. Show asOf timestamp so users trust numbers.
Permissions
- Dashboard list filtered by ACL
- Widget query 403 → specific empty state (“Ask admin”) not generic error
- Don’t leak metric names in error payloads
Performance budgets
- Shell interactive quickly; widgets progressive
- Defer below-fold widgets (
IntersectionObservermount) - Avoid one giant Redux dump of all series data
- Prefer Web Worker for heavy client aggregations (rare if server does OLAP)
Empty / error / slow query UX
- Skeleton matching chart shape
- Timeout message with retry
- Partial failure: other widgets still render
- “Query too heavy” guidance (narrow date range)
Tradeoffs
- Flexible widget builder vs curated templates — power vs support cost
- URL-complete state vs ephemeral UI-only brushes
- Client aggregation vs server — scale favors server
- Realtime everywhere vs cost — poll critical KPIs only
Interview close
Scope filters + widget grid → independent cached queries keyed by filters → progressive load → a11y of charts (tables/summaries) → concurrency and visibility-aware refresh. Mention export and saved views as extensions.
Related on this site
- Polling Metrics Dashboard
- Frontend Caching Strategies
- Design a Design System
- System Design Interview Framework