Designing a Custom Dashboard Builder
Store intent, not rendered output
The first decision is what a saved dashboard actually is. The answer is a small declarative document:
{
"version": 1,
"widgets": [
{ "id": "w1", "type": "kpi", "metric": "visitors", "x": 0, "y": 0, "w": 3, "h": 2 },
{ "id": "w2", "type": "chart", "metric": "traffic", "chart": "area", "x": 3, "y": 0, "w": 9, "h": 4 }
]
}Store the intent — which widget, which metric, where. Never store rendered data. A dashboard saved in March must show March's numbers when opened in March and today's numbers today.
The widget registry
Mirror the tool-registry pattern: one definition per widget type, declaring what it needs and how to fetch it.
{
type: 'kpi',
label: 'Single metric',
options: { metric: { type: 'string', enum: ['visitors','pageviews','bounceRate'] } },
fetch: (opts, ctx) => queries.getKPISummary(ctx.siteId, opts.dateRange),
}Adding a widget becomes a registry entry rather than changes in the renderer, the saver and the loader.
Version from day one
Layouts are user data that outlives your schema. The version field costs nothing now and is the difference between a clean migration and breaking every saved dashboard later:
function migrate(doc) {
if (doc.version === 1) doc = v1ToV2(doc);
return doc;
}Adding this retroactively means guessing which shape each stored document is in.
Batch the fetches
A dashboard with eight widgets should not fire eight sequential requests. Collect the data requirements, deduplicate them — three widgets often want the same traffic series at the same range — and fetch once.
The coalescing cache handles most of this if every widget goes through the same query layer with the same keys.
Grid geometry
Use a fixed column count (12 is conventional) with integer positions. Pixel coordinates do not survive a window resize or a different screen.
Validate on save: widgets within bounds, no negative sizes, no overlap if your grid does not support it. A corrupt layout that renders a blank page is much harder to debug than a rejected save.
Sensible defaults
Most people never build a dashboard from scratch. Ship two or three templates — an overview, a content view, an acquisition view — and let customisation start from one of those. The blank canvas is where dashboard builders go to die.
Try InsightsTrack free
Open-source, self-hosted, privacy-first analytics — explore the live demo, no install required.
Open live dashboard