# Record charts

A chart is a small declarative spec over one record type. The database groups
and aggregates, the web app draws the result, so a chart never ships single
records. Saved charts live in the Records area and can be placed on the space
dashboard.

Workflow: records.types.list for the record_type_id, records.types.describe
for the field keys, records.charts.data with an inline spec to check the
numbers, then records.charts.create. records.charts.update replaces the whole
spec and needs expected_revision.

Spec: {"title": "Revenue per month", "source": {"record_type_id": "<uuid>"},
"x": {"field": "issued", "bucket": "month"}, "measures": [{"op": "sum",
"field": "total", "label": "Revenue"}], "chart": {"type": "bar"}}.
source also takes view (key of a saved view as base filter), filter (the same
filter tree as records.rows.query), date_field plus last_days for a rolling
time window.

Dimensions: x and the optional series are {field, bucket, top}. field is a
text, number, boolean, date, datetime, single_select or multi_select key,
created_at or updated_at, a record relation key (groups by the title of the
linked record) or relation_key.field_key for one hop to a field of the linked
type. bucket is day, week, month, quarter, year or auto and only fits dates.
top keeps the N largest groups and folds the rest into "__other__". A
multi_select or many-to-many dimension counts a record once per value or link.

Measures: 1 to 4 of {op, field, label}; op is count (no field),
count_distinct, sum, avg, min or max; sum, avg, min and max need a number,
money or count field. Use either series or several measures, not both. Money
sums ignore the currency; the result carries the warning mixed_currency when
more than one currency was added up.

Chart types: kpi (no x; add "compare": "previous_period" with
source.date_field and last_days for the change against the window before),
line, area, bar (stacked, horizontal), donut (x and one measure) and heatmap
(x, series, one measure). Example by relation with a top list:
{"title": "Open invoices by supplier", "source": {"record_type_id": "<uuid>",
"filter": {"conditions": [{"field": "status", "op": "eq", "value": "open"}]}},
"x": {"field": "supplier", "top": 10}, "measures": [{"op": "sum", "field":
"total"}], "chart": {"type": "bar", "horizontal": true}, "sort": "value_desc"}.
KPI example: {"title": "New orders", "source": {"record_type_id": "<uuid>",
"date_field": "created_at", "last_days": 30}, "measures": [{"op": "count"}],
"chart": {"type": "kpi"}, "compare": "previous_period"}.

Result: columns [{key, role x|series|measure, label, type, options,
currency}] and rows as arrays in column order; numbers arrive as strings,
dates as ISO days. truncated is true when the row limit cut the result.
Agents get 200 rows by default, at most 2000 with limit.

Dashboard: records.charts.board_get returns sections of a three column grid,
records.charts.board_update replaces them: {"sections": [{"id": "sales",
"title": "Sales", "items": [{"record_chart_id": "<uuid>", "span": 2,
"height": "sm"}]}]} with span 1 to 3 and height sm or md.

Errors: invalid_chart_spec explains what does not fit the schema,
unknown_field means call records.types.describe, chart_source_missing means
the record type or a field of a saved chart is gone, chart_query_timeout means
narrow the filter or window or mark the grouped fields as indexed.
