plotagents 0.0.0-stage → 0.7.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.
Files changed (67) hide show
  1. package/AGENTS.md +98 -0
  2. package/LICENSE +21 -0
  3. package/README.md +217 -2
  4. package/bin/plotagents.js +73 -0
  5. package/dist/plotagents-geo.js +79 -0
  6. package/dist/plotagents.js +11611 -0
  7. package/docs/api.md +172 -0
  8. package/docs/charts.md +352 -0
  9. package/docs/colorscales.md +46 -0
  10. package/docs/dashboards.md +79 -0
  11. package/docs/getting-started.md +142 -0
  12. package/docs/rendering.md +47 -0
  13. package/docs/themes.md +116 -0
  14. package/index.d.ts +384 -0
  15. package/package.json +80 -4
  16. package/schema.json +1874 -0
  17. package/src/axes.js +152 -0
  18. package/src/charts/bar.js +350 -0
  19. package/src/charts/categorical.js +495 -0
  20. package/src/charts/chord.js +218 -0
  21. package/src/charts/choropleth.js +807 -0
  22. package/src/charts/dist.js +530 -0
  23. package/src/charts/donut.js +232 -0
  24. package/src/charts/flowchart.js +407 -0
  25. package/src/charts/gantt.js +460 -0
  26. package/src/charts/heatmap.js +76 -0
  27. package/src/charts/kpi.js +100 -0
  28. package/src/charts/line.js +551 -0
  29. package/src/charts/network.js +707 -0
  30. package/src/charts/pointmap.js +599 -0
  31. package/src/charts/pyramid.js +177 -0
  32. package/src/charts/radar.js +158 -0
  33. package/src/charts/regplot.js +370 -0
  34. package/src/charts/sankey.js +169 -0
  35. package/src/charts/scatter.js +214 -0
  36. package/src/charts/sunburst.js +136 -0
  37. package/src/charts/treemap.js +197 -0
  38. package/src/charts/wordcloud.js +163 -0
  39. package/src/colorscales.js +182 -0
  40. package/src/context.js +10 -0
  41. package/src/dashboard.js +438 -0
  42. package/src/dashed.js +90 -0
  43. package/src/data.js +137 -0
  44. package/src/facet.js +84 -0
  45. package/src/format.js +203 -0
  46. package/src/frame.js +226 -0
  47. package/src/geo/chile.js +7 -0
  48. package/src/geo/data/chile-regiones.js +12 -0
  49. package/src/geo/data/usa-estados.js +12 -0
  50. package/src/geo/data/world.js +12 -0
  51. package/src/geo/projection.js +217 -0
  52. package/src/geo/registry.js +110 -0
  53. package/src/geo/usa.js +8 -0
  54. package/src/geo/world.js +7 -0
  55. package/src/hierarchy.js +139 -0
  56. package/src/index.js +336 -0
  57. package/src/infer.js +58 -0
  58. package/src/interact.js +483 -0
  59. package/src/lint.js +417 -0
  60. package/src/palette.js +59 -0
  61. package/src/runtime.js +416 -0
  62. package/src/scale.js +241 -0
  63. package/src/schema.js +337 -0
  64. package/src/stats.js +303 -0
  65. package/src/svg.js +85 -0
  66. package/src/text.js +61 -0
  67. package/src/theme.js +311 -0
@@ -0,0 +1,79 @@
1
+ # Dashboards
2
+
3
+ `dashboard(spec)` turns a handful of chart specs into a finished, self-contained HTML dashboard. Like a single chart, the design decisions come built in: you say what goes on the page, and the library decides where and how.
4
+
5
+ ```js
6
+ import { dashboard } from 'plotagents';
7
+ import { writeFileSync } from 'node:fs';
8
+
9
+ writeFileSync('sales.html', dashboard({
10
+ title: 'How are southern sales doing?',
11
+ subtitle: 'Sales by month, region and channel, 2024–2025, CLP millions',
12
+ source: 'Commercial ERP',
13
+ updated: '2026-01-05',
14
+ locale: 'en',
15
+ data: sales, // shared rows
16
+ kpis: [
17
+ { label: 'Sales 2025', field: 'sales', agg: 'sum', format: 'compact',
18
+ compare: { field: 'year', current: 2025, previous: 2024 }, trendBy: 'month' },
19
+ { label: 'Orders 2025', field: 'orders', agg: 'sum', format: 'compact',
20
+ compare: { field: 'year', current: 2025, previous: 2024 } },
21
+ { label: 'Online share', value: 0.361, delta: 0.13, format: 'percent', period: 'vs 2024' },
22
+ ],
23
+ filters: ['region', 'channel'],
24
+ charts: [
25
+ { type: 'line', x: 'month', y: 'sales', series: 'region', highlight: 'Ñuble',
26
+ title: 'Ñuble grew 37% this year, faster than any other region' },
27
+ { type: 'choropleth', geo: 'chile', key: 'region', value: 'sales', fitTo: ['Biobío', 'Ñuble', 'Maule', 'Araucanía'],
28
+ title: 'Biobío makes 36% of sales' },
29
+ { type: 'bar', x: 'channel', y: 'sales', series: 'year',
30
+ title: 'Online is the only channel growing strongly' },
31
+ { type: 'treemap', path: ['region', 'channel'], value: 'sales',
32
+ title: 'Physical stores are still half the business' },
33
+ ],
34
+ }));
35
+ ```
36
+
37
+ The page works without JavaScript: every chart is drawn on the server, so it opens in any browser, prints and converts to PDF. When the browser build is available (it is, in Node), the page also gets tooltips, map zoom and live filters. In a web app, `renderDashboard('#board', spec)` draws the same dashboard into an element.
38
+
39
+ ## What it decides for you
40
+
41
+ These defaults come from dashboard design practice: Stephen Few's *Information Dashboard Design*, the Z-pattern of reading, and what makes business dashboards hard to read.
42
+
43
+ | Rule | What plotagents does |
44
+ |---|---|
45
+ | **One question per dashboard** | The `title` is the question ("How are southern sales doing?"). `lintDashboard` warns above 9 charts and suggests splitting above 6. |
46
+ | **Numbers first** | KPIs go in one row at the top, at most 5 (warning above). Each one has a label, the figure, its change with an arrow **and** a sign (never color alone), the comparison period and an optional sparkline. |
47
+ | **Z-pattern reading** | The main chart comes right after the KPIs, at full width: the one marked `primary: true`, or else the first time series. |
48
+ | **Full rows, no holes** | A 12-column grid in balanced rows: 2 charts → 6 + 6 (8 + 4 when one is wide), 3 → 4 + 4 + 4, 4 → two rows of two, 5 → 3 + 2. Panels in a row share a height. |
49
+ | **Width follows the chart** | Time series, Gantt charts, heatmaps and flows read left to right, so they get wide panels. Donuts, KPIs and short rankings get narrow ones. Chile's map gets a narrow column. |
50
+ | **Same thing, same color** | One color map across every chart: "Ñuble" is the same color in the line, the treemap and the bars. `colors` pins colors for the whole page. |
51
+ | **One voice** | One theme, locale and typeface for every panel. The source appears once, at the bottom; a chart keeps its own source only when it differs. |
52
+ | **Filters as buttons** | `filters` become buttons when a field has up to 6 values, and a dropdown when it has more. They filter every chart that has the field, recompute the KPIs and say what is filtered in each subtitle. A filter on a chart's `time` field moves that chart to the chosen period. |
53
+ | **Phones** | One column in reading order; KPIs in two columns. |
54
+ | **Print** | Panels don't break across pages; the filter bar is not sticky on paper. |
55
+
56
+ ## Spec
57
+
58
+ | Option | Meaning |
59
+ |---|---|
60
+ | `title`, `subtitle` | The question, and the scope (measure, unit, period). |
61
+ | `source`, `updated`, `note` | Footer and header metadata. |
62
+ | `locale`, `theme`, `font`, `palette` | Applied to every chart (`theme: "noche"` for a dark dashboard). |
63
+ | `data` | Shared rows. Charts without `data` use them; computed KPIs and filters work on them. |
64
+ | `kpis` | `[{ label, value, delta, period, trend, good, format }]` (fixed) or `[{ label, field, agg, over, scale, compare, trendBy, format }]` (computed). `agg`: `sum`, `mean`, `median`, `min`, `max`, `count`, `distinct`, `ratio` (with `over`). `compare: { field, current, previous }` shows the current period and its change. `trendBy: "month"` draws the sparkline. |
65
+ | `charts` | Chart specs, as in `plot()`. Per chart: `primary: true`, `size: "full" \| "large" \| "medium" \| "small"` or `span` (of 12). |
66
+ | `filters` | `["region"]` or `[{ field, label, control: "buttons" \| "select", value }]`. |
67
+ | `layout` | `"auto"` (default), `"executive"` (main chart full width, then pairs) or `"grid"` (equal panels). |
68
+ | `colors` | `{ "Biobío": "#3A47B8" }` for every chart. |
69
+
70
+ ## For agents
71
+
72
+ ```js
73
+ import { safeDashboard, toolDefinition } from 'plotagents';
74
+
75
+ const tool = toolDefinition({ kind: 'dashboard' }); // JSON Schema of the dashboard spec
76
+ const { ok, html, errors, warnings, suggestions } = safeDashboard(input);
77
+ ```
78
+
79
+ `safeDashboard` never throws. It reviews the dashboard as a whole (too many charts or KPIs, several pies, mixed themes, filters on fields no chart has, panels without a title) and every chart in it, tagging each issue with the chart's number and title so the model knows which one to fix. `planDashboard(spec)` returns the layout decisions (panels, widths, rows, colors, filters) without drawing anything.
@@ -0,0 +1,142 @@
1
+ # Getting started
2
+
3
+ plotagents turns one JSON object into a finished, accessible SVG chart. You pass rows of data and name the fields; the library makes every design decision: chart layout, colors, labels, number formats and spacing.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install plotagents
9
+ ```
10
+
11
+ Or straight from GitHub, for the latest unreleased changes:
12
+
13
+ ```bash
14
+ npm install github:carlosnavarreteliz/plotagents
15
+ ```
16
+
17
+ Node 18 or later, Deno and Bun work out of the box. The package has no dependencies.
18
+
19
+ ## Your first chart
20
+
21
+ ```js
22
+ import { plot } from 'plotagents';
23
+
24
+ const svg = plot({
25
+ type: 'bar',
26
+ title: 'Biobío accounts for 30% of southern sales',
27
+ subtitle: 'Sales by region, CLP millions, 2025',
28
+ source: 'illustrative data',
29
+ data: [
30
+ { region: 'Biobío', sales: 1240 },
31
+ { region: 'Araucanía', sales: 880 },
32
+ { region: 'Ñuble', sales: 610 },
33
+ ],
34
+ x: 'region',
35
+ y: 'sales',
36
+ locale: 'en',
37
+ });
38
+ ```
39
+
40
+ `svg` is a string. Write it to a `.svg` file, put it in HTML or Markdown, or convert it to PDF or PNG with any SVG tool.
41
+
42
+ ## Anatomy of a spec
43
+
44
+ | Field | What it is |
45
+ |---|---|
46
+ | `type` | The chart type, such as `bar`, `line` or `treemap`. Leave it out to infer it from the data. |
47
+ | `data` | A list of rows: `[{ region: "Biobío", sales: 1240 }]`. |
48
+ | `x`, `y`, `series` | Which field goes where. Each chart type documents its own roles in the [chart reference](charts.md). |
49
+ | `title` | The finding, as a full sentence with the key number. |
50
+ | `subtitle` | How to read the chart: measure, unit and period. |
51
+ | `source` | Where the data comes from. Use `"illustrative data"` when it is made up. |
52
+ | `locale` | `"es"` (the default, `1.234,5`) or `"en"` (`1,234.5`). |
53
+
54
+ ### Wide tables
55
+
56
+ When each measure is its own column, pass a list in `y` and each field becomes a series:
57
+
58
+ ```js
59
+ plot({
60
+ type: 'line',
61
+ data: [
62
+ { month: '2025-01', revenue: 120, costs: 90 },
63
+ { month: '2025-02', revenue: 132, costs: 94 },
64
+ ],
65
+ x: 'month',
66
+ y: ['revenue', 'costs'],
67
+ });
68
+ ```
69
+
70
+ ### Numbers written as text
71
+
72
+ Values such as `"1.234,5"`, `"12%"` or `"$ 3.000"` are parsed automatically, so data copied from a spreadsheet works without cleaning.
73
+
74
+ ## In the browser
75
+
76
+ Without a bundler, load the browser build and call `render()`. It draws into an element and adds tooltips, responsive width, time sliders and drill-down.
77
+
78
+ ```html
79
+ <div id="chart"></div>
80
+ <script src="https://cdn.jsdelivr.net/gh/carlosnavarreteliz/plotagents@master/dist/plotagents.js"></script>
81
+ <script>
82
+ plotagents.render('#chart', {
83
+ type: 'line',
84
+ data: [
85
+ { month: '2025-01', sales: 120 },
86
+ { month: '2025-02', sales: 140 },
87
+ { month: '2025-03', sales: 135 },
88
+ ],
89
+ x: 'month',
90
+ y: 'sales',
91
+ title: 'Sales rose 12% in the first quarter',
92
+ locale: 'en',
93
+ });
94
+ </script>
95
+ ```
96
+
97
+ The same file ships in the package as `dist/plotagents.js`. Maps need the geometries too: add `dist/plotagents-geo.js` after `plotagents.js`.
98
+
99
+ `render()` returns a handle with `update(spec)` to redraw with a new spec and `destroy()` to remove the chart and its listeners.
100
+
101
+ ## A whole report in one file
102
+
103
+ `toHTML()` builds a self-contained, interactive page with the library inlined, so it works offline and from a file.
104
+
105
+ ```js
106
+ import { writeFileSync } from 'node:fs';
107
+ import { toHTML } from 'plotagents';
108
+
109
+ writeFileSync('report.html', toHTML([spec1, spec2], { title: 'Monthly report', lang: 'en', columns: 2 }));
110
+ ```
111
+
112
+ ## From the command line
113
+
114
+ ```bash
115
+ npx plotagents spec.json -o chart.svg # SVG
116
+ npx plotagents specs.json -o report.html # several interactive charts on one page
117
+ npx plotagents spec.json --lint # review and suggestions
118
+ npx plotagents spec.json --describe # alt text
119
+ npx plotagents --tool # tool definition for an agent
120
+ ```
121
+
122
+ The CLI also reads a spec from standard input: `cat spec.json | npx plotagents > chart.svg`.
123
+
124
+ ## Catching mistakes
125
+
126
+ `plot()` throws a `PlotError` with a hint when a spec cannot be drawn. `safePlot()` never throws, and `lint()` reviews a spec without drawing it:
127
+
128
+ ```js
129
+ import { safePlot } from 'plotagents';
130
+
131
+ const { ok, svg, errors, warnings, suggestions } = safePlot(spec);
132
+ if (!ok) console.log(errors.map((e) => `${e.message} → ${e.fix}`));
133
+ ```
134
+
135
+ Every error, warning and suggestion carries a `fix` that says what to change.
136
+
137
+ ## Next steps
138
+
139
+ - Browse the [gallery](../examples/) to find the chart you need and copy its code.
140
+ - Try specs live in the [playground](../playground/).
141
+ - Read the [chart reference](charts.md) for every option of every type.
142
+ - Give an agent the [agent guide](agents.md) and the tool definition.
@@ -0,0 +1,47 @@
1
+ # Rendering: SVG or Canvas?
2
+
3
+ **Decision: SVG everywhere, with automatic path batching for dense marks.** Canvas was evaluated and rejected: batched SVG is faster than Canvas on every size we tested and keeps the properties that matter for a library used by agents.
4
+
5
+ ## What we measured
6
+
7
+ Scatter plot, two colors, 720 × 420 px, Chromium (Playwright), time from inserting the markup to the second animation frame. Same machine, same data.
8
+
9
+ | Points | SVG, one `<circle>` per point | Canvas 2D (`arc` + `fill`) | SVG, one `<path>` per color | plotagents (batched SVG, full chart) |
10
+ |---|---|---|---|---|
11
+ | 1,000 | 32 ms | 29 ms | 22 ms | 26 ms |
12
+ | 5,000 | 62 ms | 30 ms | 13 ms | 12 ms |
13
+ | 20,000 | 671 ms · 2.9 MB | 100 ms | 37 ms | 38 ms · 2.1 MB |
14
+ | 50,000 | 1,468 ms · 7.2 MB | 357 ms | 138 ms | 123 ms · 3.4 MB |
15
+
16
+ The cost of naive SVG is the DOM: one element per mark. Drawing all circles of one color as arcs inside a single `<path>` removes that cost, and the browser rasterizes one path faster than a script can issue tens of thousands of Canvas calls.
17
+
18
+ Reproduce it with `node scripts/bench.js` (needs Playwright with Chromium installed).
19
+
20
+ ## Why SVG wins for this library
21
+
22
+ | Need | SVG | Canvas |
23
+ |---|---|---|
24
+ | Render on a server, in Node, with zero dependencies | Yes: SVG is a string | Needs a native module (`node-canvas`) or a headless browser |
25
+ | An agent can read and check its own output | Yes: titles, labels and values are text | No: a bitmap |
26
+ | Accessibility (`<title>`, `<desc>`, screen readers, text selection) | Built in | Must be rebuilt by hand next to the canvas |
27
+ | Crisp at any zoom, print and PDF | Vector | Fixed resolution; must re-render for each pixel ratio |
28
+ | Paste into slides, Word, Figma, Markdown, email | Works as a file | Only as an image |
29
+ | Theming with CSS, dark mode, hover states | CSS classes | Manual redraw |
30
+ | 50,000+ points | Fast with batching (above) | Fast |
31
+
32
+ Canvas only wins when a chart needs hundreds of thousands of marks redrawn every frame, such as live streaming or animated particle fields. That is outside what a chart for decisions needs, and well past the point where a heatmap or a density plot (`kde`, `hist`) tells the story better than overplotted points.
33
+
34
+ ## How batching works
35
+
36
+ `dotLayer()` in `src/svg.js` takes the circles a chart wants to draw:
37
+
38
+ - **Up to 1,500 points:** one `<circle>` each, with its own tooltip and hover state.
39
+ - **More than 1,500 points:** circles are grouped by style (color, radius, opacity) and each group becomes one `<path>` of arcs. A transparent `<rect class="pa-near">` carries the coordinates and tooltip text of up to 20,000 points, and the browser layer finds the nearest point within 14 px on hover and rings it.
40
+
41
+ Scatter, bubble, regplot, residplot, jointplot, strip and swarm use it automatically. No option is needed.
42
+
43
+ ## Other performance choices
44
+
45
+ - **Text measurement without a DOM.** Width tables calibrated for humanist sans faces let labels be fitted, wrapped and truncated identically in Node and in the browser.
46
+ - **Numbers rounded to two decimals** in all coordinates keeps files small.
47
+ - **One file for the browser.** `dist/plotagents.js` (no dependencies) is inlined by `toHTML()` so interactive pages work offline, from a file, or inside an artifact.
package/docs/themes.md ADDED
@@ -0,0 +1,116 @@
1
+ # Themes
2
+
3
+ A theme is a JSON object. Pass a name or an object; objects are deep-merged over a base theme, so you only write what changes.
4
+
5
+ ```js
6
+ plot({ ...spec, theme: 'noche' }); // built-in dark theme
7
+ plot({ ...spec, theme: { extends: 'noche', marks: { lineWidth: 3 } } });
8
+ plot({ ...spec, palette: 'okabe-ito' }); // shortcut for theme.categorical
9
+ plot({ ...spec, ramp: 'teal' }); // shortcut for theme.sequential
10
+ ```
11
+
12
+ ## Library-wide defaults
13
+
14
+ ```js
15
+ configure({ font: 'Poppins', textScale: 1.4, theme: { extends: 'papel', marks: { lineWidth: 2.5 } } });
16
+ ```
17
+
18
+ Every later `plot()`, `render()`, `toHTML()` and dashboard starts from these values; the spec always wins. A spec's own `theme` object is deep-merged **over** the global theme, so `configure({ theme: 'noche' })` plus `theme: { marks: { barRadius: 0 } }` stays dark. `configure()` returns the current defaults and `configure(null)` clears them.
19
+
20
+ ## Your own theme, everywhere
21
+
22
+ The default theme (`papel`) never changes unless you ask. To restyle every chart, register a theme once and make it the default:
23
+
24
+ ```js
25
+ import { registerTheme, configure } from 'plotagents';
26
+
27
+ registerTheme('acme', {
28
+ extends: 'papel', // or 'noche', or another registered theme
29
+ background: '#FFFFFF', // alias of surface
30
+ text: '#111827', // alias of ink
31
+ lede: '#4B5563',
32
+ muted: '#6B7280',
33
+ grid: { color: '#EEF0F4', dash: '2 3', width: 1 },
34
+ axis: '#C7CBD6',
35
+ categorical: 'tab10', // any palette or matplotlib qualitative map
36
+ sequential: 'viridis', // any colormap name
37
+ tooltip: { bg: '#111827', ink: '#FFFFFF' },
38
+ radius: 2, // bar, tile and swatch corners
39
+ font: 'Inter', // Google Fonts name or CSS stack
40
+ });
41
+
42
+ configure({ theme: 'acme' });
43
+ ```
44
+
45
+ `theme: 'acme'` also works per chart, and `{ extends: 'acme', … }` builds on it. Colors are validated: anything that is not a CSS color (hex, `rgb()`, `hsl()` or a color name) is dropped and the base value is kept.
46
+
47
+ ## Built-in themes
48
+
49
+ | Name | Mode | Surface | Use |
50
+ |---|---|---|---|
51
+ | `papel` (alias `light`) | light | `#FCFBF9` paper | default |
52
+ | `noche` (alias `dark`) | dark | `#0B0F2B` deep navy | dark pages, slides |
53
+
54
+ Both share the same eight hues, stepped separately for each surface and validated on it.
55
+
56
+ ## Keys
57
+
58
+ | Key | Type | Meaning |
59
+ |---|---|---|
60
+ | `extends` | theme name or object | Base to merge over (default `papel`, or `noche` when `mode: 'dark'`). |
61
+ | `mode` | `'light' \| 'dark'` | Affects tooltips and text-on-fill contrast. |
62
+ | `surface` (alias `background`) | color | Chart background. Also used for 2 px gaps and rings around marks. |
63
+ | `ink` (alias `text`), `lede`, `muted` | color | Primary text, secondary text, axis and source text. |
64
+ | `grid` | color or `{ color, dash, width }` | Gridlines (default 1 px, solid). `dash: '2 3'` draws dotted gridlines. |
65
+ | `axis` | color or `{ color, width }` | Baselines. |
66
+ | `tooltip` | `{ bg, ink }` | Tooltip card colors in the browser (default: white card on light themes, navy on dark). |
67
+ | `radius`, `barRadius` | number | Corner radius of bars (default 4). `radius` sets it for every mark that has corners. |
68
+ | `tint`, `tint2` | color | Quiet fills: KPI tiles, slider tracks, meters. |
69
+ | `dim` | color | Muted marks when `highlight` is used. |
70
+ | `other` | color | The "Other" bucket and series past the 8th. |
71
+ | `categorical` | colors, palette or colormap name | Identity colors, assigned in this order. |
72
+ | `sequential` | colors, ramp or colormap name | Light → dark, for heatmaps and maps. |
73
+ | `diverging` | colors, ramp or colormap name | Two poles with a neutral midpoint. |
74
+ | `status` | `{ good, warning, critical }` | KPI deltas and meters. Never used for series. |
75
+ | `font` | `{ family, mono, import }` | Font stack and optional Google Fonts URL (`fontImport: true` embeds it in the SVG). |
76
+ | `type` | `{ title, subtitle, label, axis, legend, source, titleWeight }` | Type sizes in px. Titles shrink automatically on narrow widths. |
77
+ | `marks` | `{ barRadius, barMax, barMaxH, lineWidth, pointRadius, gap, ring, areaOpacity, bandOpacity, dotOpacity }` | Mark geometry. |
78
+ | `axes` | `{ inside, grid }` | `inside: true` puts y labels on their gridlines inside the plot (more width for data). |
79
+ | `layout` | `{ padding, paddingSmall }` | Outer margin on wide and narrow widths. |
80
+
81
+ ## Named palettes and ramps
82
+
83
+ | Palettes (`categorical`) | Ramps (`sequential` / `diverging`) |
84
+ |---|---|
85
+ | `cdia` (default), `cdia-noche`, `okabe-ito`, `tableau10`, `udec`, plus matplotlib's `tab10`, `tab20`, `tab20b`, `tab20c`, `Set1`, `Set2`, `Set3`, `Pastel1`, `Pastel2`, `Paired`, `Accent`, `Dark2` | `indigo`, `magenta`, `teal`, `amber`, `indigo-magenta`, `teal-rust`, plus every matplotlib colormap (see [Color scales](colorscales.md)) |
86
+
87
+ ## Validating a palette
88
+
89
+ ```js
90
+ import { validatePalette } from 'plotagents';
91
+
92
+ validatePalette(['#223C6A', '#E69B0A', '#D21428'], { mode: 'light', surface: '#FFFFFF', locale: 'en' });
93
+ // { ok: false, checks: [
94
+ // { name: 'lightness', status: 'fail', detail: 'Outside the lightness band 0.43–0.77: #223C6A' }, … ] }
95
+ ```
96
+
97
+ | Check | Threshold | Why |
98
+ |---|---|---|
99
+ | Lightness band | OKLCH L 0.43–0.77 (light), 0.48–0.67 (dark) | Too light disappears, too dark reads as text. |
100
+ | Chroma floor | OKLCH C ≥ 0.10 | Below it a hue reads as gray. |
101
+ | Colorblind separation | ΔE OKLab × 100 ≥ 8 between neighbors, simulated protanopia and deuteranopia (Machado 2009) | Neighbors must stay apart for color-vision deficiencies. |
102
+ | Normal-vision separation | ΔE ≥ 15 | Neighbors must be easy to tell apart for everyone. |
103
+ | Contrast with the surface | ≥ 3:1, warning only | Below it, marks need visible labels. |
104
+
105
+ Use `pairs: 'all'` for scatter plots and maps, where any two colors can sit next to each other. The built-in palette passes all-pairs for its first three colors, which is why `lint` warns about scatter plots with more than three groups.
106
+
107
+ `lint(spec)` runs this automatically whenever a spec brings its own palette. Messages follow `locale` (Spanish by default, `'en'` for English).
108
+
109
+ ## Color follows the entity
110
+
111
+ Colors are assigned to entities, never to positions:
112
+
113
+ - With `time`, colors are computed over all periods, so a group keeps its color while the slider moves even if its rank changes.
114
+ - With `focus` (drill-down), the subtree keeps its parent's color family.
115
+ - With `facet`, every panel shares one color map.
116
+ - `colors: { "Copper": "#B5562E" }` pins any entity to a color.