Widgets
Widgets are the visual building blocks of a dashboard. Each widget occupies a number of columns in a 12-column grid.
Common Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Display name |
type | string | Yes | metric, chart, table, text, image, or divider |
col | integer | No | Column span from 1 to 12 |
description | string | No | Optional tooltip or subtitle |
Data-backed widgets (metric, chart, table) also need a query source: query (named query reference), sql (inline), file (external .sql path), a semantic reference (metric: for metric widgets, dimension: + metrics: for charts), or data (inline static values — see Inline data). See Queries & Templating for the full reference, including the connection override.
Metric
Metric widgets display a single value. The value is configured with a value encoding — which column to read, its type, and a d3-format string for display.
- name: Total Revenue
type: metric
sql: SELECT SUM(amount) AS value FROM sales
value:
field: value
type: number
format: "$,.2f"
col: 3field is required; bare column names (value: revenue) are not valid. When format is omitted the number is rendered with an auto-compact fallback (e.g. 1.2M).
Metric widgets can also use semantic models:
- name: Total Revenue
type: metric
model: sales
metric: revenue
filters:
- dimension: created_at
operator: between
value:
start: "{{ filters.date_range.start }}"
end: "{{ filters.date_range.end }}"
value:
field: revenue
type: number
format: "$,.0f"
col: 3Semantic metric widgets inherit the dashboard-level model when model is omitted. See Semantic Layer for model files, filters, segments, and aliases.
Metric-specific fields:
| Field | Type | Description |
|---|---|---|
value | object | Value encoding: field (required), type, and format |
value.field | string | Result column to display |
value.type | string | number, date, or category |
value.format | string | d3-format string (numbers, e.g. "$,.2f", ".0%") or d3-time-format string (dates, e.g. "%b %Y") |
metric | string | Semantic metric name |
Note: Currency and unit affixes live in the format string —
$is a d3-format prefix ("$,.2f") and%comes from the percent type (".0%"renders0.12as12%). There are no separateprefix/suffixfields.
Chart
Charts visualize one or more series. The chart field selects the chart type and determines which fields are required.
Chart types
| Chart | Required | Optional | Description |
|---|---|---|---|
line | x, y | color, yMin, yMax | Line chart. yMin/yMax shade a CI band behind the line |
bar | x, y | color, stacked, normalized, horizontal, yMin, yMax | Bar chart. yMin/yMax add error-bar caps |
area | x, y | color, yMin, yMax | Area chart. yMin/yMax shade a CI band |
pie | label, value | Pie/donut chart | |
scatter | x, y | Scatter plot | |
bubble | x, y, size | Bubble chart | |
combo | x, y | lines | Mixed bar + line chart (y series in lines render as lines, the rest as bars) |
histogram | x | bins | Histogram (client-side binning) |
boxplot | x, y | Box-and-whisker plot (client-side quartiles) | |
funnel | label, value | horizontal | Conversion funnel: one bar per stage with each stage's share of the top and step-to-step conversion. Rows render in query order — put the top of the funnel first. horizontal: true lays stages left-to-right |
sankey | source, target, value | Sankey/flow diagram | |
heatmap | x, y, value | Grid heatmap | |
calendar | x, value | GitHub-style calendar heatmap | |
sparkline | x, y | Compact inline line (60px), no axes | |
waterfall | x, y | Waterfall chart | |
xmr | x, y | yMin, yMax | Control chart with limits |
dumbbell | x, y (2 columns) | Horizontal range comparison | |
gauge | value | target | Semi-circular KPI-vs-target gauge (uses first row) |
treemap | label, value | Rectangular part-to-whole hierarchy | |
radar | x, y | Multi-axis polar comparison | |
candlestick | x, open, high, low, close | OHLC chart | |
forest | x, y | yMin, yMax, horizontal | Point estimate + CI interval per category (grey when the interval spans 0). horizontal: false → vertical dot-and-whisker; multiple y = grouped |
SQL-backed example:
- name: Revenue Trend
type: chart
chart: area
sql: |
SELECT month, revenue
FROM monthly_revenue
ORDER BY month
x: { field: month, type: date, format: "%b %Y" }
y: { field: [revenue], type: number, title: Revenue, format: "$,.0f" }
col: 8Axis encoding
x and y are encoding objects. field names the SQL column to plot (y.field accepts a list for multiple series); the remaining keys control how the axis renders:
| Key | Type | Description |
|---|---|---|
field | string | string[] | SQL column(s) to plot. Required. |
type | string | number, date, or category. Selects the axis scale and the format language (date uses d3-time-format, otherwise d3-format). |
title | string | Human-readable axis label rendered next to the axis. |
format | string | d3-format / d3-time-format string for tick labels, e.g. "$,.0f" → $1,234, ".0%" → 12%, "%b %Y" → Jan 2024. |
beginAtZero | boolean | Anchor a line/area value axis at 0 so series scale honestly instead of auto-zooming. Opt-in (default auto-scales). |
markers | boolean | Show point markers on sparse line/area series. Default true; set false to hide the dots. |
curve | string | Chart-wide line interpolation: smooth, straight, or stepline. Use straight for period totals so the line doesn't imply movement between points. |
dash | string | Chart-wide dash pattern every series inherits: dotted, dashed, or long-dash (omitted = solid). |
Per-series style overrides live in a widget-level series map (a sibling of x/y, not inside y), keyed by y-column: series: { revenue: { color: "#EC4899", curve: straight, dash: dashed } }. Each of color/curve/dash falls back to the chart-wide default (or palette for colour). Store only genuine differences.
Without format, ticks fall back to automatic compact formatting. beginAtZero, markers, curve, and dash (on y) plus the widget-level series apply to line/area (and combo) charts. Bare column names (x: month, y: [revenue]) are not valid — always wrap the column in { field: ... }.
Series by category (color)
color names a category column that splits the single y series into one series per distinct category value. The SQL returns long format — one row per x/category pair — instead of pivoting with CASE WHEN:
- name: Sales by Region
type: chart
chart: bar
stacked: true # bar only; requires color
color: { field: region }
sql: |
SELECT month, region, SUM(amount) AS revenue
FROM sales GROUP BY 1, 2 ORDER BY 1, 2
x: { field: month }
y: { field: revenue }
col: 6Rules:
colorworks onbar,line, andareacharts and requires a singleyfield.stacked: trueis bar-only and requirescolor. Listing multipleycolumns renders grouped bars (or multiple lines/areas), never stacked.normalized: trueshows each stacked bar as percentages of the row total; the y axis renders%automatically, so omity.format.horizontal: trueflips a bar chart: categories run down the vertical axis.
Semantic example:
- name: Revenue Trend
type: chart
chart: area
model: sales
dimension: created_at
granularity: month
metrics: [revenue]
sort:
- name: created_at
direction: asc
col: 8Common chart fields:
| Field | Type | Description |
|---|---|---|
chart | string | Chart type |
x | object | X-axis encoding (field, type, title, format) |
y | object | Y-axis encoding (field may list several series columns; supports beginAtZero, markers, curve, dash, and per-series series overrides for line/area — see Axis encoding) |
label | string | Label column for pie, funnel, and treemap charts |
value | object | Value encoding ({ field: ... }) for pie, funnel, sankey, heatmap, calendar, treemap, and gauge charts |
source | string | Source node column for sankey charts |
target | string | Target/max column for gauge charts (also sankey target node) |
open | string | Open column for candlestick charts |
high | string | High column for candlestick charts |
low | string | Low column for candlestick charts |
close | string | Close column for candlestick charts |
color | object | Category column ({ field: ... }) that splits the single y series into one series per category — bar, line, and area charts |
stacked | boolean | Stack the color series (bar charts only; requires color) |
normalized | boolean | Render stacked bars as percentages of the row total (requires stacked) |
horizontal | boolean | Horizontal layout: bar charts put categories on the vertical axis; funnel charts lay stages left-to-right; forest charts are horizontal by default (false = vertical) |
size | string | Bubble size column for bubble charts |
lines | string[] | Which y series render as lines (rest as bars) for combo charts |
series | object | Per-series line style overrides keyed by y-column: { revenue: { color, curve, dash } } — line/area/combo. Each key falls back to the chart-wide y.curve/y.dash/palette |
bins | integer | Number of bins for histogram charts (default 10) |
yMin | string | object | CI lower bound: xmr control limit, line/area CI band, bar error-bar cap, forest interval. A column name, or a per-series map { seriesColumn: boundColumn } for multi-line/grouped charts |
yMax | string | object | CI upper bound (see yMin) |
refLines | object[] | Reference guide lines: [{ axis: x|y, value, label?, color? }] — dashed line (e.g. a 0 no-effect mark). line/area/bar/forest |
refBands | object[] | Shaded reference bands: [{ axis: x|y, from, to, label?, color? }] — a translucent range (e.g. a ±MDE band). line/area/bar/forest |
dimension | string | Semantic dimension name |
granularity | string | Semantic time grain for dimension |
metrics | string[] | Semantic metric names |
filters | array | Semantic filters |
segments | string[] | Semantic segments |
sort | array | Sort instructions |
limit | integer | Row limit |
Charts using dimension, metrics, segments, or semantic filters are compiled through the backend semantic layer instead of requiring hand-written SQL.
Table
Tables display query results in a scrollable grid.
SQL-backed example:
- name: Recent Orders
type: table
sql: |
SELECT id, customer_name, amount, status, created_at
FROM orders
ORDER BY created_at DESC
LIMIT 20
columns:
- name: id
label: Order ID
- name: amount
label: Amount
format: currencySemantic example:
- name: Sales Breakdown
type: table
model: sales
dimensions:
- name: region
- name: channel
metrics: [revenue, sales_count]
sort:
- name: revenue
direction: desc
columns:
- name: region
label: Region
- name: revenue
label: Revenue
format: currencyTables can mix semantic dimensions and metrics with explicit columns metadata for display labels and formatting.
Table column fields:
| Field | Type | Description |
|---|---|---|
name | string | Result column name (must match the SQL output) |
label | string | Display header (defaults to name) |
align | string | Text alignment override: left, center, or right. Applies to the column header and its body cells. Use it to right-align a text value like £177K that isn't detected as numeric. |
hidden | boolean | Keep the column in the result but don't render it, see Hidden columns |
format | string | object | Value display and conditional coloring, see below |
Conditional formatting
A column sets value display with number and coloring with format. number is currency, number, or a d3-format string. format is an ordered list of layers, and for each cell the first layer that matches wins.
format is polymorphic for backward compatibility: a scalar string (e.g. format: currency) is the older value-display shorthand and is treated as an alias for number; a list is the conditional-format layers. Prefer number in new dashboards, and use a list format for coloring (the two can coexist).
A layer's if is optional: with if, the layer styles only the cells that match the condition; without if, the layer styles every cell.
- name: score
number: number
format:
- { if: greater_than_or_equal, value: 80, backgroundColor: green } # matches only score >= 80
- { backgroundColor: [red, white, green] } # no if → every other cell, so lastColumn keys
| Key | What it does |
|---|---|
number | Value display: currency, number, or a d3-format string. |
align | Text alignment: left, center, or right (aligns the header and body cells). |
like | Mirror another column's coloring, driven by that column's per-row value (keeps own number). |
hidden | Keep the column in the result (so it can drive cross-column rules or be a like source) but don't render it. |
format | Ordered list of style layers; first match wins. |
Layer keys (the Group column shows which keys belong together):
| Group | Key | What it does |
|---|---|---|
| Condition | if | The operator (see the table below). Omit to apply the layer to every cell. |
| Condition | value | What if compares against: a scalar, [low, high] for between, { column: X } for cross-column, omitted for empty checks. |
| Fill | backgroundColor | A string (flat fill) or a list of colors (gradient, at least 2). |
| Fill | range + unit | Gradient only. range is a list of anchor numbers; unit is absolute, percent, or percentile. Omit range for an auto min/max gradient. |
| Text | textColor | Text color. |
| Text | bold / italic / underline / strikethrough | Text styling (booleans). |
Each layer is a YAML object, so these two forms are identical — use whichever reads better:
- { backgroundColor: [red, white, green], range: [-25, 0, 25], unit: absolute } # compact (flow)
- backgroundColor: [red, white, green] # block
range: [-25, 0, 25]
unit: absoluteColors (any color field)
| Kind | Values |
|---|---|
| Named | red green blue indigo cyan purple pink amber |
| Fixed | white black |
| Aliases | positive (green), negative (red), warning (amber) |
| Hex | any "#rrggbb" |
Named colors match the chart palette and adapt to light/dark; white and black are fixed literals that stay the same in both themes. Named fills are softened toward the theme surface so the theme text stays legible.
Operators (used by a layer's if)
| Group | Operators |
|---|---|
| Empty | is_empty, is_not_empty |
| Text (case insensitive) | text_contains, text_does_not_contain, text_starts_with, text_ends_with, text_is_exactly |
| Number | greater_than, greater_than_or_equal, less_than, less_than_or_equal, is_equal_to, is_not_equal_to |
| Range | is_between, is_not_between |
| Date | date_is, date_before, date_after (by day for a date, or exact instant if the value has a time) |
Worked example (a full dashboard exercising every feature above):
name: Regions
rows:
- widgets:
- name: Regions
type: table
col: 12
sql: SELECT region, revenue, growth, margin, score, status, actual, target, bonus, due FROM regions
columns:
- name: region
format:
- { backgroundColor: "#F8FAFC", bold: true } # flat fill + text style
- name: revenue
number: currency # value display: currency
format:
- { backgroundColor: [red, white, green] } # gradient, auto min→max
- name: growth
number: number
format: # compact form (0 = neutral middle)
- { backgroundColor: [blue, white, amber], range: [-25, 0, 25], unit: absolute }
- name: margin
number: ".0%" # d3-format string; same layer, block form
format:
- backgroundColor: [red, amber, green]
range: [0, 50, 100]
unit: percentile # anchor by percentile (midpoint = median)
- name: score
number: number
format: # conditions, first match wins
- { if: greater_than_or_equal, value: 80, backgroundColor: green, textColor: white, bold: true } # fill + text styles together
- { if: is_between, value: [50, 79], backgroundColor: amber } # [low, high] for between
- { if: less_than, value: 50, backgroundColor: "#FEE2E2", textColor: red, strikethrough: true } # fill + text style
- name: status
format: # text conditions
- { if: text_contains, value: urgent, backgroundColor: amber, bold: true }
- { if: text_is_exactly, value: overdue, backgroundColor: red }
- { if: is_empty, backgroundColor: "#F3F4F6", italic: true } # no value for empty checks
- name: actual
number: number
format:
- { if: less_than, value: { column: target }, backgroundColor: red } # cross-column, same row
- { if: greater_than, value: { column: target }, backgroundColor: green }
- name: target
hidden: true # in the result for the rule above, not rendered
- name: bonus
number: currency
like: score # mirror score's colors, keep own number
- name: due
format: # date conditions (fill + text)
- { if: date_before, value: "2026-07-22", backgroundColor: red, textColor: white, bold: true }
- { if: date_after, value: "2026-07-22", backgroundColor: green, textColor: white }
- name: health
number: number
format: # order matters: this wins over the layer below
- { if: is_equal_to, value: 0, backgroundColor: red, bold: true }
- { backgroundColor: [red, white, green], range: [0, 50, 100], unit: percent }Order matters. Layers are checked top to bottom and the first match wins, so list the specific conditions first and any catch-all (a gradient or flat fill with no if) last. growth is written in the compact one-line form and margin as an indented block; both are the same layer shape.
How the example reads, column by column:
region: one flat color on every cell, bold text (a single layer, string fill plus a text style).revenue: currency, colored by an auto min→max gradient.growth: a blue/white/amber gradient with the neutral point pinned to 0 viarange+unit: absolute.margin: a percentage, colored by a gradient anchored bypercentileso the midpoint sits at the median.score: numeric conditions covering twovalueforms, a scalar (>= 80) and a[low, high]range (50 to 79); each layer combines abackgroundColorfill with text styles (< 50gets a light-red fill plus red struck-through text).status: text conditions checked in order, first match wins (contains "urgent", exactly "overdue", or empty).actual: cross-column conditions, compared totargetin the same row (explained next).target:hidden: true, so it stays in the result to driveactual's comparison but is not rendered as its own column.bonus:like: score, so it takes score's colors per row but keeps its own currency display.due: date conditions, before or after a cutoff day.health: a condition (= 0turns red) checked first, then apercent-range gradient with noiflast, for every other value.
Cross column, step by step. Look at the actual rule:
{ if: less_than, value: { column: target }, backgroundColor: red }The cell being colored is actual. value: { column: target } means "compare it to the target column in the same row", not to a fixed number. So for every row independently: if that row's actual is less than that row's target, the actual cell turns red. Row 1 uses row 1's target, row 2 uses row 2's target, and so on. target is marked hidden: true here, so it drives the comparison without taking up a column of its own. (Writing value: 100 instead would compare against the constant 100.)
Hidden columns
A column with hidden: true stays in the query result but is not rendered. It is optional: coloring reads a column whether or not it's shown. Hide only to drop a column from the display.
columns:
- name: actual
number: number
format:
- { if: less_than, value: { column: target }, backgroundColor: red }
- name: target
hidden: true # feeds the rule above, never renderedHidden columns are still in the underlying data, so they appear in CSV exports. hidden has no effect on non-table widgets.
Inline data
metric, chart, and table widgets can carry their data inline instead of a query source. A widget with data renders without a connection or SQL — useful for hardcoded numbers, sample dashboards, or building a dashboard before a warehouse is connected. The encoding fields (x, y, value, label, columns) reference the column names declared in data.columns.
- name: Revenue by Quarter
type: chart
chart: bar
col: 6
data:
columns: [quarter, revenue]
rows:
- [Q1, 12000]
- [Q2, 15500]
- [Q3, 14200]
- [Q4, 18900]
x: { field: quarter, type: category }
y: { field: [revenue], type: number, format: "$,.0f" }data fields:
| Field | Type | Description |
|---|---|---|
columns | list of strings | Column names; referenced by the encoding fields |
rows | list of lists | One list per row, positional — each must have one value per column |
data is mutually exclusive with sql and query. It is not valid on text, image, or divider widgets.
Text
Text widgets render Markdown content.
- name: Notes
type: text
col: 6
content: |
**Important:** Data refreshes daily at 06:00 UTC.Supported markdown:
- Headers (
#through######) - Bold (
**text**), italic (*text*), bold italic (***text***), strikethrough (~~text~~) - Inline code (
`code`) - Links (
[text](url)) and images () - Unordered lists (
-or*) and ordered lists (1.) - Blockquotes (
> text) - Horizontal rules (
---,***, or___)
Image
Image widgets render an image from a URL.
- name: Logo
type: image
col: 3
src: https://example.com/logo.png
alt: Company LogoImage-specific fields:
| Field | Type | Required | Description |
|---|---|---|---|
src | string | Yes | Image URL |
alt | string | No | Alt text for accessibility |
Divider
Divider widgets add visual separation between sections.
- name: Section Break
type: divider
col: 12Exports
The dashboard viewer can export widget data as CSV and rendered dashboard or widget views as PNG/PDF. In dac serve, exports use the currently applied filters. In static dac build dashboards, exports use the query results baked into the HTML at build time.
Per-widget Exports
Chart and table widgets show an export menu in the widget header on hover or keyboard focus:
- CSV downloads that widget's result set as
<widget-name>.csv(RFC 4180 CSV with quoted cells). - PNG captures the rendered widget as
<widget-name>.png. - PDF captures the rendered widget as
<widget-name>.pdf.
Dashboard Exports
The dashboard header has an Export menu:
- CSV exports every data-bearing widget (metrics, charts, and tables) in one file. Each widget is written as a section introduced by a
# <widget-name>divider line, followed by the column header and rows, with a blank line separating sections. The file is named<dashboard-name>.csv. - PNG captures the rendered dashboard view as
<dashboard-name>.png. - PDF captures the rendered dashboard view as
<dashboard-name>.pdf.
In dac serve, CSV exports reflect the active filter values at the time of the click. Static dashboard CSV exports use the baked query results. Text, divider, and image widgets are skipped in CSV exports. Widgets with query errors or no rows are omitted from the dashboard CSV.
PNG and PDF exports capture the visible rendered view, excluding viewer controls such as export buttons and YAML controls. For tabbed dashboards, they capture the currently active tab.