graphein-mcp 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/chunk-N5KVZBDZ.js +338 -0
- package/dist/chunk-N5KVZBDZ.js.map +1 -0
- package/dist/index.d.ts +104 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +17 -0
- package/dist/server.js.map +1 -0
- package/package.json +69 -0
- package/resources/agent-guide.md +719 -0
- package/resources/chart-spec.schema.json +7828 -0
- package/resources/spec-reference.md +1382 -0
|
@@ -0,0 +1,1382 @@
|
|
|
1
|
+
# Graphein Spec Reference
|
|
2
|
+
|
|
3
|
+
Every Graphein chart is described by a single **`ChartSpec`** — a plain, JSON‑serializable
|
|
4
|
+
object. There are no functions, classes, or callbacks in a spec, so specs round‑trip
|
|
5
|
+
through `JSON.stringify` and are safe for a coding agent to emit, store, and replay.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { render } from 'graphein';
|
|
9
|
+
|
|
10
|
+
const instance = render('#chart', {
|
|
11
|
+
type: 'line',
|
|
12
|
+
data: [{ month: '2024-01', users: 4200 }, { month: '2024-02', users: 4650 }],
|
|
13
|
+
encoding: {
|
|
14
|
+
x: { field: 'month', type: 'temporal' },
|
|
15
|
+
y: { field: 'users', type: 'quantitative' },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **`type`** is the discriminator. One of:
|
|
21
|
+
`line`, `area`, `bar`, `scatter`, `pie`, `heatmap`, `kpi`, `table`, `matrix`.
|
|
22
|
+
- **`data`** is a tidy/row‑oriented array of records (`Array<Record<string, unknown>>`).
|
|
23
|
+
The same long‑format table feeds every chart; you select fields via `encoding`
|
|
24
|
+
(cartesian charts) or via explicit field lists (`table`, `matrix`).
|
|
25
|
+
|
|
26
|
+
Runnable JSON for every chart type lives in [`docs/examples/`](./examples).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Table of contents
|
|
31
|
+
|
|
32
|
+
- [Common fields (`BaseSpec`)](#common-fields-basespec)
|
|
33
|
+
- [Encoding & `FieldDef`](#encoding--fielddef)
|
|
34
|
+
- [Scales](#scales)
|
|
35
|
+
- [Transforms](#transforms)
|
|
36
|
+
- [Annotations (reference lines, bands, zones, points)](#annotations-reference-lines-bands-zones-points)
|
|
37
|
+
- [Self-explaining charts (summaries & auto-insights)](#self-explaining-charts-summaries--auto-insights)
|
|
38
|
+
- [Trendlines (regression overlays)](#trendlines-regression-overlays)
|
|
39
|
+
- [Faceting (small multiples)](#faceting-small-multiples)
|
|
40
|
+
- [Chart types](#chart-types)
|
|
41
|
+
- [line](#line) · [area](#area) · [bar](#bar) · [scatter](#scatter) · [combo](#combo) · [histogram](#histogram) · [pie](#pie)
|
|
42
|
+
- [heatmap](#heatmap) · [kpi](#kpi) · [table](#table) · [matrix](#matrix)
|
|
43
|
+
- [box](#box) · [funnel](#funnel) · [sankey](#sankey) · [choropleth](#choropleth)
|
|
44
|
+
- [treemap](#treemap) · [gauge](#gauge) · [bullet](#bullet) · [calendarHeatmap](#calendarheatmap)
|
|
45
|
+
- [waterfall](#waterfall) · [slope](#slope) · [dumbbell](#dumbbell)
|
|
46
|
+
- [Slicers](#slicers)
|
|
47
|
+
- [dropdown](#dropdown) · [search](#search) · [list](#list) · [range](#range) · [dateRange](#daterange)
|
|
48
|
+
- [Interactivity (selection · highlight · filter)](#interactivity-selection--highlight--filter)
|
|
49
|
+
- [Dashboards](#dashboards)
|
|
50
|
+
- [Conditional formatting](#conditional-formatting)
|
|
51
|
+
- [Themes](#themes)
|
|
52
|
+
- [Sketch (hand-drawn) mode](#sketchconfig)
|
|
53
|
+
- [Format mini‑language](#format-mini-language)
|
|
54
|
+
- [Enumerations](#enumerations)
|
|
55
|
+
- [Runtime API](#runtime-api)
|
|
56
|
+
- [Validation & linting](#validation--linting) · [Self-repairing specs](#self-repairing-specs) · [Render report](#render-report) · [Performance](#performance) · [Animation](#animation)
|
|
57
|
+
- [Accessibility](#accessibility)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Common fields (`BaseSpec`)
|
|
62
|
+
|
|
63
|
+
Shared by **all** chart types.
|
|
64
|
+
|
|
65
|
+
| Field | Type | Default | Notes |
|
|
66
|
+
| --- | --- | --- | --- |
|
|
67
|
+
| `data` | `Datum[]` | — | Row‑oriented records. Required for every chart/table. |
|
|
68
|
+
| `transform` | `Transform[]` | — | Declarative pipeline that reshapes `data` **before** charting (aggregate, bin, filter, fold, timeUnit). See [Transforms](#transforms). |
|
|
69
|
+
| `theme` | `'light' \| 'dark' \| ThemeOverride` | `'light'` | Theme name or a partial override (see [Themes](#themes)). |
|
|
70
|
+
| `dimensions` | `{ width?, height?, autoResize? }` | responsive | Omit `width`/`height` to fill the container and track resizes. |
|
|
71
|
+
| `title` | `string \| TitleConfig` | — | `string`, or `{ text, subtitle?, align? }`. |
|
|
72
|
+
| `description` | `string` | auto | Accessible alt text. Used verbatim as the chart's `aria-label`; auto‑synthesized from type/title/data when omitted (see [Accessibility](#accessibility)). |
|
|
73
|
+
| `legend` | `LegendConfig \| boolean` | auto | `false` hides it; `{ show?, position?, title? }`. `position`: `top \| right \| bottom \| left`. |
|
|
74
|
+
| `tooltip` | `TooltipConfig \| boolean` | `true` | `false` (or `{ show: false }`) disables hover tooltips. |
|
|
75
|
+
| `axes` | `{ x?: AxisConfig, y?: AxisConfig }` | auto | Per‑axis overrides (cartesian charts). |
|
|
76
|
+
| `animation` | `AnimationConfig \| boolean` | on | Brief entrance on first render. `false` disables; `{ enabled?, duration?, easing? }`. Honors `prefers-reduced-motion` (see [Animation](#animation)). |
|
|
77
|
+
| `padding` | `Partial<Insets>` | auto | Extra `{ top, right, bottom, left }` px around the plot. |
|
|
78
|
+
| `background` | `string` | theme bg | CSS color override for the chart surface. |
|
|
79
|
+
| `sketch` | `boolean \| SketchConfig` | `false` | Render with the hand‑drawn ("sketch") look — wobbly outlines, hachure fills, and a handwriting font (see [`SketchConfig`](#sketchconfig)). |
|
|
80
|
+
| `params` | `SelectionParam[]` | — | Named selections this visual **publishes** (click/brush/slicer). See [Interactivity](#interactivity-selection--highlight--filter). |
|
|
81
|
+
| `highlight` | `HighlightConfig \| HighlightConfig[]` | — | Emphasize rows matching a param; dim the rest. An array unions sources. |
|
|
82
|
+
| `filter` | `FilterClause[]` | — | Subset rows to those matching **every** clause (a `{ param }` or a literal predicate). |
|
|
83
|
+
|
|
84
|
+
### `TitleConfig`
|
|
85
|
+
|
|
86
|
+
```jsonc
|
|
87
|
+
{ "text": "Monthly active users", "subtitle": "Trailing 6 months", "align": "left" }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `AxisConfig`
|
|
91
|
+
|
|
92
|
+
| Field | Type | Notes |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| `show` | `boolean` | Hide the whole axis with `false`. |
|
|
95
|
+
| `title` | `string` | Axis title (overrides the field title). |
|
|
96
|
+
| `grid` | `boolean` | Toggle gridlines for this axis. |
|
|
97
|
+
| `ticks` | `number` | Approximate tick count (a hint, not exact). |
|
|
98
|
+
| `tickValues` | `number[]` | Explicit tick positions. |
|
|
99
|
+
| `format` | `string` | [Format hint](#format-mini-language) for tick labels. |
|
|
100
|
+
| `labels` | `boolean` | Show/hide tick labels. |
|
|
101
|
+
|
|
102
|
+
### `SketchConfig`
|
|
103
|
+
|
|
104
|
+
Turns on the alternate **hand‑drawn renderer** — a from‑scratch, rough.js‑style
|
|
105
|
+
engine baked into `graphein` (no extra dependency). Marks get wobbly multi‑pass
|
|
106
|
+
outlines and hachure fills; text switches to a bundled handwriting font (Patrick
|
|
107
|
+
Hand, SIL OFL). Works for **every** chart type, including the DOM charts
|
|
108
|
+
(`kpi`/`table`/`matrix`, which get the font plus subtle hand‑drawn chrome).
|
|
109
|
+
|
|
110
|
+
Set `sketch: true` for all defaults, or pass an object to tune the look:
|
|
111
|
+
|
|
112
|
+
| Field | Type | Default | Notes |
|
|
113
|
+
| --- | --- | --- | --- |
|
|
114
|
+
| `roughness` | `number` | `1` | Jitter amount. `0` ≈ clean; higher is wilder. |
|
|
115
|
+
| `bowing` | `number` | `1` | How much straight strokes bow/curve. |
|
|
116
|
+
| `fillStyle` | `'hachure' \| 'solid' \| 'cross-hatch'` | `'hachure'` | Shape fill style. |
|
|
117
|
+
| `hachureGap` | `number` | auto | Px between hachure lines (scales with stroke when omitted). |
|
|
118
|
+
| `hachureAngle` | `number` | `-41` | Hachure line angle, in degrees. |
|
|
119
|
+
| `strokeWidth` | `number` | `1` | Outline width multiplier. |
|
|
120
|
+
| `seed` | `number` | derived | Explicit PRNG seed. Omit for a stable seed derived from the spec (renders identically every time). |
|
|
121
|
+
| `font` | `boolean` | `true` | Apply the handwriting font. Set `false` to keep the theme font. |
|
|
122
|
+
|
|
123
|
+
```jsonc
|
|
124
|
+
// All defaults
|
|
125
|
+
{ "type": "bar", "data": [/* … */], "sketch": true }
|
|
126
|
+
|
|
127
|
+
// Tuned: bolder cross-hatch fill
|
|
128
|
+
{ "type": "pie", "data": [/* … */], "sketch": { "fillStyle": "cross-hatch", "roughness": 1.6 } }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rendering is **deterministic**: a given spec always produces the same drawing, so
|
|
132
|
+
sketch charts are safe to snapshot/screenshot‑test. Turning sketch off restores the
|
|
133
|
+
default crisp rendering path exactly (zero cost when unused — the font and engine are
|
|
134
|
+
code‑split and only loaded on the sketch path).
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Encoding & `FieldDef`
|
|
139
|
+
|
|
140
|
+
Cartesian charts (`line`, `area`, `bar`, `scatter`) and `pie`/`heatmap` map data
|
|
141
|
+
columns onto visual **channels** through `encoding`.
|
|
142
|
+
|
|
143
|
+
```jsonc
|
|
144
|
+
"encoding": {
|
|
145
|
+
"x": { "field": "month", "type": "temporal" },
|
|
146
|
+
"y": { "field": "users", "type": "quantitative", "format": ",d" },
|
|
147
|
+
"series": { "field": "region" }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Channels (`Encoding`)
|
|
152
|
+
|
|
153
|
+
| Channel | Used by | Purpose |
|
|
154
|
+
| --- | --- | --- |
|
|
155
|
+
| `x` | line, area, bar, scatter, heatmap | Horizontal position. |
|
|
156
|
+
| `y` | line, area, bar, scatter, heatmap | Vertical position. |
|
|
157
|
+
| `y2` | area | Upper bound for ranged/band marks. |
|
|
158
|
+
| `color` | heatmap, pie, choropleth | Continuous color (heatmap/choropleth) or slice color (pie). |
|
|
159
|
+
| `size` | scatter | Bubble radius. |
|
|
160
|
+
| `series` | line, area, bar, box | Splits data into multiple series (multi‑line, grouped/stacked bars, stacked areas, grouped boxes). |
|
|
161
|
+
| `theta` | pie | Angular measure (the slice value). |
|
|
162
|
+
| `source` | sankey | Link source node (one row per link). |
|
|
163
|
+
| `target` | sankey | Link target node. |
|
|
164
|
+
| `value` | sankey | Flow magnitude (ribbon/node thickness). |
|
|
165
|
+
| `key` | choropleth | Joins a data row to a map feature. |
|
|
166
|
+
| `label` | any | Text/label channel. |
|
|
167
|
+
|
|
168
|
+
### `FieldDef`
|
|
169
|
+
|
|
170
|
+
| Field | Type | Notes |
|
|
171
|
+
| --- | --- | --- |
|
|
172
|
+
| `field` | `string` | **Required.** Column name. Dotted paths (`a.b`) read nested values. |
|
|
173
|
+
| `type` | `FieldType` | `quantitative \| temporal \| ordinal \| nominal`. Inferred from the data when omitted. |
|
|
174
|
+
| `aggregate` | `AggOp` | Aggregation when grouping (e.g. `sum` of `sales`). See [enums](#enumerations). |
|
|
175
|
+
| `title` | `string` | Axis/legend title override. |
|
|
176
|
+
| `format` | `string` | [Format hint](#format-mini-language) for labels/tooltips. |
|
|
177
|
+
| `scale` | `ScaleConfig` | Per‑channel scale overrides (see below). |
|
|
178
|
+
|
|
179
|
+
> **Temporal fields:** JSON has no `Date` type, so pass dates as ISO‑ish strings
|
|
180
|
+
> (`"2024-01-15"`, `"2024-01"`) or epoch milliseconds. Graphein parses them for time
|
|
181
|
+
> axes, and `%` date formats coerce date strings for display in tables.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Scales
|
|
186
|
+
|
|
187
|
+
`FieldDef.scale` tunes how a channel maps data → pixels/color.
|
|
188
|
+
|
|
189
|
+
| Field | Type | Applies to | Notes |
|
|
190
|
+
| --- | --- | --- | --- |
|
|
191
|
+
| `type` | `'linear' \| 'log' \| 'time' \| 'band' \| 'point'` | x/y | Override the inferred scale type. |
|
|
192
|
+
| `domain` | `[number, number] \| string[]` | x/y/color | Explicit domain; numbers for continuous, strings for categorical. |
|
|
193
|
+
| `nice` | `boolean` | continuous | Round the domain to nice tick values. |
|
|
194
|
+
| `zero` | `boolean` | continuous | Force the domain to include 0. |
|
|
195
|
+
| `clamp` | `boolean` | continuous | Clamp out‑of‑domain values into range. |
|
|
196
|
+
| `padding` | `number` (0..1) | band/point | Inter‑category padding. |
|
|
197
|
+
| `base` | `number` | log | Log base. |
|
|
198
|
+
| `reverse` | `boolean` | any | Reverse the output range. |
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Transforms
|
|
203
|
+
|
|
204
|
+
`transform` is an ordered pipeline that reshapes the `data` array **inside the
|
|
205
|
+
spec**, before the chart model is built. It exists to kill the most common agent
|
|
206
|
+
mistake — *mis‑shaping the data array before charting*. Instead of pre‑aggregating
|
|
207
|
+
or pivoting rows in code, point a chart at raw rows and let a validatable transform
|
|
208
|
+
do the shaping. Encodings may reference columns the pipeline produces.
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"type": "bar",
|
|
213
|
+
"data": [ { "region": "West", "sales": 10 }, { "region": "West", "sales": 5 }, { "region": "East", "sales": 8 } ],
|
|
214
|
+
"transform": [
|
|
215
|
+
{ "filter": { "field": "sales", "gt": 0 } },
|
|
216
|
+
{ "aggregate": [ { "op": "sum", "field": "sales", "as": "sales" } ], "groupby": ["region"] }
|
|
217
|
+
],
|
|
218
|
+
"encoding": { "x": { "field": "region" }, "y": { "field": "sales" } }
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Steps run in array order. Each step carries **exactly one** operator key. The
|
|
223
|
+
pipeline is pure (it never mutates `data`) and is also exported standalone as
|
|
224
|
+
`applyTransforms(transforms, data)`. It runs **before** any selection cross‑filter.
|
|
225
|
+
|
|
226
|
+
| Operator | Shape | Notes |
|
|
227
|
+
| --- | --- | --- |
|
|
228
|
+
| `aggregate` | `{ aggregate: AggregateOp[], groupby?: string[] }` | Group rows and summarize. Omit `groupby` to collapse to one row. |
|
|
229
|
+
| `bin` | `{ bin: string, as: string \| [string,string], maxbins?, step?, extent?, nice? }` | Bucket a numeric field. `as` as a `[start,end]` pair captures both edges (drives the histogram). |
|
|
230
|
+
| `filter` | `{ filter: FilterPredicate }` | Keep rows matching a JSON predicate. |
|
|
231
|
+
| `fold` | `{ fold: string[], as?: [string,string] }` | Wide → long: gather columns into key/value rows (`as` defaults to `['key','value']`). |
|
|
232
|
+
| `timeUnit` | `{ timeUnit: TimeUnit, field: string, as: string }` | Truncate a timestamp to a calendar unit start (writes a `Date`). |
|
|
233
|
+
| `calculate` | `{ calculate: string, as: string }` | Derive a column from a safe expression (see [`calculate`](#calculate-expressions)). |
|
|
234
|
+
|
|
235
|
+
### `AggregateOp`
|
|
236
|
+
|
|
237
|
+
| Field | Type | Notes |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `op` | `AggOp` | `sum \| mean \| avg \| min \| max \| count \| countDistinct \| median \| first \| last`. |
|
|
240
|
+
| `field` | `string` | Source column. Omit only for `count`. |
|
|
241
|
+
| `as` | `string` | Output column. |
|
|
242
|
+
|
|
243
|
+
### `FilterPredicate`
|
|
244
|
+
|
|
245
|
+
A leaf predicate tests one `field`; `and` / `or` / `not` compose them. Comparisons
|
|
246
|
+
coerce numerically (numbers first, then dates), so temporal bounds work as ISO
|
|
247
|
+
strings.
|
|
248
|
+
|
|
249
|
+
- Leaf on a `field`: one of `equals`, `ne`, `oneOf`, `range: [lo,hi]`, `contains`,
|
|
250
|
+
`gt`, `gte`, `lt`, `lte`, or `valid: boolean` (drops null/NaN when `true`).
|
|
251
|
+
- Composite: `{ and: [...] }`, `{ or: [...] }`, `{ not: {...} }`.
|
|
252
|
+
|
|
253
|
+
```json
|
|
254
|
+
{ "filter": { "and": [
|
|
255
|
+
{ "field": "year", "gte": 2020 },
|
|
256
|
+
{ "field": "region", "oneOf": ["West", "East"] }
|
|
257
|
+
] } }
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### `TimeUnit`
|
|
261
|
+
|
|
262
|
+
`year · quarter · month · week · day · hour · minute · second`. Truncates to the
|
|
263
|
+
start of the unit so rows aggregate by period without manual date math:
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{ "timeUnit": "month", "field": "date", "as": "month" }
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `calculate` expressions
|
|
270
|
+
|
|
271
|
+
`{ "calculate": "<expr>", "as": "col" }` derives a column by evaluating `<expr>`
|
|
272
|
+
for each row. Bare identifiers reference columns; use `datum['my field']` for
|
|
273
|
+
names with spaces. The expression is parsed to an AST and evaluated with **no
|
|
274
|
+
`eval`/`Function`** and no access to globals — it is pure and deterministic.
|
|
275
|
+
|
|
276
|
+
```json
|
|
277
|
+
{ "calculate": "round(revenue / users, 2)", "as": "arpu" }
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Supported:
|
|
281
|
+
|
|
282
|
+
- **Operators:** `+ - * / %`, comparisons `< <= > >= == != === !==`, logical
|
|
283
|
+
`&& || !`, ternary `cond ? a : b`. `+` concatenates if either side is a string,
|
|
284
|
+
else adds numerically; comparisons coerce to numbers when both sides are numeric.
|
|
285
|
+
- **Literals:** numbers, `'single'`/`"double"` quoted strings, `true`, `false`, `null`.
|
|
286
|
+
- **Member access:** `datum.field` and `datum['field']` (prototype keys are blocked).
|
|
287
|
+
- **Functions:** `abs round floor ceil trunc sign sqrt exp log log10 log2 pow min
|
|
288
|
+
max number isFinite isNaN str lower upper trim length substring replace contains
|
|
289
|
+
startsWith endsWith concat coalesce if year month day hours minutes`. No
|
|
290
|
+
`now()`/`random()` — transforms stay deterministic.
|
|
291
|
+
|
|
292
|
+
> **Note:** `FieldDef.aggregate` (on `kpi`/`matrix`) still aggregates at encode
|
|
293
|
+
> time. For cartesian charts, prefer a `transform` `aggregate` step so there is one
|
|
294
|
+
> row per mark.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Annotations (reference lines, bands, zones, points)
|
|
299
|
+
|
|
300
|
+
Cartesian charts (`line`, `area`, `bar`, `scatter`, `box`) accept an optional
|
|
301
|
+
`annotations: Annotation[]` — reference lines, shaded bands, threshold zones, and labeled
|
|
302
|
+
points drawn over the plot. They're declarative data (no callbacks) and ship as overlay
|
|
303
|
+
marks plus an HTML label layer, so an agent can call out a target, SLA, safe range, or a
|
|
304
|
+
specific data point in one field.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
{
|
|
308
|
+
type: 'line',
|
|
309
|
+
data: rows,
|
|
310
|
+
encoding: { x: { field: 'month', type: 'temporal' }, y: { field: 'latency' } },
|
|
311
|
+
annotations: [
|
|
312
|
+
{ value: 200, label: 'SLA', color: '#ef4444' }, // horizontal rule on y
|
|
313
|
+
{ type: 'zone', from: 0, to: 100, label: 'Healthy' }, // shaded threshold band
|
|
314
|
+
{ axis: 'x', value: '2024-06', label: 'Launch' }, // vertical rule on x
|
|
315
|
+
{ type: 'point', x: '2024-04', y: 210, label: 'Spike' }, // labeled marker dot
|
|
316
|
+
],
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
| Field | Type | Default | Notes |
|
|
321
|
+
| --- | --- | --- | --- |
|
|
322
|
+
| `type` | `'line' \| 'band' \| 'zone' \| 'point'` | inferred | Omit to infer: `line` when `value` is set, `band` when `from`/`to` are set. `zone` is a semantic alias for a threshold band; `point` marks a single `(x, y)` coordinate. |
|
|
323
|
+
| `axis` | `'x' \| 'y'` | `'y'` | `y` draws a horizontal line / full‑width band; `x` draws a vertical line / full‑height band. |
|
|
324
|
+
| `value` | `number \| string \| Date` | — | Reference value for a `line` (matches the chosen axis). |
|
|
325
|
+
| `from`, `to` | `number \| string \| Date` | — | Span extents for a `band`/`zone`. |
|
|
326
|
+
| `x`, `y` | `number \| string \| Date` | — | Data coordinates for a `point` callout (matching the x/y axes). |
|
|
327
|
+
| `markerRadius` | `number` | `3.5` | Dot radius in pixels for a `point`. |
|
|
328
|
+
| `label` | `string` | — | Short text drawn beside the annotation. |
|
|
329
|
+
| `color` | `string` | muted theme color | Stroke (line) / fill (band) / dot (point) color. |
|
|
330
|
+
| `strokeWidth` | `number` | `1.5` | Line width in pixels. |
|
|
331
|
+
| `strokeDash` | `number[]` | dashed | Dash pattern; `[]` is solid. |
|
|
332
|
+
| `fillOpacity` | `number` | `0.12` | Band/zone fill opacity (0..1). |
|
|
333
|
+
| `labelPosition` | `'start' \| 'middle' \| 'end'` | `'end'` | Where the label anchors along a line/band. |
|
|
334
|
+
|
|
335
|
+
Validation rules: a `line` needs `value`; a `band`/`zone` needs both `from` and `to`;
|
|
336
|
+
`value` and `from`/`to` are mutually exclusive; values must be scalars (number, string,
|
|
337
|
+
or date). Annotations on a non‑cartesian chart produce a warning (they're ignored).
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Self-explaining charts (summaries & auto-insights)
|
|
342
|
+
|
|
343
|
+
Graphein derives the analytical labeling and prose an agent would otherwise have to reason
|
|
344
|
+
out — deterministically, with no LLM. Two surfaces share one pure analysis core
|
|
345
|
+
([`analyze/`](../packages/core/src/analyze)):
|
|
346
|
+
|
|
347
|
+
**`summarize(spec): string`** — a one-line natural-language summary of what the numbers say
|
|
348
|
+
(trend + net change, the largest/smallest category and its share, scatter correlation, a
|
|
349
|
+
value vs. its target). It doubles as alt-text and is attached to every render report:
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { summarize, render } from 'graphein';
|
|
353
|
+
|
|
354
|
+
summarize(spec); // "Users rose 46% from 4,200 to 6,150 between 2024-01 and 2024-06, peaking at 6,400 in 2024-03."
|
|
355
|
+
render('#app', spec).report().summary; // same string, on the RenderReport
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The summary also feeds the chart's `aria-description` automatically (unless you set an
|
|
359
|
+
explicit `description`), so the chart explains itself to screen readers.
|
|
360
|
+
|
|
361
|
+
**`insights: boolean | InsightOptions`** — opt a cartesian chart (`line`, `area`, `bar`)
|
|
362
|
+
into automatic on-chart callouts. The library finds the notable points and draws labeled
|
|
363
|
+
`point` annotations, so an agent never hardcodes where the peak is:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
{ type: 'line', data: rows, encoding: { /* … */ }, insights: true } // marks max ▲ + min ▼
|
|
367
|
+
{ type: 'bar', data: rows, encoding: { /* … */ }, insights: { outliers: true } }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
| `InsightOptions` | Type | Default | Notes |
|
|
371
|
+
| --- | --- | --- | --- |
|
|
372
|
+
| `max` | `boolean` | `true` | Mark the maximum point (or top category for a `bar`). |
|
|
373
|
+
| `min` | `boolean` | `true` | Mark the minimum point (or bottom category). |
|
|
374
|
+
| `outliers` | `boolean` | `false` | Mark points beyond the 1.5×IQR Tukey fences. |
|
|
375
|
+
|
|
376
|
+
`insights: true` is shorthand for `{ max: true, min: true }`. Multi-series charts are
|
|
377
|
+
skipped (markers on every series would clutter the plot) — use `insights` on a single
|
|
378
|
+
series. Auto-insight annotations merge with any explicit `annotations` you provide.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Trendlines (regression overlays)
|
|
383
|
+
|
|
384
|
+
`trendline: boolean | TrendlineConfig` — overlay a **linear line of best fit** on a
|
|
385
|
+
`scatter`, `line`, or `area` chart. The library computes the ordinary-least-squares
|
|
386
|
+
regression from the plotted rows, so an agent never derives slope/intercept coordinates by
|
|
387
|
+
hand — it just declares intent. A trendline needs a **continuous or temporal x-axis**;
|
|
388
|
+
asking for one on a categorical/band chart (e.g. `bar`) is a no-op warning.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
{ type: 'scatter', data: rows, encoding: { /* … */ }, trendline: true } // one fit
|
|
392
|
+
{ type: 'scatter', data: rows, encoding: { /* … */, color: { field: 'group' } },
|
|
393
|
+
trendline: { label: true } } // a fit per group + R²
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
When the chart splits into multiple series (a `color`/`series` channel), Graphein fits one
|
|
397
|
+
line **per group**, colored to match that group's points; otherwise it fits a single
|
|
398
|
+
overall line. The fit is stroked across each group's observed x-extent and clipped to the
|
|
399
|
+
plot.
|
|
400
|
+
|
|
401
|
+
| `TrendlineConfig` | Type | Default | Notes |
|
|
402
|
+
| --- | --- | --- | --- |
|
|
403
|
+
| `method` | `'linear'` | `'linear'` | Fit method (ordinary least squares). |
|
|
404
|
+
| `groupBy` | `boolean` | auto | Fit per series group. Defaults `true` when the chart has multiple series, else one overall fit. |
|
|
405
|
+
| `label` | `boolean` | `false` | Draw an `R²=…` label at the end of each line. |
|
|
406
|
+
| `color` | `string` | series color | Override the line color. |
|
|
407
|
+
| `strokeWidth` | `number` | `2` | Line width in px. |
|
|
408
|
+
| `strokeDash` | `number[]` | `[]` (solid) | Dash pattern. |
|
|
409
|
+
|
|
410
|
+
`trendline: true` is shorthand for `{ method: 'linear' }`. The overlay is purely derived —
|
|
411
|
+
no rows are added to your data, keeping the spec declarative and validatable.
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Faceting (small multiples)
|
|
416
|
+
|
|
417
|
+
`facet: FacetConfig` — split a chart into a **trellis grid of panels**, one per distinct
|
|
418
|
+
value of a field, all sharing **identical x/y/color scales** so the panels are directly
|
|
419
|
+
comparable. A single field reference yields a whole comparison grid, which is why faceting
|
|
420
|
+
is so agent-friendly. Supported on `line`, `area`, `bar`, and `scatter`.
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
{ type: 'line', data: rows,
|
|
424
|
+
encoding: { x: { field: 'month' }, y: { field: 'sales' } },
|
|
425
|
+
facet: { field: 'region', columns: 2 } } // one panel per region, 2 across
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
| `FacetConfig` | Type | Default | Notes |
|
|
429
|
+
| --- | --- | --- | --- |
|
|
430
|
+
| `field` | `string` | — (required) | The field whose distinct values become one panel each. |
|
|
431
|
+
| `columns` | `number` | ≈ √n | Number of grid columns (capped for readability). |
|
|
432
|
+
| `sort` | `'ascending' \| 'descending' \| 'none'` | `'ascending'` | Order the panels by their facet value. |
|
|
433
|
+
|
|
434
|
+
Every panel reuses **one shared set of scales** derived from the full dataset: the y-domain
|
|
435
|
+
spans the global extent (not each panel's local max), the x categories/domain are the union
|
|
436
|
+
across panels, and a series keeps its color in every panel — even when a panel's subset is
|
|
437
|
+
missing some categories or series. Multi-series facets render a single **shared legend** in
|
|
438
|
+
the header. Faceting is static (no per-panel interaction) in this release, and renders
|
|
439
|
+
identically headless via [`@graphein/node`](#render-report).
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Chart types
|
|
444
|
+
|
|
445
|
+
### line
|
|
446
|
+
|
|
447
|
+
Time/continuous series with optional multi‑series, markers, and area fill.
|
|
448
|
+
Large series are automatically [decimated (LTTB)](#performance) for fast redraws.
|
|
449
|
+
|
|
450
|
+
| Field | Type | Notes |
|
|
451
|
+
| --- | --- | --- |
|
|
452
|
+
| `encoding` | requires `x`, `y`; optional `series` | Multi‑series via `series`. |
|
|
453
|
+
| `curve` | `CurveType` | `linear \| monotone \| step \| stepBefore \| stepAfter \| catmullRom`. |
|
|
454
|
+
| `points` | `boolean` | Draw point markers. |
|
|
455
|
+
| `area` | `boolean` | Fill under the line. |
|
|
456
|
+
| `trendline` | `boolean \| TrendlineConfig` | Overlay a linear [line of best fit](#trendlines-regression-overlays). |
|
|
457
|
+
| `facet` | `FacetConfig` | Split into a [trellis grid of small multiples](#faceting-small-multiples). |
|
|
458
|
+
|
|
459
|
+
→ [`examples/line.json`](./examples/line.json)
|
|
460
|
+
|
|
461
|
+
### area
|
|
462
|
+
|
|
463
|
+
Filled series; stack multiple series into a band chart.
|
|
464
|
+
|
|
465
|
+
| Field | Type | Notes |
|
|
466
|
+
| --- | --- | --- |
|
|
467
|
+
| `encoding` | requires `x`, `y`; optional `series` | — |
|
|
468
|
+
| `curve` | `CurveType` | Same options as `line`. |
|
|
469
|
+
| `stack` | `boolean` | Stack series (totals). Non‑stacked areas overlap with translucency. |
|
|
470
|
+
| `trendline` | `boolean \| TrendlineConfig` | Overlay a linear [line of best fit](#trendlines-regression-overlays). |
|
|
471
|
+
| `facet` | `FacetConfig` | Split into a [trellis grid of small multiples](#faceting-small-multiples). |
|
|
472
|
+
|
|
473
|
+
→ [`examples/area-stacked.json`](./examples/area-stacked.json)
|
|
474
|
+
|
|
475
|
+
### bar
|
|
476
|
+
|
|
477
|
+
Columns/bars with grouped or stacked series and rounded corners.
|
|
478
|
+
|
|
479
|
+
| Field | Type | Notes |
|
|
480
|
+
| --- | --- | --- |
|
|
481
|
+
| `encoding` | requires `x`, `y`; optional `series` | — |
|
|
482
|
+
| `orientation` | `'vertical' \| 'horizontal'` | Default `vertical`. |
|
|
483
|
+
| `stack` | `boolean` | Stack series. |
|
|
484
|
+
| `group` | `boolean` | Side‑by‑side groups. Default when `series` is present and not stacked. |
|
|
485
|
+
| `cornerRadius` | `number` | Bar corner radius in px. |
|
|
486
|
+
| `facet` | `FacetConfig` | Split into a [trellis grid of small multiples](#faceting-small-multiples). |
|
|
487
|
+
|
|
488
|
+
→ [`examples/bar-grouped.json`](./examples/bar-grouped.json)
|
|
489
|
+
|
|
490
|
+
### scatter
|
|
491
|
+
|
|
492
|
+
Points/bubbles with optional size and color grouping. Hover focuses the nearest point.
|
|
493
|
+
|
|
494
|
+
| Field | Type | Notes |
|
|
495
|
+
| --- | --- | --- |
|
|
496
|
+
| `encoding` | requires `x`, `y`; optional `size`, `series` | `size` drives bubble radius; `series` colors groups. |
|
|
497
|
+
| `trendline` | `boolean \| TrendlineConfig` | Overlay a linear [line of best fit](#trendlines-regression-overlays) (per group when `series`/`color` is set). |
|
|
498
|
+
| `facet` | `FacetConfig` | Split into a [trellis grid of small multiples](#faceting-small-multiples). |
|
|
499
|
+
|
|
500
|
+
→ [`examples/scatter.json`](./examples/scatter.json)
|
|
501
|
+
|
|
502
|
+
### combo
|
|
503
|
+
|
|
504
|
+
Dual-axis / layered cartesian chart — the canonical BI **bar + line**. Each entry in
|
|
505
|
+
`layers` is a mark (`bar`/`line`/`area`/`scatter`) plotting its own `y` measure over the
|
|
506
|
+
shared `encoding.x`. A layer can read against the primary (`left`) or a secondary
|
|
507
|
+
(`right`) y-axis, each with an independent scale. Multiple `bar` layers group side-by-side;
|
|
508
|
+
line/area/point layers align to category centres. The legend shows one entry per layer.
|
|
509
|
+
|
|
510
|
+
| Field | Type | Notes |
|
|
511
|
+
| --- | --- | --- |
|
|
512
|
+
| `encoding` | requires `x` | The shared category/time axis. Bars force a categorical x. |
|
|
513
|
+
| `layers` | `ComboLayer[]` (≥1) | One mark + measure per layer (see below). |
|
|
514
|
+
|
|
515
|
+
**`ComboLayer`**
|
|
516
|
+
|
|
517
|
+
| Field | Type | Notes |
|
|
518
|
+
| --- | --- | --- |
|
|
519
|
+
| `mark` | `'line' \| 'bar' \| 'area' \| 'scatter'` | The mark drawn for this layer. |
|
|
520
|
+
| `encoding` | requires `y` | The measure (a `FieldDef`, with optional per-layer `format`) plotted on `y`. |
|
|
521
|
+
| `axis` | `'left' \| 'right'` | Which y-axis to measure against (default `left`). Add a `right` layer for dual-axis. |
|
|
522
|
+
| `curve` | `CurveType` | Interpolation for `line`/`area` layers. |
|
|
523
|
+
| `points` | `boolean` | Show point markers (line/area). |
|
|
524
|
+
| `area` | `boolean` | Fill under a `line` layer. |
|
|
525
|
+
| `cornerRadius` | `number` | Bar corner radius. |
|
|
526
|
+
| `name` | `string` | Legend label (defaults to the y field's title/name). |
|
|
527
|
+
| `color` | `string` | Override the layer colour (else the theme palette). |
|
|
528
|
+
|
|
529
|
+
> Dual-axis charts can imply correlations that aren't real — the linter emits an
|
|
530
|
+
> advisory `combo-dual-axis` (info) when both axes are used. Reserve a secondary axis for
|
|
531
|
+
> genuinely different units, and label both axes.
|
|
532
|
+
|
|
533
|
+
→ [`examples/combo-dual-axis.json`](./examples/combo-dual-axis.json)
|
|
534
|
+
|
|
535
|
+
### histogram
|
|
536
|
+
|
|
537
|
+
Distribution of a single quantitative field. Binning happens **inside** the chart
|
|
538
|
+
(reusing the `bin` transform), so you pass raw observations — no manual pre-binning. Bars
|
|
539
|
+
are gapless on a continuous x-axis; height is the per-bin count, or a probability density
|
|
540
|
+
when `density:true`.
|
|
541
|
+
|
|
542
|
+
| Field | Type | Notes |
|
|
543
|
+
| --- | --- | --- |
|
|
544
|
+
| `encoding` | requires `x` | `x` is the quantitative field to bin (carries the axis title/format). |
|
|
545
|
+
| `bin` | `HistogramBin` | `{ maxbins?, step?, extent?, nice? }` — default ~10 nice bins. |
|
|
546
|
+
| `density` | `boolean` | Normalize heights to a probability density (area sums to 1). Default `false` (raw counts). |
|
|
547
|
+
| `color` | `string` | Bar colour (defaults to the first theme palette colour). |
|
|
548
|
+
| `cornerRadius` | `number` | Bar corner radius. |
|
|
549
|
+
|
|
550
|
+
**`HistogramBin`**
|
|
551
|
+
|
|
552
|
+
| Field | Type | Notes |
|
|
553
|
+
| --- | --- | --- |
|
|
554
|
+
| `maxbins` | `number` | Target bin count (approximate — a "nice" step is chosen). Default `10`. |
|
|
555
|
+
| `step` | `number` | Explicit bin width (overrides `maxbins`). |
|
|
556
|
+
| `extent` | `[number, number]` | Restrict binning to `[min, max]`; values outside are dropped. |
|
|
557
|
+
| `nice` | `boolean` | Snap bin edges to round numbers (default `true`). |
|
|
558
|
+
|
|
559
|
+
→ [`examples/histogram.json`](./examples/histogram.json)
|
|
560
|
+
|
|
561
|
+
### pie
|
|
562
|
+
|
|
563
|
+
Pie or donut with value/percent labels and slice‑lift hover.
|
|
564
|
+
|
|
565
|
+
| Field | Type | Notes |
|
|
566
|
+
| --- | --- | --- |
|
|
567
|
+
| `encoding` | requires `theta`, `color` | `theta` = value, `color` = slice category. |
|
|
568
|
+
| `donut` | `boolean \| number` | `true` for a default donut, or a `0..1` inner‑radius ratio. |
|
|
569
|
+
| `labels` | `boolean \| PieLabels` | `true`/`false` toggles labels; pass a `PieLabels` object for callout control (default `true` ⇒ auto). |
|
|
570
|
+
|
|
571
|
+
**`PieLabels`** — auto inside/outside callouts with leader lines:
|
|
572
|
+
|
|
573
|
+
| Field | Type | Notes |
|
|
574
|
+
| --- | --- | --- |
|
|
575
|
+
| `show` | `boolean` | Master toggle (default `true`). |
|
|
576
|
+
| `placement` | `'inside' \| 'outside' \| 'auto'` | `auto` (default) keeps a label inside when the text fits, otherwise an outside callout with a leader line. |
|
|
577
|
+
| `content` | `'percent' \| 'value' \| 'category' \| 'category-percent' \| 'category-value'` | What each label says (default: `percent` inside, `category-percent` for outside callouts). |
|
|
578
|
+
| `minShare` | `number` | Hide labels for slices below this share of the total, `0..1` (default `0.01`). |
|
|
579
|
+
| `connector` | `'slice' \| 'muted'` | Leader-line colour for outside callouts (default `slice`). |
|
|
580
|
+
|
|
581
|
+
→ [`examples/pie-donut.json`](./examples/pie-donut.json) · [`examples/donut-callouts.json`](./examples/donut-callouts.json)
|
|
582
|
+
|
|
583
|
+
### heatmap
|
|
584
|
+
|
|
585
|
+
Dense category × category grid colored by a measure.
|
|
586
|
+
|
|
587
|
+
| Field | Type | Notes |
|
|
588
|
+
| --- | --- | --- |
|
|
589
|
+
| `encoding` | requires `x`, `y`, `color` | `x`/`y` are categories; `color` is the numeric measure. |
|
|
590
|
+
| `scheme` | `string` | Sequential ramp: `blues`, `teal`, `viridis`, `magma`, `greys`. |
|
|
591
|
+
|
|
592
|
+
→ [`examples/heatmap.json`](./examples/heatmap.json)
|
|
593
|
+
|
|
594
|
+
### kpi
|
|
595
|
+
|
|
596
|
+
A single stat card: big value, label, delta indicator, and inline sparkline.
|
|
597
|
+
|
|
598
|
+
| Field | Type | Notes |
|
|
599
|
+
| --- | --- | --- |
|
|
600
|
+
| `value` | `number \| { field, aggregate? }` | A literal, or a field aggregated over `data`. |
|
|
601
|
+
| `label` | `string` | Caption under/above the value. |
|
|
602
|
+
| `delta` | `number \| { field, aggregate? }` | Drives the up/down indicator (e.g. `0.124` → +12.4%). |
|
|
603
|
+
| `format` | `string` | [Format hint](#format-mini-language) for the value. |
|
|
604
|
+
| `sparkline` | `boolean \| { field }` | Inline trend from a numeric field. |
|
|
605
|
+
|
|
606
|
+
→ [`examples/kpi.json`](./examples/kpi.json)
|
|
607
|
+
|
|
608
|
+
### table
|
|
609
|
+
|
|
610
|
+
Virtualized, sortable data table with per‑column formatting, alignment, and
|
|
611
|
+
[conditional formatting](#conditional-formatting). Handles large row counts via
|
|
612
|
+
windowing.
|
|
613
|
+
|
|
614
|
+
| Field | Type | Notes |
|
|
615
|
+
| --- | --- | --- |
|
|
616
|
+
| `columns` | `TableColumn[]` | Explicit columns; inferred from `data` keys when omitted. |
|
|
617
|
+
| `sort` | `{ field, order? }` | `order`: `asc \| desc`. |
|
|
618
|
+
| `density` | `'comfortable' \| 'standard' \| 'compact'` | Row/header spacing preset. |
|
|
619
|
+
| `totals` | `boolean \| { label? }` | Adds a sticky footer row; measure columns default to `sum`. |
|
|
620
|
+
| `striped` | `boolean` | Zebra striping (off by default — flat aesthetic). |
|
|
621
|
+
| `stickyHeader` | `boolean` | Sticky header (default `true`). |
|
|
622
|
+
|
|
623
|
+
**`TableColumn`**
|
|
624
|
+
|
|
625
|
+
| Field | Type | Notes |
|
|
626
|
+
| --- | --- | --- |
|
|
627
|
+
| `field` | `string` | **Required.** Column key. |
|
|
628
|
+
| `title` | `string` | Header label (defaults to `field`). |
|
|
629
|
+
| `type` | `FieldType` | Affects default alignment/formatting. |
|
|
630
|
+
| `format` | `string` | [Format hint](#format-mini-language). |
|
|
631
|
+
| `align` | `'left' \| 'center' \| 'right'` | Cell/text alignment. |
|
|
632
|
+
| `width` | `number` | Fixed column width in px. |
|
|
633
|
+
| `conditionalFormat` | `ConditionalFormat` | In‑cell bar, color scale, icons, or rules. |
|
|
634
|
+
| `prefix` / `suffix` | `string` | Display text around formatted numeric values (for example `$`, `%`). |
|
|
635
|
+
| `negativeStyle` | `'sign' \| 'parens' \| 'red' \| 'parens-red'` | Negative number display. |
|
|
636
|
+
| `hidden` | `boolean` | Drops the column from rendering. |
|
|
637
|
+
| `sortable` | `boolean` | Set `false` to remove the sort button for that column. |
|
|
638
|
+
| `wrap` | `boolean` | Allows multi-line cell text. |
|
|
639
|
+
| `group` | `string` | Adds a top header band spanning consecutive columns with the same group. |
|
|
640
|
+
| `total` | `AggOp \| false` | Footer aggregation when `totals` is enabled. |
|
|
641
|
+
|
|
642
|
+
→ [`examples/table.json`](./examples/table.json)
|
|
643
|
+
|
|
644
|
+
### matrix
|
|
645
|
+
|
|
646
|
+
Pivot/cross‑tab: hierarchical row & column groups, aggregated measures, and
|
|
647
|
+
subtotals/grand totals — rendered through the table engine.
|
|
648
|
+
|
|
649
|
+
| Field | Type | Notes |
|
|
650
|
+
| --- | --- | --- |
|
|
651
|
+
| `rows` | `string[]` | **Required.** Row grouping fields (hierarchical, outer→inner). |
|
|
652
|
+
| `columns` | `string[]` | Column grouping fields (hierarchical). |
|
|
653
|
+
| `values` | `MatrixValueDef[]` | **Required.** Measures to aggregate. |
|
|
654
|
+
| `subtotals` | `boolean` | Group subtotals. |
|
|
655
|
+
| `grandTotals` | `boolean` | Overall totals row/column. |
|
|
656
|
+
| `density` | `'comfortable' \| 'standard' \| 'compact'` | Row/header spacing preset. |
|
|
657
|
+
| `columnSort` | `{ by:'value'\|'label', valueIndex?, order? }` | Sort leaf columns by label or aggregated measure. |
|
|
658
|
+
|
|
659
|
+
**`MatrixValueDef`**
|
|
660
|
+
|
|
661
|
+
| Field | Type | Notes |
|
|
662
|
+
| --- | --- | --- |
|
|
663
|
+
| `field` | `string` | **Required.** Measure column. |
|
|
664
|
+
| `op` | `AggOp` | **Required.** Aggregation (`sum`, `mean`, `count`, …). |
|
|
665
|
+
| `label` | `string` | Header label for the measure. |
|
|
666
|
+
| `format` | `string` | [Format hint](#format-mini-language). |
|
|
667
|
+
| `conditionalFormat` | `ConditionalFormat` | Per‑cell formatting. |
|
|
668
|
+
| `prefix` / `suffix` | `string` | Display text around formatted values. |
|
|
669
|
+
| `negativeStyle` | `TableColumn['negativeStyle']` | Negative number display. |
|
|
670
|
+
| `showAs` | `'value' \| 'percentOfRow' \| 'percentOfColumn' \| 'percentOfTotal'` | Display cell shares as percentages. Denominators are computed from leaf cells even when subtotals/grand totals are off. |
|
|
671
|
+
|
|
672
|
+
→ [`examples/matrix.json`](./examples/matrix.json)
|
|
673
|
+
|
|
674
|
+
### box
|
|
675
|
+
|
|
676
|
+
Box‑and‑whisker distributions: one box per category (and per `series`) showing
|
|
677
|
+
quartiles, median, whiskers, and outliers.
|
|
678
|
+
|
|
679
|
+
| Field | Type | Notes |
|
|
680
|
+
| --- | --- | --- |
|
|
681
|
+
| `encoding` | requires `x`, `y`; optional `series` | `x` is the category; `y` holds the **raw observations** (many rows per category) — Graphein computes the quartiles. `series` draws grouped boxes side‑by‑side. |
|
|
682
|
+
| `whisker` | `'tukey' \| 'minMax'` | `tukey` (default): whiskers reach the furthest points within 1.5×IQR of the quartiles; points beyond are outliers. `minMax`: whiskers span the full range (no outliers). |
|
|
683
|
+
| `outliers` | `boolean` | Draw outlier points beyond the whiskers (tukey only; default `true`). |
|
|
684
|
+
|
|
685
|
+
→ [`examples/box.json`](./examples/box.json)
|
|
686
|
+
|
|
687
|
+
### funnel
|
|
688
|
+
|
|
689
|
+
Conversion funnel: tapering stages stacked top‑to‑bottom, each labeled with its value
|
|
690
|
+
and the share retained. Values are **aggregated by stage** (in first‑seen order), so you
|
|
691
|
+
can pass raw rows.
|
|
692
|
+
|
|
693
|
+
| Field | Type | Notes |
|
|
694
|
+
| --- | --- | --- |
|
|
695
|
+
| `encoding` | requires `stage`, `value` | `stage` = the step (ordered as first seen); `value` = the measure (**summed** per stage). |
|
|
696
|
+
| `labels` | `boolean` | Show stage name, value, and percent inside the funnel (default `true`). |
|
|
697
|
+
| `percent` | `'first' \| 'previous'` | Per‑stage percentage basis: `first` (default) = share retained vs. the top of the funnel; `previous` = step conversion vs. the stage above. |
|
|
698
|
+
|
|
699
|
+
→ [`examples/funnel.json`](./examples/funnel.json)
|
|
700
|
+
|
|
701
|
+
### sankey
|
|
702
|
+
|
|
703
|
+
Flow diagram: nodes linked by value‑weighted ribbons. Each data row is one
|
|
704
|
+
**link**; nodes are derived from the distinct `source`/`target` values and laid
|
|
705
|
+
out in layers by longest path, with ribbons colored by their source.
|
|
706
|
+
|
|
707
|
+
| Field | Type | Notes |
|
|
708
|
+
| --- | --- | --- |
|
|
709
|
+
| `encoding` | requires `source`, `target`, `value` | One row per link. `value` sets ribbon/node thickness. |
|
|
710
|
+
| `nodeWidth` | `number` | Node block width in px (default `16`). |
|
|
711
|
+
| `nodePadding` | `number` | Vertical gap between stacked nodes in px (default `14`). |
|
|
712
|
+
| `nodeValues` | `boolean` | Show each node's total beside its label (default `true`). |
|
|
713
|
+
|
|
714
|
+
→ [`examples/sankey.json`](./examples/sankey.json)
|
|
715
|
+
|
|
716
|
+
### choropleth
|
|
717
|
+
|
|
718
|
+
Thematic map: GeoJSON regions filled by a sequential color scale, with a value
|
|
719
|
+
legend and per‑region hover. Data rows join to features by a key.
|
|
720
|
+
|
|
721
|
+
| Field | Type | Notes |
|
|
722
|
+
| --- | --- | --- |
|
|
723
|
+
| `geo` | `GeoFeatureCollection` | **Required.** GeoJSON `FeatureCollection` of `Polygon`/`MultiPolygon` features. |
|
|
724
|
+
| `encoding` | requires `key`, `color` | `key` joins each row to a feature; `color` is the numeric value driving the fill. |
|
|
725
|
+
| `featureId` | `string` | Where to read a feature's join id: a property name (e.g. `'name'` → `feature.properties.name`), or `'id'` for the top‑level `feature.id`. Defaults to `feature.id` → `properties.id` → `properties.name`. |
|
|
726
|
+
| `projection` | `MapProjection` | `mercator` (default), `equirectangular`, or `identity` (coordinates are already planar `[x, y]`). |
|
|
727
|
+
| `scheme` | `string` | Sequential ramp: `blues`, `teal`, `viridis`, `magma`, `greys`. |
|
|
728
|
+
|
|
729
|
+
The map auto‑fits its container. Antimeridian‑crossing geometry (e.g. Alaska's
|
|
730
|
+
Aleutians) is handled by choosing a central meridian from the widest longitude
|
|
731
|
+
gap. For composite layouts like Alaska/Hawaii insets, pre‑project the geometry to
|
|
732
|
+
planar coordinates and use `projection: 'identity'`.
|
|
733
|
+
|
|
734
|
+
→ [`examples/choropleth.json`](./examples/choropleth.json)
|
|
735
|
+
|
|
736
|
+
### treemap
|
|
737
|
+
|
|
738
|
+
Part‑to‑whole as nested rectangles sized by a measure. A **squarified** layout keeps
|
|
739
|
+
tiles close to square so areas stay visually comparable; input order is preserved for
|
|
740
|
+
deterministic output. An optional `group` field nests leaves under one level of parent
|
|
741
|
+
tiles, each with a header label.
|
|
742
|
+
|
|
743
|
+
| Field | Type | Notes |
|
|
744
|
+
| --- | --- | --- |
|
|
745
|
+
| `encoding` | requires `category`, `value` | `category` labels/identifies each leaf tile; `value` is the numeric field sizing its area (summed per leaf). |
|
|
746
|
+
| `encoding.group` | `FieldDef` | Optional parent grouping → nested parent tiles (one level deep), each with a header label. |
|
|
747
|
+
| `encoding.color` | `FieldDef` | Optional field driving tile color — numeric ⇒ sequential `scheme`, otherwise the categorical palette. Without it, tiles color by group (else category). |
|
|
748
|
+
| `scheme` | `string` | Sequential ramp name when `color` is numeric (default `teal`). |
|
|
749
|
+
| `labels` | `boolean` | Show the value beneath each tile label (default `true`). |
|
|
750
|
+
|
|
751
|
+
→ [`examples/treemap.json`](./examples/treemap.json)
|
|
752
|
+
|
|
753
|
+
### gauge
|
|
754
|
+
|
|
755
|
+
A radial dial showing one value against a `[min, max]` scale, with an optional `target`
|
|
756
|
+
tick and qualitative background `bands`. Like [`kpi`](#kpi), the value is a literal
|
|
757
|
+
**or** a field (optionally aggregated over `data`). Renders to canvas (headless‑safe).
|
|
758
|
+
|
|
759
|
+
| Field | Type | Notes |
|
|
760
|
+
| --- | --- | --- |
|
|
761
|
+
| `value` | `ValueRef` | The measured value: a literal number, or `{ field, aggregate? }` summarized over `data`. |
|
|
762
|
+
| `max` | `number` | **Required.** Scale end (full‑scale). |
|
|
763
|
+
| `min` | `number` | Scale start (default `0`). |
|
|
764
|
+
| `target` | `ValueRef` | Optional threshold drawn as a needle/tick. |
|
|
765
|
+
| `bands` | `{ to: number, color? }[]` | Qualitative arc bands, each filling the scale up to `to`. |
|
|
766
|
+
| `label` | `string` | Caption under the value (defaults to the title or value field). |
|
|
767
|
+
| `format` | `string` | Number format for the value + scale ticks (e.g. `,.0f`, `.0%`). |
|
|
768
|
+
|
|
769
|
+
→ [`examples/gauge.json`](./examples/gauge.json)
|
|
770
|
+
|
|
771
|
+
### bullet
|
|
772
|
+
|
|
773
|
+
A compact linear KPI‑vs‑target: a measure bar over qualitative `ranges` (poor/ok/good
|
|
774
|
+
background bands) with a `target` comparative tick. Value and target are literals or
|
|
775
|
+
fields (optionally aggregated) — ideal as a dashboard tile. Renders to canvas
|
|
776
|
+
(headless‑safe).
|
|
777
|
+
|
|
778
|
+
| Field | Type | Notes |
|
|
779
|
+
| --- | --- | --- |
|
|
780
|
+
| `value` | `ValueRef` | The featured measure (literal or `{ field, aggregate? }`). |
|
|
781
|
+
| `target` | `ValueRef` | Comparative marker drawn as a vertical tick. |
|
|
782
|
+
| `ranges` | `number[]` | Qualitative range boundaries on the scale (e.g. `[600000, 800000, 1000000]`). |
|
|
783
|
+
| `min` / `max` | `number` | Scale bounds (`min` default `0`; `max` derived from value/target/ranges when omitted). |
|
|
784
|
+
| `label` | `string` | Caption to the left of the bar (defaults to the title or value field). |
|
|
785
|
+
| `format` | `string` | Number format for the value + axis ticks. |
|
|
786
|
+
|
|
787
|
+
→ [`examples/bullet.json`](./examples/bullet.json)
|
|
788
|
+
|
|
789
|
+
### calendarHeatmap
|
|
790
|
+
|
|
791
|
+
A GitHub‑contributions‑style grid: one cell per day colored by a value on a sequential
|
|
792
|
+
scale, with weekday rows and month labels. Pass tidy `{ date, value }` rows — dates may
|
|
793
|
+
be `Date` objects or ISO strings (both coerce for the `temporal` field).
|
|
794
|
+
|
|
795
|
+
| Field | Type | Notes |
|
|
796
|
+
| --- | --- | --- |
|
|
797
|
+
| `encoding` | requires `date`, `color` | `date` → one cell per day; `color` is the numeric value driving the cell fill. |
|
|
798
|
+
| `scheme` | `string` | Sequential ramp name for the value scale (default `teal`). |
|
|
799
|
+
|
|
800
|
+
→ [`examples/calendar-heatmap.json`](./examples/calendar-heatmap.json)
|
|
801
|
+
|
|
802
|
+
### waterfall
|
|
803
|
+
|
|
804
|
+
A cash‑flow / bridge chart: floating bars walk a running total across ordered `stage`s,
|
|
805
|
+
where each `value` is a **signed delta** (positive rises, negative falls). Mark a stage in
|
|
806
|
+
`totals` (or append one with `showTotal`) to draw an absolute bar from the baseline — it
|
|
807
|
+
shows the running total but does **not** advance it. Dashed connectors join each step to
|
|
808
|
+
the next. Renders to canvas (headless‑safe).
|
|
809
|
+
|
|
810
|
+
| Field | Type | Notes |
|
|
811
|
+
| --- | --- | --- |
|
|
812
|
+
| `encoding` | requires `stage`, `value` | `stage` → one bar per row (in data order); `value` is the **signed** change at that stage. |
|
|
813
|
+
| `totals` | `string[]` | Stage labels to draw as absolute running‑total bars from zero. |
|
|
814
|
+
| `showTotal` | `boolean` | Append a final bar summing every step (default `false`). |
|
|
815
|
+
| `totalLabel` | `string` | Label for the appended total bar (default `'Total'`). |
|
|
816
|
+
| `labels` | `boolean` | Show per‑bar value labels (default `true`). |
|
|
817
|
+
| `cornerRadius` | `number` | Bar corner radius in px (default `2`). |
|
|
818
|
+
| `increaseColor` / `decreaseColor` / `totalColor` | `string` | Override the up / down / total colors (default theme positive / negative / accent). |
|
|
819
|
+
|
|
820
|
+
→ [`examples/waterfall.json`](./examples/waterfall.json)
|
|
821
|
+
|
|
822
|
+
### slope
|
|
823
|
+
|
|
824
|
+
A slope graph — a minimal before/after chart: each `series` is a line joining its `y`
|
|
825
|
+
value across two (or a few) ordinal `x` positions, with **direct end labels** instead of a
|
|
826
|
+
legend, so change‑in‑rank and rise/fall read at a glance. Pass tidy `{ x, y, series }`
|
|
827
|
+
rows (one per series per x position). Renders to canvas (headless‑safe).
|
|
828
|
+
|
|
829
|
+
| Field | Type | Notes |
|
|
830
|
+
| --- | --- | --- |
|
|
831
|
+
| `encoding` | requires `x`, `y`, `series` | `x` → the ordinal positions (typically two); `y` is the numeric value; `series` is one line each. |
|
|
832
|
+
| `colorByChange` | `boolean` | Color each line green/red by its net rise/fall instead of by series. |
|
|
833
|
+
| `labels` | `boolean` | Direct labels (series name + value) at each line's ends (default `true`). |
|
|
834
|
+
| `format` | `string` | Number format for the value labels and y‑axis. |
|
|
835
|
+
|
|
836
|
+
→ [`examples/slope.json`](./examples/slope.json)
|
|
837
|
+
|
|
838
|
+
### dumbbell
|
|
839
|
+
|
|
840
|
+
A dumbbell / connected‑dot plot: for each `category`, a dot per `group` placed on a shared
|
|
841
|
+
**horizontal** value axis and joined by a connector, so the gap between groups (before vs.
|
|
842
|
+
after, male vs. female) reads directly. Categories run down the y‑axis. Pass tidy
|
|
843
|
+
`{ category, value, group }` rows. Renders to canvas (headless‑safe).
|
|
844
|
+
|
|
845
|
+
| Field | Type | Notes |
|
|
846
|
+
| --- | --- | --- |
|
|
847
|
+
| `encoding` | requires `category`, `value`, `group` | `category` → one band down the y‑axis; `value` → dot position on the x‑axis; `group` → one dot each (2+ levels), connected. |
|
|
848
|
+
| `sort` | `'ascending' \| 'descending' \| 'gap'` | Order the category rows (default: data order). `gap` sorts by dot spread. |
|
|
849
|
+
| `labels` | `boolean` | Value labels beside the dots (default `false`). |
|
|
850
|
+
| `format` | `string` | Number format for the value labels and x‑axis. |
|
|
851
|
+
|
|
852
|
+
→ [`examples/dumbbell.json`](./examples/dumbbell.json)
|
|
853
|
+
|
|
854
|
+
---
|
|
855
|
+
|
|
856
|
+
## Slicers
|
|
857
|
+
|
|
858
|
+
Interactive controls that **publish a selection** instead of plotting marks. A slicer
|
|
859
|
+
reads one `field` and writes to a `param` (defaulting to the field name), so it
|
|
860
|
+
auto‑connects to any visual that filters/highlights on that param. Options and bounds
|
|
861
|
+
derive from the **unfiltered** data, so a slicer never hides its own choices. Slicers
|
|
862
|
+
render standalone (they're ordinary DOM widgets) and slot into a [dashboard](#dashboards)
|
|
863
|
+
like any other view. They share the [common slicer fields](#common-slicer-fields) below
|
|
864
|
+
plus their own.
|
|
865
|
+
|
|
866
|
+
#### Common slicer fields
|
|
867
|
+
|
|
868
|
+
| Field | Type | Default | Notes |
|
|
869
|
+
| --- | --- | --- | --- |
|
|
870
|
+
| `field` | `string` | **required** | Column the slicer reads options/bounds from and filters on. |
|
|
871
|
+
| `param` | `string` | `field` | Param name to publish to (the wire other visuals consume). |
|
|
872
|
+
| `label` | `string` | `title`/`field` | Label shown above the control. |
|
|
873
|
+
| `as` | `'filter' \| 'highlight'` | `'filter'` | How consumers react. |
|
|
874
|
+
|
|
875
|
+
Plus all [`BaseSpec`](#common-fields-basespec) chrome (`theme`, `title`, `sketch`, …).
|
|
876
|
+
|
|
877
|
+
### dropdown
|
|
878
|
+
|
|
879
|
+
Choose one (single) or several (multi) of a field's distinct values. Emits a `set`.
|
|
880
|
+
|
|
881
|
+
| Field | Type | Notes |
|
|
882
|
+
| --- | --- | --- |
|
|
883
|
+
| `multiple` | `boolean` | Allow multiple values. Default `false` (single‑select). |
|
|
884
|
+
| `placeholder` | `string` | Shown when nothing is selected. |
|
|
885
|
+
|
|
886
|
+
### search
|
|
887
|
+
|
|
888
|
+
A debounced, case‑insensitive substring filter over a text field. Emits a `text`
|
|
889
|
+
selection (`contains`).
|
|
890
|
+
|
|
891
|
+
| Field | Type | Notes |
|
|
892
|
+
| --- | --- | --- |
|
|
893
|
+
| `placeholder` | `string` | Input placeholder. |
|
|
894
|
+
| `debounce` | `number` | Ms before publishing the query (default `200`). |
|
|
895
|
+
|
|
896
|
+
### list
|
|
897
|
+
|
|
898
|
+
A scrollable checkbox list of a field's distinct values (multi‑select). Emits a `set`.
|
|
899
|
+
|
|
900
|
+
| Field | Type | Notes |
|
|
901
|
+
| --- | --- | --- |
|
|
902
|
+
| `selectAll` | `boolean` | Show the "Select all" / "Clear" row (default `true`). |
|
|
903
|
+
| `searchThreshold` | `number` | Show a search‑within box once options exceed this (default `8`). |
|
|
904
|
+
|
|
905
|
+
### range
|
|
906
|
+
|
|
907
|
+
A numeric min/max range over a quantitative field (dual‑thumb slider). Emits a `range`.
|
|
908
|
+
|
|
909
|
+
| Field | Type | Notes |
|
|
910
|
+
| --- | --- | --- |
|
|
911
|
+
| `min` / `max` | `number` | Bounds; default to the data min/max of `field`. |
|
|
912
|
+
| `step` | `number` | Thumb step; defaults to a fraction of the range. |
|
|
913
|
+
| `format` | `string` | Number [format hint](#format-mini-language) for value labels. |
|
|
914
|
+
|
|
915
|
+
### dateRange
|
|
916
|
+
|
|
917
|
+
A temporal min/max range over a date field, with relative presets. Emits a `range`.
|
|
918
|
+
|
|
919
|
+
| Field | Type | Notes |
|
|
920
|
+
| --- | --- | --- |
|
|
921
|
+
| `presets` | `boolean` | Show relative presets (last 7/30/90 days, all). Default `true`. |
|
|
922
|
+
| `format` | `string` | Date [format hint](#format-mini-language) for value labels. |
|
|
923
|
+
|
|
924
|
+
→ [`examples/slicer-dropdown.json`](./examples/slicer-dropdown.json)
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## Interactivity (selection · highlight · filter)
|
|
929
|
+
|
|
930
|
+
The unit of interactivity is a **selection** — a named, JSON‑serializable value a
|
|
931
|
+
visual *publishes* and others *consume*. Selections are plain data (no callbacks), so
|
|
932
|
+
specs still round‑trip through `JSON.stringify`. Inspired by Vega‑Lite params, pared
|
|
933
|
+
down to point/interval definitions and four resolved value shapes.
|
|
934
|
+
|
|
935
|
+
**Publish — `params`.** A `SelectionParam` names a selection a visual writes when the
|
|
936
|
+
user clicks a mark, brushes, or changes a slicer:
|
|
937
|
+
|
|
938
|
+
```jsonc
|
|
939
|
+
"params": [{
|
|
940
|
+
"name": "pick",
|
|
941
|
+
"select": {
|
|
942
|
+
"type": "point", // 'point' (discrete picks) | 'interval' (a range)
|
|
943
|
+
"on": "click", // 'click' (default) | 'hover'
|
|
944
|
+
"fields": ["region"], // identity fields; default = the chart's key channel
|
|
945
|
+
"toggle": true, // click to add/remove (default true for click)
|
|
946
|
+
"empty": "all" // empty selection ⇒ match all (default) | none
|
|
947
|
+
}
|
|
948
|
+
}]
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
**Consume — `highlight` & `filter`.** `highlight: { param }` emphasizes rows matching a
|
|
952
|
+
param and dims the rest (per‑mark, ~22% dim alpha); an array unions several sources.
|
|
953
|
+
`filter` subsets rows to those matching **every** clause (logical AND). Each clause is a
|
|
954
|
+
named param or a literal predicate:
|
|
955
|
+
|
|
956
|
+
| Clause | Shape | Meaning |
|
|
957
|
+
| --- | --- | --- |
|
|
958
|
+
| param | `{ "param": "region" }` | Match the param's current value. |
|
|
959
|
+
| equals | `{ "field": "region", "equals": "West" }` | `field === value`. |
|
|
960
|
+
| oneOf | `{ "field": "region", "oneOf": ["West","East"] }` | Membership. |
|
|
961
|
+
| range | `{ "field": "sales", "range": [100, 500] }` | Inclusive numeric/temporal span. |
|
|
962
|
+
| contains | `{ "field": "product", "contains": "wid" }` | Case‑insensitive substring. |
|
|
963
|
+
|
|
964
|
+
**Resolved value shapes** (`SelectionValue` — what `getSelection` returns and
|
|
965
|
+
`setSelection` accepts):
|
|
966
|
+
|
|
967
|
+
| `kind` | Shape | Emitted by |
|
|
968
|
+
| --- | --- | --- |
|
|
969
|
+
| `point` | `{ kind, fields, tuples }` | clicking marks |
|
|
970
|
+
| `set` | `{ kind, field, values }` | `dropdown`, `list` |
|
|
971
|
+
| `range` | `{ kind, field, min?, max? }` | `range`, `dateRange`, brushing |
|
|
972
|
+
| `text` | `{ kind, field, query }` | `search` |
|
|
973
|
+
|
|
974
|
+
To link **independently rendered** charts, pass a shared store:
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
import { render, createSelectionStore } from 'graphein';
|
|
978
|
+
const store = createSelectionStore();
|
|
979
|
+
render('#a', specA, { store }); // a publishes `params`
|
|
980
|
+
render('#b', specB, { store }); // b consumes via `highlight`/`filter`
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## Dashboards
|
|
986
|
+
|
|
987
|
+
A `dashboard` is the single‑JSON, agent‑facing layer that composes charts and slicers
|
|
988
|
+
into one cross‑interacting page. It owns a shared dataset and selection store, lays
|
|
989
|
+
views out on a responsive grid, and **auto‑wires** cross‑interaction. Validated by
|
|
990
|
+
`validateSpec`; rendered with `renderDashboard`.
|
|
991
|
+
|
|
992
|
+
| Field | Type | Default | Notes |
|
|
993
|
+
| --- | --- | --- | --- |
|
|
994
|
+
| `type` | `'dashboard'` | — | Discriminator. |
|
|
995
|
+
| `data` | `Datum[]` | — | Shared dataset; views without their own `data` inherit it. |
|
|
996
|
+
| `views` | `DashboardView[]` | **required** | The placed charts/slicers. |
|
|
997
|
+
| `layout` | `DashboardLayout` | see below | Grid sizing + responsive behavior. |
|
|
998
|
+
| `interactions` | `'auto' \| 'none' \| InteractionLink[]` | `'auto'` | Cross‑interaction policy. |
|
|
999
|
+
| `params` | `SelectionParam[]` | — | Dashboard‑level selections (e.g. seeded initial values). |
|
|
1000
|
+
| `subtitle` | `string` | — | Muted line shown under the title in the page header. |
|
|
1001
|
+
| `theme` `title` `background` `dimensions` | — | — | Page chrome; `theme` cascades to every view. |
|
|
1002
|
+
|
|
1003
|
+
**`DashboardLayout`** — `{ cols?, rowHeight?, gap?, breakpoints?, navigators?, sections?, preset?, maxWidth?, density?, padding? }`:
|
|
1004
|
+
|
|
1005
|
+
| Field | Type | Default | Notes |
|
|
1006
|
+
| --- | --- | --- | --- |
|
|
1007
|
+
| `cols` | `number` | `12` | Grid columns at full width. |
|
|
1008
|
+
| `rowHeight` | `number` | `96` | Height of one grid row (px). |
|
|
1009
|
+
| `gap` | `number` | `14` | Gap between cells (px). |
|
|
1010
|
+
| `breakpoints` | `{ maxWidth, cols }[]` | `[{600,1},{960,6}]` | Responsive column counts: when narrower than a breakpoint's `maxWidth`, the grid switches to its `cols` and tiles reflow (DataZen‑style). Smallest match wins. |
|
|
1011
|
+
| `navigators` | `'top' \| 'inline'` | `'top'` | `top`: compact slicers (`dropdown`/`search`/`range`/`dateRange`) form a navigator strip above the grid (a BI filter bar); `inline`: every slicer is placed in the grid like a chart. |
|
|
1012
|
+
| `sections` | `DashboardSection[]` | — | Stack multiple section grids with header bands; views omitted from all sections render in an implicit trailing section. |
|
|
1013
|
+
| `preset` | `'auto' \| 'kpi-first' \| 'sidebar'` | `'auto'` | Auto-arrange unplaced views. Explicit `x`/`y`/`w`/`h` placement always wins. |
|
|
1014
|
+
| `maxWidth` | `number` | — | Constrain and center the dashboard page. |
|
|
1015
|
+
| `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | Applies spacing/row-height presets before explicit `gap`/`rowHeight` overrides. |
|
|
1016
|
+
| `padding` | `number` | derived from `gap` | Page padding in px. |
|
|
1017
|
+
|
|
1018
|
+
**`DashboardSection`**:
|
|
1019
|
+
|
|
1020
|
+
| Field | Type | Default | Notes |
|
|
1021
|
+
| --- | --- | --- | --- |
|
|
1022
|
+
| `id` | `string` | — | Optional stable id for the section. |
|
|
1023
|
+
| `title` / `subtitle` | `string` | — | Header band copy above the section grid. |
|
|
1024
|
+
| `views` | `string[]` | **required** | View ids in this section; each id may appear in only one section. |
|
|
1025
|
+
| `cols` | `number` | layout `cols` | Section-specific columns. |
|
|
1026
|
+
| `rowHeight` | `number` | layout `rowHeight` | Section-specific row height. |
|
|
1027
|
+
| `background` | `string` | transparent | Header band tint. |
|
|
1028
|
+
| `collapsed` | `boolean` | `false` | Start with the body hidden behind a clickable header. |
|
|
1029
|
+
|
|
1030
|
+
**`DashboardView`** — `{ id, spec, x?, y?, w?, h?, title?, subtitle?, frame?, background?, accent?, padding?, responsive? }`.
|
|
1031
|
+
`id` is unique within the dashboard (used for layout + link references). `spec` is any
|
|
1032
|
+
chart or slicer spec (inherits the dashboard's `data` when it has none). `x`/`y` are
|
|
1033
|
+
1‑based grid placement (omit to auto‑flow); `w`/`h` are column/row spans (sensible
|
|
1034
|
+
per‑type defaults).
|
|
1035
|
+
|
|
1036
|
+
| Field | Type | Default | Notes |
|
|
1037
|
+
| --- | --- | --- | --- |
|
|
1038
|
+
| `title` / `subtitle` | `string` | — | Card header drawn by the dashboard. When present, Graphein suppresses the inner chart `title` to avoid duplicate headings. |
|
|
1039
|
+
| `frame` | `boolean` | `true` | Draw the framed card chrome. Set `false` for frameless tiles. |
|
|
1040
|
+
| `background` | `string` | theme surface | Card background override. |
|
|
1041
|
+
| `accent` | `string` | — | Solid left accent bar color. |
|
|
1042
|
+
| `padding` | `'none' \| 'standard'` | `'standard'` | Use `'none'` for flush tables/maps. |
|
|
1043
|
+
| `responsive` | `{ maxWidth, w?, h?, hidden? }[]` | — | Per-view span overrides at section/dashboard widths; smallest matching `maxWidth` wins. |
|
|
1044
|
+
|
|
1045
|
+
**`interactions: 'auto'`** (Power BI semantics):
|
|
1046
|
+
|
|
1047
|
+
- **Slicers filter the whole page** — every non‑slicer view whose data contains the
|
|
1048
|
+
field is subset by the slicer (a KPI, a table, a chart that inherits the dashboard
|
|
1049
|
+
data). A filter on a column a view's data doesn't contain is **ignored for that view**,
|
|
1050
|
+
not blanked — so a pre‑aggregated view only reacts to the dimensions it carries.
|
|
1051
|
+
- **Chart clicks cross‑highlight** — clicking a mark emphasizes the matching subset in
|
|
1052
|
+
views that encode the same field, and always self‑highlights.
|
|
1053
|
+
|
|
1054
|
+
**Explicit links** replace auto‑wiring with an array of `InteractionLink`:
|
|
1055
|
+
|
|
1056
|
+
| Field | Type | Notes |
|
|
1057
|
+
| --- | --- | --- |
|
|
1058
|
+
| `source` | `string` | View id whose selection drives the interaction. |
|
|
1059
|
+
| `target` | `string \| string[] \| '*'` | Target view id(s), or `'*'` for every other view. |
|
|
1060
|
+
| `as` | `'highlight' \| 'filter' \| 'none'` | How targets react (defaults by source type). |
|
|
1061
|
+
| `fields` | `string[]` | Identity fields to match on; defaults to the source's key field(s). |
|
|
1062
|
+
|
|
1063
|
+
```jsonc
|
|
1064
|
+
"interactions": [
|
|
1065
|
+
{ "source": "region", "target": "*", "as": "filter" },
|
|
1066
|
+
{ "source": "byRegion", "target": ["trend"], "as": "highlight", "fields": ["region"] }
|
|
1067
|
+
]
|
|
1068
|
+
```
|
|
1069
|
+
|
|
1070
|
+
`renderDashboard(target, spec, options?)` returns a **`DashboardInstance`**:
|
|
1071
|
+
`update(next)`, `resize()`, `destroy()`, `spec`, plus the
|
|
1072
|
+
[selection API](#runtime-api) (`getSelection` / `setSelection` / `clearSelection` /
|
|
1073
|
+
`on('selectionchange', …)` / `off`) and `views` (the wired view specs).
|
|
1074
|
+
|
|
1075
|
+
→ [`examples/dashboard.json`](./examples/dashboard.json)
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
## Conditional formatting
|
|
1080
|
+
|
|
1081
|
+
Used by `table` columns and `matrix` values.
|
|
1082
|
+
|
|
1083
|
+
```jsonc
|
|
1084
|
+
// In‑cell horizontal bar sized by value; zero-baseline bars diverge for negatives
|
|
1085
|
+
{ "type": "bar", "color": "#0d9488", "negativeColor": "#dc2626", "baseline": "zero" }
|
|
1086
|
+
|
|
1087
|
+
// Background or text color scale
|
|
1088
|
+
{ "type": "colorScale", "scheme": "teal", "domain": [0, 1], "target": "background" }
|
|
1089
|
+
|
|
1090
|
+
// Unicode icon set (no icon fonts)
|
|
1091
|
+
{ "type": "icon", "set": "arrows", "position": "left" }
|
|
1092
|
+
|
|
1093
|
+
// First matching rule wins
|
|
1094
|
+
{ "type": "rules", "rules": [{ "when": "lt", "value": 0, "color": "#dc2626", "weight": "bold" }] }
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
| Field | Type | Applies to | Notes |
|
|
1098
|
+
| --- | --- | --- | --- |
|
|
1099
|
+
| `type` | `'bar' \| 'colorScale' \| 'icon' \| 'rules'` | all | Selects the style. |
|
|
1100
|
+
| `domain` | `[number, number]` | `bar`, `colorScale` | Value range; inferred from the column when omitted. |
|
|
1101
|
+
| `color`, `negativeColor`, `baseline`, `showValue` | strings/boolean | `bar` | Bar fill, diverging negative fill, zero/min baseline, and value overlay toggle. |
|
|
1102
|
+
| `scheme`, `midpoint`, `diverging`, `target` | mixed | `colorScale` | Sequential/diverging ramp controls; `target:'text'` colors text instead of background. |
|
|
1103
|
+
| `set`, `rules`, `position` | mixed | `icon` | Built-ins: `arrows`, `triangles`, `dots`, `trafficLights`; rules override thresholds. |
|
|
1104
|
+
| `rules` | `ValueRule[]` | `rules` | `when`: `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`; `between` is inclusive and needs `to`. |
|
|
1105
|
+
|
|
1106
|
+
Icon glyphs are Unicode: arrows `▲ ▬ ▼`, triangles `▲ ▼`, dots/traffic lights `●` with green/amber/red tones.
|
|
1107
|
+
|
|
1108
|
+
---
|
|
1109
|
+
|
|
1110
|
+
## Themes
|
|
1111
|
+
|
|
1112
|
+
`theme` is either a built‑in name (`'light'` / `'dark'`) or a partial override
|
|
1113
|
+
with an optional `base`:
|
|
1114
|
+
|
|
1115
|
+
```jsonc
|
|
1116
|
+
"theme": { "base": "dark", "color": { "accent": "#2dd4bf" } }
|
|
1117
|
+
```
|
|
1118
|
+
|
|
1119
|
+
Overridable token groups: `color` (`background`, `surface`, `text`, `textMuted`,
|
|
1120
|
+
`axis`, `grid`, `border`, `accent`, `palette[]`, `positive`, `negative`),
|
|
1121
|
+
`font`, `spacing`, `radius`, `stroke`. The default categorical `palette` is a
|
|
1122
|
+
vibrant, accessibility‑tuned 10‑color set that reads well on light and dark.
|
|
1123
|
+
|
|
1124
|
+
---
|
|
1125
|
+
|
|
1126
|
+
## Format mini‑language
|
|
1127
|
+
|
|
1128
|
+
A small, dependency‑free subset of d3‑format (numbers) plus strftime‑style dates.
|
|
1129
|
+
|
|
1130
|
+
**Numbers** — grammar `[$][,][.precision][type]`:
|
|
1131
|
+
|
|
1132
|
+
| Hint | Input | Output |
|
|
1133
|
+
| --- | --- | --- |
|
|
1134
|
+
| `,d` | `1234567` | `1,234,567` |
|
|
1135
|
+
| `.1f` | `3.14159` | `3.1` |
|
|
1136
|
+
| `.0%` | `0.42` | `42%` |
|
|
1137
|
+
| `$,.0f` | `5230` | `$5,230` |
|
|
1138
|
+
| `.1s` | `1200` | `1.2k` |
|
|
1139
|
+
| `.2e` | `12300` | `1.23e+4` |
|
|
1140
|
+
| `.3g` | `12345` | `12300` |
|
|
1141
|
+
|
|
1142
|
+
**Dates** — a hint containing `%` is treated as a date pattern. Tokens:
|
|
1143
|
+
`%Y` (2024), `%y` (24), `%m` (01), `%d` (01), `%e` (1), `%B`/`%b` (January/Jan),
|
|
1144
|
+
`%A`/`%a` (Monday/Mon), `%H` (00‑23), `%I` (01‑12), `%M`, `%S`, `%L` (ms),
|
|
1145
|
+
`%p` (AM/PM), `%j` (day of year), `%%` (literal `%`). Example: `%b %e, %Y` →
|
|
1146
|
+
`Jan 2, 2024`.
|
|
1147
|
+
|
|
1148
|
+
---
|
|
1149
|
+
|
|
1150
|
+
## Enumerations
|
|
1151
|
+
|
|
1152
|
+
| Name | Values |
|
|
1153
|
+
| --- | --- |
|
|
1154
|
+
| `ChartType` | `line`, `area`, `bar`, `scatter`, `pie`, `funnel`, `heatmap`, `kpi`, `table`, `matrix`, `box`, `sankey`, `choropleth`, `dropdown`, `search`, `list`, `range`, `dateRange` |
|
|
1155
|
+
| `SlicerType` | `dropdown`, `search`, `list`, `range`, `dateRange` |
|
|
1156
|
+
| `SelectionKind` | `point`, `set`, `range`, `text` |
|
|
1157
|
+
| `FieldType` | `quantitative`, `temporal`, `ordinal`, `nominal` |
|
|
1158
|
+
| `AggOp` | `sum`, `mean`, `avg`, `min`, `max`, `count`, `countDistinct`, `median`, `first`, `last` |
|
|
1159
|
+
| `CurveType` | `linear`, `monotone`, `step`, `stepBefore`, `stepAfter`, `catmullRom` |
|
|
1160
|
+
| `MapProjection` | `mercator`, `equirectangular`, `identity` |
|
|
1161
|
+
| Sequential schemes | `blues`, `teal`, `viridis`, `magma`, `greys` |
|
|
1162
|
+
| Diverging schemes | `redBlue`, `spectral`, `blueRed` |
|
|
1163
|
+
| `LegendPosition` | `top`, `right`, `bottom`, `left` |
|
|
1164
|
+
|
|
1165
|
+
---
|
|
1166
|
+
|
|
1167
|
+
## Runtime API
|
|
1168
|
+
|
|
1169
|
+
```ts
|
|
1170
|
+
import { render } from 'graphein';
|
|
1171
|
+
|
|
1172
|
+
const chart = render(target, spec); // target: HTMLElement | CSS selector string
|
|
1173
|
+
chart.update(nextSpec); // re-render with new data/config
|
|
1174
|
+
chart.resize(width?, height?); // re-measure (or force explicit dims) and redraw
|
|
1175
|
+
chart.destroy(); // tear down DOM, observers, listeners
|
|
1176
|
+
chart.spec; // the currently rendered spec (readonly)
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
`render()` returns a **`ChartInstance`**. With responsive `dimensions` (the
|
|
1180
|
+
default), the chart tracks its container via `ResizeObserver`. When a render
|
|
1181
|
+
settles, Graphein sets `data-graphein-ready="true"` on the surface root and increments
|
|
1182
|
+
`window.__GRAPHEIN_READY` — handy for screenshot/automation tooling to wait on.
|
|
1183
|
+
|
|
1184
|
+
### Validation & linting
|
|
1185
|
+
|
|
1186
|
+
```ts
|
|
1187
|
+
import { validateSpec, lintSpec } from 'graphein';
|
|
1188
|
+
|
|
1189
|
+
const { valid, errors, warnings } = validateSpec(spec);
|
|
1190
|
+
```
|
|
1191
|
+
|
|
1192
|
+
`validateSpec(spec)` returns `{ valid, errors, warnings }`. Each item is a
|
|
1193
|
+
`ValidationError` `{ path, message, rule?, severity?, fix?, suggestion? }`:
|
|
1194
|
+
|
|
1195
|
+
- **`errors`** are structural problems (missing channel, bad enum, unknown field).
|
|
1196
|
+
Fix every one — `valid` is `false` while any remain.
|
|
1197
|
+
- **`warnings`** include **dataviz lint** findings: best-practice advice that never
|
|
1198
|
+
blocks rendering. Lint findings carry a stable `rule` id and a `severity`
|
|
1199
|
+
(`'warning'` | `'info'`) so you can recognize or suppress a specific one.
|
|
1200
|
+
- **`fix`** — when the right correction is unambiguous (a misspelled chart `type`
|
|
1201
|
+
or enum value, a temporal-looking field typed as a category), the error carries
|
|
1202
|
+
a list of **JSON Patch** (RFC 6902) ops that resolve it. Apply them directly
|
|
1203
|
+
instead of regenerating, or let [`repairSpec`](#self-repairing-specs) do it.
|
|
1204
|
+
- **`suggestion`** — `{ kind, candidates }` "did you mean" hints (nearest first)
|
|
1205
|
+
for an unrecognized `type`, enum, or field name. Suggestions are advisory; only
|
|
1206
|
+
the unambiguous ones also come with a `fix`.
|
|
1207
|
+
|
|
1208
|
+
`lintSpec(spec)` runs just the lint rules (also reachable via `validateSpec`
|
|
1209
|
+
warnings). Current rules:
|
|
1210
|
+
|
|
1211
|
+
| `rule` | Fires when |
|
|
1212
|
+
| --- | --- |
|
|
1213
|
+
| `temporal-typed-as-categorical` | a date‑like field is typed `nominal`/`ordinal` (set `type:"temporal"`). |
|
|
1214
|
+
| `pie-too-many-slices` | a pie/donut has more than 7 slices. |
|
|
1215
|
+
| `too-many-series` | a `series`/`color` field has more than 12 distinct values. |
|
|
1216
|
+
| `bar-nonzero-baseline` | a bar's `y` scale disables zero (`zero:false`) or starts above 0. |
|
|
1217
|
+
| `log-nonpositive-data` | a `log` axis covers data with values ≤ 0. |
|
|
1218
|
+
| `high-cardinality-axis` | a discrete `x`/`y` axis has more than 50 categories. |
|
|
1219
|
+
|
|
1220
|
+
Lint rules evaluate the **post‑`transform`** data, so cardinality reflects what
|
|
1221
|
+
actually renders.
|
|
1222
|
+
|
|
1223
|
+
### Self-repairing specs
|
|
1224
|
+
|
|
1225
|
+
```ts
|
|
1226
|
+
import { repairSpec } from 'graphein';
|
|
1227
|
+
|
|
1228
|
+
const { spec, applied, remaining } = repairSpec(brokenSpec);
|
|
1229
|
+
// spec → a corrected copy (input is never mutated)
|
|
1230
|
+
// applied → the JsonPatchOp[] that were applied, in order
|
|
1231
|
+
// remaining → structural errors still unresolved (empty ⇒ the spec is now valid)
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
`repairSpec(spec)` applies every **safe, unambiguous** `fix` that `validateSpec`
|
|
1235
|
+
attaches, then re-validates — iterating, because one fix can unlock another (e.g.
|
|
1236
|
+
correcting `type` changes which channels are required). It only applies fixes the
|
|
1237
|
+
validator is confident about; genuinely ambiguous problems (a missing channel
|
|
1238
|
+
with no obvious field, a far-from-anything type) are left in `remaining` for you
|
|
1239
|
+
to resolve. This turns the common agent mistakes — a typo'd chart type, a
|
|
1240
|
+
misspelled aggregate `op`, a date field typed `nominal` — into a one-step
|
|
1241
|
+
correction rather than a full regenerate.
|
|
1242
|
+
|
|
1243
|
+
You can also apply fixes yourself with the exported JSON Patch helpers
|
|
1244
|
+
`applyPatch(doc, ops)` and `toPointer(dottedPath)`.
|
|
1245
|
+
|
|
1246
|
+
### Render report
|
|
1247
|
+
|
|
1248
|
+
After a chart draws, `instance.report()` returns a **machine-readable** description
|
|
1249
|
+
of what was actually rendered — so an agent can verify the chart "looks right"
|
|
1250
|
+
without ever seeing a pixel. It closes the critique loop: generate → validate →
|
|
1251
|
+
render → **report** → repair.
|
|
1252
|
+
|
|
1253
|
+
```ts
|
|
1254
|
+
const chart = render('#app', spec);
|
|
1255
|
+
const report = chart.report();
|
|
1256
|
+
// report.ok → true when no warning/error diagnostics were raised
|
|
1257
|
+
// report.markCount → number of data marks drawn
|
|
1258
|
+
// report.seriesCount → distinct series
|
|
1259
|
+
// report.colorCount → distinct series colors
|
|
1260
|
+
// report.plot → the plot rectangle (cartesian charts)
|
|
1261
|
+
// report.diagnostics → RenderDiagnostic[] (most-severe first)
|
|
1262
|
+
for (const d of report.diagnostics) {
|
|
1263
|
+
// d.code · d.severity ('error' | 'warning' | 'info') · d.message · d.axis? · d.details?
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
`buildRenderReport(input)` is also exported as a pure function if you want to
|
|
1268
|
+
compute a report outside the render lifecycle. The report is derived entirely
|
|
1269
|
+
from the resolved model (scales, ticks, legend, theme colors) — **no canvas
|
|
1270
|
+
read-back** — so it returns identically in the browser and headless. To run the
|
|
1271
|
+
loop server-side, [`@graphein/node`](https://www.npmjs.com/package/@graphein/node)'s
|
|
1272
|
+
`renderChart(spec)` returns the PNG **and** this report; core's dependency-free
|
|
1273
|
+
`renderToContext(target, spec)` returns it while painting onto any 2D context.
|
|
1274
|
+
|
|
1275
|
+
**Diagnostic codes**
|
|
1276
|
+
|
|
1277
|
+
| `code` | Severity | Meaning |
|
|
1278
|
+
| --- | --- | --- |
|
|
1279
|
+
| `empty-data` | warning | No rows to plot — the chart is blank. |
|
|
1280
|
+
| `empty-plot` | error | The plot area collapsed (chrome ate all the space). |
|
|
1281
|
+
| `axis-label-overlap` | warning | Adjacent x-axis category labels collide — too many to show. |
|
|
1282
|
+
| `legend-overflow` | warning | A vertical legend was truncated; some series aren't shown. |
|
|
1283
|
+
| `degenerate-axis` | warning/info | The y values are all equal (flat line) or the scale is too narrow. |
|
|
1284
|
+
| `marks-clipped` | warning | Data falls outside the y range and is clipped at the plot edge. |
|
|
1285
|
+
| `low-contrast-mark` | warning | A series color is nearly invisible against the background. |
|
|
1286
|
+
| `low-contrast-text` | warning | Axis/legend label color fails the 4.5:1 text-contrast minimum. |
|
|
1287
|
+
| `too-many-colors` | info | More than ~8 series share one color scale — hard to tell apart. |
|
|
1288
|
+
|
|
1289
|
+
In `@graphein/react`, read the report from the instance handed to `onReady`:
|
|
1290
|
+
`<Chart spec onReady={(c) => console.log(c.report())} />`.
|
|
1291
|
+
|
|
1292
|
+
### Selections & dashboards
|
|
1293
|
+
|
|
1294
|
+
Both `ChartInstance` and `DashboardInstance` expose an imperative **selection API**,
|
|
1295
|
+
and `renderDashboard` mounts a whole [dashboard](#dashboards) spec:
|
|
1296
|
+
|
|
1297
|
+
```ts
|
|
1298
|
+
import { renderDashboard, render, createSelectionStore } from 'graphein';
|
|
1299
|
+
|
|
1300
|
+
const d = renderDashboard(target, dashboardSpec);
|
|
1301
|
+
d.getSelection(name?); // current SelectionValue (or a map when name omitted)
|
|
1302
|
+
d.setSelection('region', value); // drive a param programmatically
|
|
1303
|
+
d.clearSelection(name?); // clear one / all params
|
|
1304
|
+
const off = d.on('selectionchange', (name, value) => {/* … */});
|
|
1305
|
+
off(); // or d.off('selectionchange', fn)
|
|
1306
|
+
d.update(next); d.resize(); d.destroy(); d.spec; d.views; d.store;
|
|
1307
|
+
|
|
1308
|
+
// Linking standalone charts: share one store across render() calls.
|
|
1309
|
+
const store = createSelectionStore();
|
|
1310
|
+
const a = render('#a', specA, { store, onSelectionChange: (n, v) => {} });
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
`render(target, spec, options?)` accepts `{ store?, onSelectionChange? }` (both
|
|
1314
|
+
optional, backward compatible). In `@graphein/react`, `<Dashboard spec onSelectionChange? />`,
|
|
1315
|
+
`<Chart spec store? onSelectionChange? />`, and `useSelection(target, name?) → [value, setValue]`
|
|
1316
|
+
mirror this surface.
|
|
1317
|
+
|
|
1318
|
+
### Performance
|
|
1319
|
+
|
|
1320
|
+
- **LTTB decimation** downsamples very large line/area series to roughly one point
|
|
1321
|
+
per pixel for drawing, while hit‑testing/tooltips keep full resolution. A 50k‑point
|
|
1322
|
+
series renders crisply and pans smoothly.
|
|
1323
|
+
- **Layered canvases:** hover/crosshair paints only an interaction layer, never the
|
|
1324
|
+
marks layer — so interaction never triggers a full redraw.
|
|
1325
|
+
- **Virtualized tables** window rows so `table`/`matrix` stay responsive at large
|
|
1326
|
+
row counts.
|
|
1327
|
+
|
|
1328
|
+
### Animation
|
|
1329
|
+
|
|
1330
|
+
Charts play a brief **entrance animation** the first time they render; **resizes**
|
|
1331
|
+
are always instant (no re‑animation, no jank while dragging):
|
|
1332
|
+
|
|
1333
|
+
- **Cartesian charts** (line/area/bar/scatter/box/heatmap) sweep their marks in
|
|
1334
|
+
left‑to‑right with a short fade — the axes, gridlines, and labels are drawn
|
|
1335
|
+
immediately so only the data "draws on".
|
|
1336
|
+
- **Pie, funnel, KPI, sankey, choropleth, tables** fade and rise in subtly.
|
|
1337
|
+
|
|
1338
|
+
On **`update()`** (new data or config), canvas‑mark charts
|
|
1339
|
+
(line/area/bar/scatter/box/pie/heatmap/sankey/choropleth) **cross‑fade** the marks
|
|
1340
|
+
layer from the previous frame to the next — a smooth dissolve whose final frame is
|
|
1341
|
+
pixel‑identical to an instant redraw. DOM charts (`kpi`/`table`/`matrix`) update
|
|
1342
|
+
instantly, and a simultaneous size change snaps (that's a resize, not a data
|
|
1343
|
+
morph) to avoid a stretched bitmap.
|
|
1344
|
+
|
|
1345
|
+
Tuning via the `animation` field:
|
|
1346
|
+
|
|
1347
|
+
```jsonc
|
|
1348
|
+
{ "animation": false } // disable entirely
|
|
1349
|
+
{ "animation": { "duration": 700, "easing": "cubicOut" } }
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
- Default entrance duration is **480ms** (easing `cubicOut`); the update
|
|
1353
|
+
cross‑fade is **360ms** (easing `cubicInOut`).
|
|
1354
|
+
- **`prefers-reduced-motion`** is honored automatically — when the OS requests
|
|
1355
|
+
reduced motion, entrances and update cross‑fades are suppressed and charts
|
|
1356
|
+
render directly in their final state.
|
|
1357
|
+
- Automation/screenshot harnesses can force‑disable all motion by setting
|
|
1358
|
+
`window.__GRAPHEIN_DISABLE_ANIM = true` before rendering, which keeps captures
|
|
1359
|
+
deterministic without changing any spec.
|
|
1360
|
+
- **Web fonts:** axis‑gutter and label widths depend on the chart's font. If a
|
|
1361
|
+
chart renders before its web font has loaded, it lays out with fallback metrics
|
|
1362
|
+
and then **re‑lays‑out automatically once the font loads**, so the final result
|
|
1363
|
+
is always correct. (For pixel‑exact screenshots, also `await
|
|
1364
|
+
document.fonts.ready` before capturing.)
|
|
1365
|
+
|
|
1366
|
+
---
|
|
1367
|
+
|
|
1368
|
+
## Accessibility
|
|
1369
|
+
|
|
1370
|
+
Every rendered chart is wrapped as an accessible **figure**:
|
|
1371
|
+
|
|
1372
|
+
- The surface root gets `role="figure"` and an `aria-label`. Set `description`
|
|
1373
|
+
for precise alt text; otherwise Graphein synthesizes one from the type, title, and
|
|
1374
|
+
row count (e.g. _"Bar chart: Quarterly revenue. 4 data points."_).
|
|
1375
|
+
- The canvas layers are `aria-hidden` (decorative). For canvas‑drawn charts
|
|
1376
|
+
(line/area/bar/scatter/pie/heatmap) Graphein also injects a **visually‑hidden
|
|
1377
|
+
`<table>`** mirroring the data (capped at 100 rows) so screen‑reader users can
|
|
1378
|
+
read the underlying numbers. `table`/`matrix` already render a semantic
|
|
1379
|
+
`<table>` (with `aria-sort` on sortable headers) and `kpi` renders real text,
|
|
1380
|
+
so no fallback is added for those.
|
|
1381
|
+
- All titles, axis labels, and legends are real DOM text (not canvas pixels), so
|
|
1382
|
+
they're selectable and readable by assistive tech.
|