Widgets
Every dashboard.toml widget type — required and optional keys, a working example, and a live render for each.
Every widget is a [[widgets]] entry
A dashboard file (dashboard/<name>.toml, or the legacy root dashboard.toml)
renders one view per [[widgets]] table. Every entry, of
every type below, needs the same three keys:
Prop
Type
Everything else is type-specific — covered per type below, each with a live
render produced by the same components the product uses. A widget with an
unknown type, or missing one of its type's required keys, is dropped from
the rendered dashboard and reported by harumi dashboard validate rather than breaking the page.
Beyond widgets
dashboard.toml can also declare reusable datasets, define new KPIs as SQL,
and add clock-driven playback — none of which are covered here. See
Dashboard datasets, metrics, and the clock.
For what a color/colors value below may be, and the full palette of
names, see Colors.
Layout
You don't place widgets. The dashboard packs them into rows in the order they
appear in [[widgets]], by how much room each type needs:
metriccards that follow each other share one row, side by side. They wrap onto more rows only when the dashboard is too narrow for them.chart,treemapandheatmaptake half a row: two in a row sit side by side (on a narrow dashboard they stack). One left without a partner takes the full row.table,timeline,gantt-chartandmaptake a row of their own.
Declare related widgets next to each other: three metric widgets in a row
form a strip, but a table between two of them splits the strip in two.
layout.columns is ignored
Older dashboards may still have a [layout] table with columns. It still
parses, but it no longer changes anything; you can delete it.
KPI card
metric
The one KPI widget: a title, the value large, and an optional line under it
with a description on the left and a trend on the right. Write one metric
per number; cards that follow each other share a row. A breakdown per entity
(per machine, per shift) is not a KPI card: put it in a table or a chart.
Prop
Type
[[widgets]]
type = "metric"
id = "utilization"
title = "Machine utilization"
value_key = "totals.utilization"
delta_key = "totals.utilization_delta"
format = "percent"
description = "4 machines, 480 min"Live preview, rendered with the real dashboard components:
When less is better, set good_direction = "down": a rise then turns red and
a fall green. Here late jobs went up by one, so the trend is red, and the
description line is read from the run output with description_key:
[[widgets]]
type = "metric"
id = "late-jobs"
title = "Late jobs"
value_key = "totals.late_jobs"
delta_key = "totals.late_jobs_delta"
good_direction = "down"
description_key = "totals.late_jobs_note"kpi-rail was removed
The kpi-rail widget, which grouped several tiles in one widget, is gone:
write one metric per number instead. A dashboard that still uses it keeps
showing its single-value tiles as separate metric cards, but
harumi dashboard validate reports it as
an unknown type and its per-entity rows_key lists are no longer drawn.
Table
table
A sortable data grid bound to an array of row objects.
Prop
Type
[[widgets]]
type = "table"
id = "jobs"
title = "Jobs"
rows_key = "jobs"
columns = [
{ key = "job", label = "Job" },
{ key = "priority", label = "Priority" },
{ key = "due", label = "Due (min)" },
{ key = "completed", label = "Completed (min)" },
{ key = "slack", label = "Slack (min)" },
{ key = "cost", label = "Cost" },
{ key = "status", label = "Status" },
]Live preview, rendered with the real dashboard components:
Charts
chart
One widget for every chart picture, picked by variant: line, bar,
area, scatter, pie or donut. Switching how data is drawn never means
switching widget, and the fonts, grid, grays and spacing are the product's own,
so every chart looks like it belongs.
Prop
Type
[[widgets]]
type = "chart"
id = "convergence"
title = "Solver convergence"
variant = "line"
data_key = "convergence"
x_key = "t"
series = [
{ key = "best", label = "Best makespan" },
{ key = "bound", label = "Lower bound" },
]Live preview, rendered with the real dashboard components:
The same type with variant = "bar", comparing categories instead of a trend:
[[widgets]]
type = "chart"
id = "machine-time"
title = "Machine time by state"
variant = "bar"
data_key = "machine_hours"
x_key = "machine"
series = [
{ key = "busy", label = "Busy" },
{ key = "setup", label = "Setup" },
{ key = "idle", label = "Idle" },
]Choosing a variant
| Your question | Use |
|---|---|
| How does it change over time? | line (or area to emphasize volume) |
| Which category is bigger? | bar, with orientation = "horizontal" when the labels are long |
| What share of the whole is each part? | pie or donut, only for about six slices or fewer |
| How do two numbers relate? | scatter |
A pie/donut shows only positive values, in source order unless you set
sort, and past eight slices the smallest are folded into one Other slice —
a pie of a dozen slivers says nothing, so prefer a sorted bar for that.
[[widgets]]
type = "chart"
id = "cost-share"
title = "Where the cost goes"
variant = "donut"
data_key = "cost_by_machine"
x_key = "machine"
series = [{ key = "cost", label = "Cost" }]
sort = "desc"
format = "currency"Several groups: group_key, stack and series[].mark
When each row is one category in one group (long format), group_key turns
every distinct group into its own colored series, so you don't list them by
hand. Add stack to pile them up:
[[widgets]]
type = "chart"
id = "cost-mix"
title = "Cost by machine and shift"
variant = "bar"
stack = "stacked"
data_key = "cost_by_shift"
x_key = "machine"
group_key = "shift"
series = [{ key = "cost", label = "Cost" }]
format = "currency"stack = "percent" normalizes each category to 100% (bars and areas only; a
line series makes it fall back to stacked). To mix pictures on one axis, give
a series its own mark:
[[widgets]]
type = "chart"
id = "output-vs-target"
title = "Output against target"
variant = "bar"
data_key = "daily"
x_key = "day"
series = [
{ key = "output", label = "Output" },
{ key = "target", label = "Target", mark = "line" },
]line-chart and bar-chart still work
Dashboards written with line-chart or bar-chart keep rendering: the
platform reads them as chart with variant = "line" or "bar". Write
chart in anything new.
Timelines & schedules
timeline
A resource-row timeline on a numeric time axis — the current, recommended
type for scheduling/job-shop solver results (one row per machine/resource,
one bar per task). Supersedes gantt-chart (below): interrupted work folds
into one item drawn with gaps, non-working spans render as background bands,
the axis is drag-to-zoom, and a [clock] (see Dashboard datasets, metrics,
and the clock) puts its play bar at the top of the
card and shows as a now-marker.
Prop
Type
[[widgets]]
type = "timeline"
id = "schedule"
title = "Machine schedule"
items_key = "schedule"
id_key = "op"
color_key = "job"
colors = { "Job E" = "red-600" }
regions_key = "breaks"
region_label_key = "label"
region_resource_key = "resource"
time_unit = "min"Live preview, rendered with the real dashboard components:
gantt-chart
A resource-row Gantt chart on a numeric time axis — the same shape timeline
covers, kept so dashboards written before timeline existed keep rendering.
Prefer timeline for a new dashboard.
Prop
Type
[[widgets]]
type = "gantt-chart"
id = "schedule"
title = "Machine schedule"
tasks_key = "schedule"
color_key = "job"
time_unit = "min"Live preview, rendered with the real dashboard components:
Breakdowns
treemap
A flat treemap — one rectangle per row, area proportional to value_key,
labeled from name_key. Good for a breakdown where relative size matters
more than exact values (e.g. cost by machine).
Prop
Type
[[widgets]]
type = "treemap"
id = "machine-load"
title = "Busy minutes by machine"
items_key = "machine_hours"
value_key = "busy"
name_key = "machine"
capacity_key = "totals.capacity"Live preview, rendered with the real dashboard components:
heatmap
An entity x time-bucket grid, shaded by intensity — one row per resource, one
column per bucket (e.g. hour of day). A (resource, bucket) pair with no
matching row renders as a distinct unavailable cell rather than a zero.
Prop
Type
[[widgets]]
type = "heatmap"
id = "hourly-cycles"
title = "Cycles per hour"
items_key = "hourly_cycles"
resource_key = "resource"
bucket_key = "bucket"
value_key = "value"
unit = "cycles"Live preview, rendered with the real dashboard components:
Maps
map
Geocoded points and/or routes on a real-world map — delivery stops, vehicle
routes, facility locations. Set points_key, routes_key, or both: a stop
layer plus the paths connecting them. A widget with neither renders an empty
state. Clicking a point toggles it in the dashboard's selection, joined by
id_key the same way timeline's is.
Prop
Type
[[widgets]]
type = "map"
id = "deliveries"
title = "Delivery run"
points_key = "stops"
label_key = "label"
id_key = "id"
color_key = "van"
routes_key = "routes"
route_color_key = "van"Live preview, rendered with the real dashboard components:
Tiles in production
OpenStreetMap's public tile servers are fine for trying the widget out, but
they're a shared, rate-limited service. For a dashboard with real traffic,
set tile_url to your own tile server or a paid provider, and attribution
to its credit.
Legacy chart types
line-chart and bar-chart predate chart, which folds both into
one type picked by variant. They are still fully supported — a dashboard
written before chart existed keeps rendering, because the platform reads each
as a chart — but they are no longer separate widgets, so prefer chart for
anything new.
| Legacy type | Same as |
|---|---|
type = "line-chart" | type = "chart" with variant = "line" |
type = "bar-chart" | type = "chart" with variant = "bar" |
They take the same data_key, x_key and series keys as chart; a
variant you set yourself wins over the one the old name implies.
Checking your widgets before you commit
harumi dashboard widgets prints this same
reference from your terminal (optionally filtered with --type), and
harumi dashboard validate checks a real dashboard.toml against it —
flagging an unknown type, a missing required key, or (given --against,
--run, or --latest) a dot-path that doesn't resolve against an actual
output.json.