SigNoz Dashboard Design Guide

Rules distilled from building the LiteLLM Proxy dashboard, where the first iteration was generated from a one-line brief and every improvement after that came from a human opening the page and looking at it. The corrections were not exotic. They repeat, they are cheap to apply up front, and they are expensive to discover one deploy cycle at a time. Panel JSON lives in nix/home-manager/files/dashboards/ in the dotfiles repo and reconciles through tofu on hm switch.

The rule underneath all the others

A panel ships when the rendered panel is legible, not when the query is correct. A green tofu apply and a valid payload prove nothing about whether a human can read the result. Open the dashboard over a time window that actually contains data and look at every panel you touched. Nearly every fix listed below was visible in one screenshot and none were visible in the JSON.

Panel type follows the question

QuestionPanel
How many, per intervalBar chart. Discrete counted events are bars, not a line.
What is it made ofStacked bar, one segment per component
How is it distributed over timeLine chart with explicit quantiles
One headline numberNumber tile (“hero” tile)
Per-entity comparison across many attributesOne table, one row per entity
Share of a total, few slicesPie
What went wrongLog or trace list

Two corollaries that came out of real corrections:

  • A quantile belongs on a line, not a bar. Bucketed bar charts of p50 and p99 were rejected on sight in favor of one line chart carrying both series.
  • Do not keep a distribution histogram next to per-quantile lines. Once latency is p50/p95/p99 per model, the histogram answers nothing the lines do not, and it costs a panel slot.

Layout is an argument, not a grid

  • The headline number goes first, top-left, before any chart. Cost was the reason the dashboard existed, so the cost tile leads.
  • Group by the question being asked, in sections: Traffic, then Models and Cost, then Latency, then Errors.
  • Errors go last, in their own section. They are the thing you scroll to during an incident, not the thing you read daily.
  • Put panels that are read together side by side (tokens-per-model pie beside cost over time).
  • One table per entity beats three tables keyed by the same entity. Three per-model tables (models info, cost detail, token detail) all had to be read together to answer any question, so they collapsed into one By Model row-per-model table.
  • Column count is bounded by the panel width. Nine columns overflowed at full width; the fix was dropping the two columns whose data is still on the span for anyone who needs it, not shrinking the font.

Series, statistics and honesty

  • Split by the dimension that actually varies. A service-wide p95 hid an order-of-magnitude gap between two models. Per-model series exposed it.
  • Show p50, p95 and p99 together. One quantile is an assertion; three are a shape.
  • Never label a statistic as something it is not. A sum/count mean under a panel titled p95 is a lie that survives every later reader. If a degraded statistic is genuinely the only option, rename the panel and say why in its description.
  • If the metric cannot answer the question, fix the emitter. Flat quantiles usually mean bucket boundaries, not a bad query; a missing series usually means label cardinality. Both live in the instrumentor. See 2026-09-03 The Histogram Had One Series Per Request.
  • A duplicated legend entry is a rendering artifact, not data. Same for a straight line across an idle stretch. Check the series count against the entities you know were active in the window.
  • Do not publish a number whose meaning you cannot source. gen_ai.cost.original_cost looked like a list price and would have justified a “savings” column, until OpenRouter’s own billing records showed it sitting below total cost on a quarter of spans. Verify against the provider’s docs and billing before a number becomes a column.
  • Columns that are subsets of other columns invite wrong arithmetic. Cache-read cost and reasoning cost are subsets of input and output cost, so beside them they read as addable. Percentages (“cache hit”, “reasoning share”) say the same thing and cannot be misread.

Units and precision, chosen against the real magnitude

This is the single most repeated correction. Set it once, everywhere, for every numeric panel and every table column that shares the rule.

DataUnitWhy
Counts, tokensshort636790 is unreadable; 636.79 K is a number you can compare
Sub-cent currencynone, currency in the titlecurrencyUSD axis ticks are hardcoded to two decimals, so every tick reads $0
Durationss
Ratiospercent

The y-axis never receives the panel’s decimal_precision; only tooltips do. That is why raising precision on a currencyUSD axis changes nothing and the unit itself has to change.

Colour and labels carry meaning

  • Colour encodes semantics, not variety. In the token stack: grey and dark grey for cache read and cache write (cheap, recycled), yellow for input, red for output (the expensive one). A reader learns the mapping once.
  • Legends say what the series is, never the aggregation that produced it. count() is an implementation detail; Requests is the answer.
  • Set thresholds where a number has an operational meaning, so the panel colours itself when it matters.
  • Write the panel description for the next reader, including what was deliberately left off and why. The By Model description is the reason nobody re-adds the subset cost columns.

Renderer and provider traps

Mechanics that produced visible defects, all documented in more detail in the dotfiles repo’s converter and memory notes:

  • A series with a single valid point in the window draws as a bare dot. Turn points on for sparse panels.
  • The default span-gaps mode bridges every gap, so two bursts thirty minutes apart draw a straight connecting line that reads as an extra series. Bound the fill (for example, fill gaps under 10 minutes only); the frontend floors the threshold at the step interval.
  • A panel with two standalone builder queries is rejected with a bare Update dashboard and no detail. Wrap multi-query panels in a composite query.
  • ClickHouse panels receive the window timestamps but no interval variable. Derive the bucket from the window so bar count stays fixed at any range.
  • The provider-shaped JSON files are insertion-ordered. Round-trip with json.dumps(indent=2); sort_keys=True rewrites the whole file.

Checklist before calling a dashboard done

  1. Every panel type matches the question it answers.
  2. Hero numbers first, errors last, related panels adjacent.
  3. Every numeric axis, tile and column has a unit chosen against real magnitudes.
  4. Every statistic is labelled as what it computes.
  5. Legends read as meanings; colours encode semantics; thresholds set where they matter.
  6. Descriptions record what was left off and why.
  7. The rendered dashboard has been opened in a browser over a window with real data, and looked at.

2026-09-03 The Histogram Had One Series Per Request · 2026-09-01 The Rename Was the Visible Half · 2026-07-31 SigNoz Dashboard v2 Break and Upgrade Readiness · Visual Hierarchy Principles · Homelab Services Architecture