Dashboard style

How a composable dashboard should look in any tool: layout, colours, type, numbers and status.

ℹ️

Private preview

Composable dashboards and the metrics API are in private preview: available to invited accounts, and features may change. See Integration and feature lifecycles for what this means, and reach out to your account representative to request access.

🤖

Using an agent? It follows this page automatically. You only need it if you are building a dashboard by hand.

Overview

The dashboard examples say what to show; this page says how to lay it out so a reader sees the answer quickly, whether the page is built by an agent writing HTML, Streamlit, a notebook or a BI tool.

Each dashboard example lists its own blocks under Layout and one query per block under Queries; the header, filters, cards and "How this was built" come from this page.

A dashboard is a page the reader opens: a file, a notebook or a dashboard in their BI tool, in the format they chose. Tables in a chat answer are not a dashboard.

Layout

A dashboard tells one story from the top down, from the general to the particular: the title names the dashboard, the first row answers its question, and each block below answers the question the one above it raises. Is it good? (the answer) Where is it not? (the breakdown) Which ones exactly? (the detail) What do I open next? (links to Monte Carlo). A block that answers none of these is left out. Blocks and columns come only from the dashboard example's Layout, and every block it lists is built, in its order: a word in the prompt ("by domain") never adds one and never removes one. The Monte Carlo product dashboards (Data quality, Coverage, Data operations) follow this order:

  1. Header and filters. The title: the dashboard's name, the example's name when built from one ("Alert operations", "Asset-level data quality"), without the grouping or the window ("by domain", "this week") and never a question. Then one muted line with what the reader cannot see elsewhere, each said once, in the product's words, with no identifiers, disclaimers or glosses: the account's name, the window in dates ("Oct 12 to Oct 19") or "current state", and "Data as of Oct 19, 12:07 UTC", to the minute as the account gives it, never rounded. A window ends on the last day its data holds: credits arrive by day, so a credit window that runs to today ends yesterday. What a grouping means goes in How this was built. Under it a row of filters that set the scope of the whole page (Interaction).
    • One grouping per dashboard: domains, unless the request asks for data products. Never show both side by side: they overlap, so mixing them double counts tables.
    • Both at once (one domain and one data product, as an "and", like the product's filters): take the tables of each member, keep the tables in both, and filter the table-level queries on them, as Combining groupings on the Dimensions page says. Per-member metrics cannot do this in one query.
    • Domains of alerts: an alert has the domain of its monitor, as the Alerts page filters, and the domains of its tables; the two counts differ. Say under the alert card which one it uses.
    • A filter sets the scope and shows no values; the grouping in a prompt ("by domain") asks for this filter.
    • The filter lists the members of that grouping (domain_count or data_product_count grouped by name, deleted data products left out) and starts at all tables. With a member selected, every block uses the grouping's metric of the same name (domain_... or data_product_...) filtered on its name; a block with no such metric keeps its card and says in its caption that it counts all tables. A block never ignores the filter silently.
  2. The answer. One row of up to four number cards (Cards): the headline value large, a short label, its parts in one small line ("27 of 32 tables"), and its trend beside it when the data keeps history.
  3. The breakdown. A chart, not text: the members (data quality dimensions, monitor types, severities, alert types) as horizontal bars or small cards with a ring, worst first, sorted as the dashboard page says. Many members, such as domains or data products: a table with one row per member and a bar in the row, worst first, the ones with no result last.
  4. The detail. A table to drill into: one row per table, monitor or alert, worst first, with a status column, the reason (failing data quality dimensions, alert type) and a count. Show the first 10 to 25 rows and say how many more there are.
  5. How this was built. Collapsed at the bottom, in the order and form How this was built gives.

Put related numbers side by side and keep the page to one screen above the detail table. The page uses the width of the window, up to about 1,400 px.

Show, then tell. The first screen is mostly shapes: number cards with a ring or a sparkline, and at least one chart. A page whose first screen is text and tables reads as a report, not a dashboard. Words support the shapes: a label of a few words, one short caption at most, and the rest on hover or in How this was built.

Cards

  • Number card: the value large (32 to 40 px, bold), a label of two to four words under it ("tables alerted", never a sentence), and at most one small muted line with its parts or its comparison ("1.5% of 2,007 tables", "−13.6% against 2,149"). A share of a whole is drawn, not only written: a small ring or a thin bar in the card, coloured by status when the share is passing or failing. When the metric keeps history, a sparkline of the last periods sits under the value. The headline is one number, never "31 of 2,007". A card says each word once: with a title above the value ("Alerts"), the line under it adds context ("+4.0% against 174"), never the title again ("alerts").
  • Chart card: the title small, top left; the chart's summary number top right ("28.1 hours, median"); the chart from Choosing a chart.
  • Empty card: keep the card and say why it is empty or cannot be drawn ("No alerts in this period. Change the filters to see more.", "No history kept yet."), rather than dropping it.
  • Caption: under a block, at most one short line, about ten words, on what it counts or leaves out. Longer notes (what a grouping means, why a value can differ from the product) sit behind a small ⓘ that shows them on hover. Sort order, ties, the requests and how the numbers were derived go in How this was built.
  • Table: values carry small visuals where they help reading: a data bar behind a count, a status dot, a ring or bar for a rate, a sparkline for a trend. Fits its card with every column in view, never behind a horizontal scroll; headers may wrap to two lines to make room. One line per row. The name column is wide enough for the names, and a long name is cut with "…" and shown whole on hover. A cell that lists members (domains, tags) shows the first two and "+3 more", never one chip per member. Blocks that share rows are matched on the item's UUID or MCON, never its name; an item missing from one request shows 0 for a count and "no result" for a score, never a blank. A value the data does not have shows "no score" or "not seen", never NaN, null or undefined. Names are shown exactly as the request returned them, never retyped. A table cut at a row limit is sorted by its order and then by name, so the cut never falls at random inside a tie.
  • Cards sit on the page background with a thin border and small rounded corners, with even gaps between them.

Colours and type

UseValue
Page background#f9f9f7
Card and chart surface#fcfcfb, a 1 px border #e1e0d9
Text#0b0b0b; muted text and axes #52514e
Primary series, bars, links#2a78d6
Accent (one highlight per page)#eb6834
Categorical series, in this order#2a78d6, #eb6834, #1baf7a, #eda100, #e87ba4, #008300, #4a3aa7, #e34948
Passing / failing statusgreen #1baf7a / red #e34948, as a small pill with a dot

Use a light theme. Type: one clean sans serif for text and figures (Inter, Outfit, or the system font), with tabular figures (font-variant-numeric: tabular-nums) so digits line up in columns; no monospaced face. Hierarchy by size and weight: the page title 24 px, card and block titles 13 to 14 px semibold, values 32 to 40 px bold, labels and captions 12 to 13 px muted. Generous padding inside cards (16 to 20 px).

Colour carries meaning. Green and red are for passing and failing only (pills, rings, dots), so red always means a problem; a plain share or count (8 of 9 data products, coverage) is drawn in the primary blue. The primary blue is for the measured series, the accent once per page for the thing to look at. A page all in one blue with grey text has no meaning in its colour; a page with more than eight colours has too much.

Choosing a chart

Pick the chart from the shape of the answer: what is measured, and what it is split by.

The answer isChartNotes
One current value: a count, a rate, a scoreNumber cardSee Cards; the change against the previous period of the same length when the metric keeps history
A count per period: alerts per week, credits per dayVertical bars, one slot per periodSee Numbers and Status and labels
A rate, median or score over timeLine, with a point on hoverA few lines at most, labelled at their ends
Parts of a total per period, parts that add up: credits by monitor typeStacked bars, one slot per periodAt most six parts, the rest as "Other"; the total on top
Members of an overlapping grouping over time: domains, data products, tagsSeparate lines, or one small chart per member on the same scaleSee Charts
A ranking: alerts by alert type, tables by alertsHorizontal bars, largest firstThe value at the end of each bar; the first 10, then "N more"
The share of two or three parts that add up: passing and failing tablesOne 100% bar, or a pie or donutAt most three parts
Two categories at once: monitor type by result, domain by warehouseHeatmap, a grid with the value in each cellRows and columns sorted by their totals
A distribution: tables by importance scoreBars per band of equal width, split in two by a status (with and without a monitor)Bands labelled by their range ("0.8 to 0.9")
A monitor's measured values, run by run: a metric, a row count, a validation resultAlways a line of the value per run, with the threshold as a dashed line (or a band between the lower and upper bound) and breached runs as red pointsNever bars, never averaged or smoothed
Many members of a grouping with their value: domains or data products by healthTableSee The breakdown
A list of tables, monitors or alertsTableSee The detail; a small line in the row when each item has its own trend

Charts

  • Value axis from zero. Bars, areas and counts start at 0; a rate runs from 0 to 100%. Only a line of measured values, such as a monitor's value against its threshold, may span just the range of the data, and then the axis says where it starts.
  • Round ticks. Steps of 1, 2 or 5 times a power of ten (1, 2, 5, 10, 20, 50, 100, 1,000), four to six of them, whole numbers for counts. The axis ends at the first round tick above the largest value, a stacked bar's total included, so no bar is cut.
  • One scale. One value axis per chart, and the same scale on charts set side by side for comparison.
  • Time left to right. Oldest first, a tick per period of the grain or a round subset of them (every second week, every month), with the grain in the axis title.
  • Bars sit on period slots, not on a time scale. Daily, weekly or monthly bars get one evenly spaced slot per period; on a continuous time scale they shrink to slivers and weekly bars read as daily. Lines may use a time scale.
  • Every category is labelled. On a category axis no label is skipped; when they do not fit, turn them or fold the smallest into "Other", and say so. A chart of categories grows in height with their number instead of squeezing them.
  • Overlapping members are separate bars. Domains, data products and tags are never stacked or drawn as parts of a pie (Totals across members); the account's number goes on its own card or as a reference line.
  • Sized to its card. Whatever draws the chart (a charting library such as Vega-Lite, Chart.js or ECharts, the BI tool's own charts, or SVG written by hand), it fills the card's width and is redrawn when the page is resized, keeps its text at the page's small size (11 to 13 px, never larger than the card title) and leaves room for ticks and axis titles. An SVG drawn at a fixed size (a viewBox) and stretched to the card scales its text with it: draw it at the card's width instead.
  • Name what is measured. The axis title and the legend say the measure and its unit ("invalid rows", "credits", "h"), never "value".
  • Labels stay off the data. A threshold, target or note is labelled at the end of its line, outside the plot area or in the legend, not across points or bars.
  • Light gridlines, no 3D.

Interaction

When the dashboard is interactive (HTML, Streamlit, a BI tool):

  • Self-contained file: a dashboard sent as a file or opened without a network carries everything it needs: styles and charts inside the file, no external scripts, fonts or images, unless the environment is known to load them.
  • Hover: every bar, point and cell shows its exact value, period or member, and parts ("12 of 14 tables").
  • Filters: the window choices are 7, 30 or 90 days, the example's window by default.
  • Only controls that work. A filter, toggle or selector appears only when changing it changes the page. A fixed window is text in the header ("Last 30 days, Oct 1 to Oct 30"), never a control that cannot be changed; a choice with one option is not a choice.
  • Filters in a static file: an HTML file cannot query when a filter changes, so it carries the data for every choice: one request per block grouped by the member's name (the grouping's metric, domain_... or data_product_...), next to the all-tables request; a request per member only for a block whose metric has no per-grouping version. The row limit decides how many members the filter keeps; say how many are left out.
  • Sections: when the blocks below the answer take more than one screen, switch between them with tabs rather than one long page; the answer row stays on top.
  • Legends: a click on a series in the legend hides or shows it.
  • Tables: sortable by every column, with the default sort the page names.
  • Display controls (a minimum count to list, the number of rows) change what is shown, never how a metric is counted.

Numbers

  • Every number comes from a request. Put the answers in the page as they came back and compute totals, shares and changes in the page; never type or estimate a number.
  • Percentages with one decimal (91.9%), counts with thousands separators (1,222), hours with one decimal (16.1 h).
  • A rate shows its parts nearby, in smaller text: 91.9% with 548 of 596 tables. A median or average shows how many items it rests on, in its card and in every row: 10.3 h over 1 alert; a change between two medians gives both counts (2 alerts against 19).
  • A period with no rows is zero: fill missing days and weeks with 0, so a series has no gaps.
  • A header names what the metric counts, in the catalog's word: alerts for ..._incident_count, anomalies for ..._anomaly_count, metered credits for consumed_credits.
  • Keep empty members as their own row, last whatever the sort; do not drop them: "Not classified" for monitors without a data quality dimension, "No severity set" for alerts without a severity.
  • Under a table, say how many rows it leaves out; how ties were ordered goes in How this was built.

Links to Monte Carlo

Link each alert, monitor, table, domain or data product a dashboard or an answer names to its page in Monte Carlo, with the item's name as the link text, so the reader can act on it.

  • Address: the one you open Monte Carlo at: https://getmontecarlo.com, or your instance's, such as https://eu1.getmontecarlo.com. With the Monte Carlo MCP server, its get_mc_webapp_url tool returns it.
  • IDs: only from a query's rows; never guess one or build it from a name. Every request that lists items the dashboard names groups by the item itself as well as its name (monitor_domain and monitor_domain__domain_name), so each row carries the UUID, or the MCON for a table, its link needs.
  • Write the MCON as it is. Keep its ++ and :; only / becomes %2F and # becomes %23. Encoding the whole MCON as a URL component (%2B%2B, %3A) opens a blank page.
PagePath after the addressGroup by
An alert/alerts/<uuid>incident
A monitor/monitors/<uuid>monitor
A table or view/assets/<mcon>asset
A domain/domains/<uuid>domain or monitor_domain
A data product/data-products/<uuid>data_product
An audience/settings/audiences/<uuid>audience
Monitors of a domain/monitors?domainIds=<uuid>monitor_domain (the count is monitor_domain_monitor_count)
Monitors with a monitor tag/monitors?tags=<name>, or <name>%1F<value> for a tag with a valuemonitor_tag__monitor_tag_name, monitor_tag__monitor_tag_value
Data quality dashboard of a domain or data product/dashboard/data-quality?domain_ids=<uuid> or ?data_product_ids=<uuid>domain, data_product
Coverage dashboard of a domain or data product/dashboard/coverage?domain_ids=<uuid> or ?data_product_ids=<uuid>domain (the domain of the tables), data_product

Example: https://getmontecarlo.com/monitors/7c76b27e-3450-4315-8f7f-4ec5c775b928. Other items, AI agents among them, have no link here; link the list or dashboard above instead.

Links to a list of alerts

A count of alerts links to the Alerts page showing the same alerts: the page must count what the block counts, and a count no page can show gets no link. Put every filter of the block's request into the link, joined with &; a list of values is joined with commas.

The block's requestIn the linkGroup by, for the value
The window, metric_time__day from D1 to D2start_time=D1T00:00:00.000Z&end_time=D2T23:59:59.999Z; a window that runs to today ends at "Data as of": end_time=2026-10-23T12:00:00.000Z
incident_count, or a grouping's ..._incident_count (every alert)severities=no_severity,SEV-1,SEV-2,SEV-3,SEV-4
incident_declared_count, or filtered on incident__is_incident_declaredseverities=SEV-1,SEV-2,SEV-3,SEV-4
incident_open_count, or a grouping's ..._incident_open_count (open alerts)statuses=no_status,investigating,work_in_progress, with severities as for every alert
A severityseverities=SEV-2incident__incident_user_severity
A monitor domaindomain_ids=<uuid>monitor_domain
A data productdata_product_ids=<uuid>data_product
A monitor tagmonitor_tags=<uuid>monitor_tag (one UUID for each name and value)
An audienceaudience_ids=<uuid>audience
An alert typesub_types=<sub type>,<sub type>incident_sub_type__incident_alert_type and incident_sub_type: the link lists every sub type of that alert type
A statusstatuses=<status>, or statuses=no_status for noneincident__incident_status

Example, the alerts of one domain in a 30-day window: https://getmontecarlo.com/alerts?domain_ids=dc7db362-2dca-4ece-9398-0d92ee9c9229&severities=no_severity,SEV-1,SEV-2,SEV-3,SEV-4&start_time=2026-09-23T00:00:00.000Z&end_time=2026-10-22T23:59:59.999Z.

  • Always set severities. Without it the page leaves out alerts in declared incidents and counts fewer than incident_count.
  • The window in UTC, whole days, ending at "Data as of" to the second when it runs to today, or the page also counts the alerts created since. Never the page's presets ("Past 4 weeks"): they count from the reader's local midnight to now.
  • At most 65 days. The Alerts page refuses a longer window ("Maximum allowed date range is 2 months"). A number over 90 days or 13 weeks gets no link; link each week or month of its series instead.
  • Open alerts need a window too. A link without one shows the page's own window and every status, not the open alerts the block counts. Open alerts over all time get no link; to link them, count the open alerts created in a window of at most 65 days and say so.
  • A tag by its UUID. The Alerts page takes monitor tag UUIDs, not names; the Monitors page takes names. Write a tag with a value as <name>%1F<value>: <name>:<value> matches nothing.
  • No domain in the link shows the domain picked in the Monte Carlo header, which may be a single domain. For a number over the whole account, say so in the caption.
  • Counts can drift a little. A status set after "Data as of" moves an alert to another status on the page; its total stays the same.

Status and labels

  • Status words only where the catalog defines them: a table is passing or failing in a window (no data quality alert or at least one), an alert is open or resolved, a run breached or not.
  • A yes-or-no column (key table, deleted, monitored) is titled with the question and shows Yes or No, never the column's own word for yes ("Key" / "No").
  • No grades for a score ("Excellent", "Good", "Poor") and no red-amber-green on a score: the catalog defines no cut-offs. Show the number.
  • A partial period (this week to date) is marked as partial, not drawn as a drop, and has no change against a full period.
  • A change against a previous period shows its sign in muted text, never green or red: whether more or less is better depends on the reader. Green and red are for passing and failing only.

How this was built

The last block of every dashboard, collapsed: four parts in this order, each a heading over a list with one item per line, every item under 20 words, about 15 lines in all besides the requests. Two items never share a line or a paragraph. It says what a reader needs to check a number, nothing more.

  1. Source. One line: the account's name, "Data as of" in UTC, and "a snapshot: run the requests again to refresh it".
  2. Definitions. One line for each metric on the page, in the data catalog's words: the name on the page, the metric in backticks, what it counts. Example: "Alerts (incident_count): alerts created in the window, without monitors that failed to run." A grouping only when its membership is not obvious; a metric that is the same for each grouping is defined once.
  3. Choices. At most three lines, only choices that change a number on the page: a window the request did not give, a filter a topic's rule added, a number computed from returned rows. Sort orders, ties, and how many rows a block leaves out stay out: that count is the block's caption.
  4. Requests. For each block, under its title, the metrics API request that fills it, exactly as sent and ready to send again: the JSON body on one line in a code block, for example {"metrics": ["asset_incident_count"], "group_by": ["asset"], "filters": [{"field": "metric_time__day", "op": "gte", "values": ["2026-10-16"]}]}. A request two blocks share is listed once, under both titles. A window that ends before today has both ends in its filters (gte and lte), so it returns the same rows when sent again. Requests are not described in words.

When the product shows one of the page's numbers differently (the example's "In the product:" lines), the definition says how they differ, so a reader who compares them is not misled.

A definition is not a choice and a choice is not a request: each goes in its own part. No account of how the page was made.

Before you hand it over

Check the page against every bold rule in Charts and Interaction, then for these common ways a correct number gets drawn wrong:

  • A title that is a question or differs from the example's name, or carries the grouping ("by domain").
  • A first screen of text and tables with no chart, ring or sparkline; a card whose headline is a sentence or "N of M"; a card that repeats its title under the value; red on anything that is not failing; a caption longer than one line; figures in a monospaced font.
  • A line over time with a single point, or a series that is mostly empty: show a number card instead.
  • A chart of a request that returned no rows, or was cut at its row limit, without saying so.
  • A number typed by hand, or one that no request returned.
  • More than eight colours on one chart, or a colour that means different things on two charts.
  • A link to a list of alerts without severities or the block's window, or with a window over 65 days, or a link to open alerts without statuses.
  • How this was built longer than about 15 lines, with its parts out of order or mixed, with two items in one paragraph, or with a count of rows a block leaves out.
  • A table row taller than one line, or a column hidden behind a horizontal scroll.
  • A yes-or-no column showing anything but Yes and No.
  • NaN, null or undefined anywhere on the page, a name that differs from the returned row, or a row limit that cuts inside a tie.
  • A block that ignores the page's filter without saying so in its caption.
  • A median without the count it rests on, or "alerts" over a count of anomalies.
  • A member the example says links to Monte Carlo, shown without its link.
  • "Not classified", "No monitor" or "Other" anywhere but last.
  • A script error that leaves the page empty. Open the file before handing it over when you can, and keep the script's names inside one function: a global named top, name or status clashes with the browser's own and stops the page.

Did this page help you?