plotagents 0.5.0 → 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 (51) hide show
  1. package/AGENTS.md +24 -4
  2. package/README.md +26 -14
  3. package/bin/plotagents.js +38 -14
  4. package/dist/plotagents-geo.js +1 -1
  5. package/dist/plotagents.js +4294 -637
  6. package/docs/api.md +23 -5
  7. package/docs/charts.md +143 -9
  8. package/docs/colorscales.md +46 -0
  9. package/docs/dashboards.md +79 -0
  10. package/docs/getting-started.md +6 -2
  11. package/docs/themes.md +39 -9
  12. package/index.d.ts +188 -17
  13. package/package.json +9 -3
  14. package/schema.json +1025 -46
  15. package/src/axes.js +11 -3
  16. package/src/charts/bar.js +28 -17
  17. package/src/charts/categorical.js +31 -7
  18. package/src/charts/chord.js +3 -3
  19. package/src/charts/choropleth.js +284 -36
  20. package/src/charts/dist.js +37 -20
  21. package/src/charts/donut.js +1 -1
  22. package/src/charts/gantt.js +460 -0
  23. package/src/charts/heatmap.js +2 -2
  24. package/src/charts/kpi.js +9 -6
  25. package/src/charts/line.js +173 -41
  26. package/src/charts/network.js +552 -189
  27. package/src/charts/pointmap.js +599 -0
  28. package/src/charts/radar.js +7 -1
  29. package/src/charts/regplot.js +7 -1
  30. package/src/charts/sankey.js +41 -5
  31. package/src/charts/scatter.js +100 -22
  32. package/src/charts/sunburst.js +5 -4
  33. package/src/charts/treemap.js +10 -7
  34. package/src/charts/wordcloud.js +163 -0
  35. package/src/colorscales.js +182 -0
  36. package/src/dashboard.js +438 -0
  37. package/src/dashed.js +1 -1
  38. package/src/data.js +2 -2
  39. package/src/format.js +46 -14
  40. package/src/frame.js +95 -26
  41. package/src/geo/projection.js +74 -9
  42. package/src/geo/registry.js +87 -8
  43. package/src/index.js +76 -11
  44. package/src/infer.js +10 -1
  45. package/src/interact.js +157 -36
  46. package/src/lint.js +289 -9
  47. package/src/runtime.js +209 -97
  48. package/src/scale.js +80 -10
  49. package/src/schema.js +187 -31
  50. package/src/stats.js +71 -14
  51. package/src/theme.js +101 -14
package/docs/api.md CHANGED
@@ -3,7 +3,8 @@
3
3
  ```js
4
4
  import {
5
5
  plot, safePlot, lint, describe, render, toHTML, configure,
6
- schema, toolDefinition, validatePalette, registerGeo,
6
+ dashboard, safeDashboard, renderDashboard, lintDashboard, planDashboard,
7
+ schema, dashboardSchema, toolDefinition, validatePalette, registerGeo, countWords,
7
8
  } from 'plotagents';
8
9
  ```
9
10
 
@@ -39,7 +40,11 @@ const { ok, svg, type, description, errors, warnings, suggestions } = safePlot(s
39
40
 
40
41
  ### `lint(spec)`
41
42
 
42
- Reviews a spec without drawing it and returns `{ ok, type, errors, warnings, suggestions }`. Messages are written in Spanish; each one carries a `fix` with the change to make.
43
+ Reviews a spec without drawing it and returns `{ ok, type, errors, warnings, suggestions }`. Messages follow the spec's `locale` (Spanish by default, English with `locale: "en"`); each one carries a `fix` with the change to make.
44
+
45
+ ### `dashboard(spec)` · `safeDashboard(spec)` · `renderDashboard(el, spec)`
46
+
47
+ A self-contained HTML dashboard with best-practice layout, shared colors, KPIs and filters. `safeDashboard` never throws and returns `{ ok, html, plan, errors, warnings, suggestions }`; `renderDashboard` draws into an element in the browser; `lintDashboard` reviews; `planDashboard` returns the layout decisions. See [Dashboards](dashboards.md).
43
48
 
44
49
  ### `describe(spec)`
45
50
 
@@ -49,7 +54,7 @@ Returns a text description of the chart, such as "Highest: Biobío with 1,240 (3
49
54
 
50
55
  ### `render(target, spec, opts?)`
51
56
 
52
- Draws into an element (a CSS selector or a DOM node) with tooltips, responsive width, time sliders, a range slider for time series, drill-down for treemaps and sunbursts, and click-to-focus for networks.
57
+ Draws into an element (a CSS selector or a DOM node) with tooltips, responsive width, time controls when the spec asks for them (`slider: true`), drill-down for treemaps and sunbursts, click-to-focus for networks, collapsible Gantt phases, and legend clicks (shift + click excludes an entry; in treemaps and sunbursts a click zooms to the category).
53
58
 
54
59
  ```js
55
60
  const chart = plotagents.render('#chart', spec);
@@ -59,7 +64,7 @@ chart.destroy(); // remove listeners and markup
59
64
 
60
65
  | Option | Meaning |
61
66
  |---|---|
62
- | `controls` | `false` hides sliders and play buttons. |
67
+ | `controls` | `false` hides time controls even when the spec asks for them (`slider: true`). |
63
68
 
64
69
  The handle exposes `svg` (the current `<svg>` element) and `spec`.
65
70
 
@@ -89,7 +94,20 @@ configure(); // returns the current defaults
89
94
  configure(null); // clears them
90
95
  ```
91
96
 
92
- It accepts any spec option: `font`, `textScale`, `locale`, `theme`, `palette`, `ramp` and so on.
97
+ It accepts any spec option: `font`, `textScale`, `locale`, `theme`, `palette`, `ramp`, `colorScale`, `legend` and so on. A spec's own `theme` object is merged over the global one.
98
+
99
+ ### `registerTheme(name, theme)`
100
+
101
+ Registers a named theme (merged over `extends`, `papel` by default) so `theme: name` and `configure({ theme: name })` can use it. See [Themes](themes.md#your-own-theme-everywhere).
102
+
103
+ ```js
104
+ registerTheme('acme', { background: '#FFF', text: '#111', categorical: 'tab10', grid: { dash: '2 3' } });
105
+ configure({ theme: 'acme' });
106
+ ```
107
+
108
+ ### `colorScaleNames()` · `resolveColorScale(name)`
109
+
110
+ All color scale names grouped by kind (`sequential`, `diverging`, `cyclic`, `qualitative`), and the stops of one scale: `{ name, stops, kind }`. `interpolateColor(stops, t)` returns the color at `t ∈ [0, 1]` (OKLab).
93
111
 
94
112
  ### `validatePalette(colors, opts?)`
95
113
 
package/docs/charts.md CHANGED
@@ -21,12 +21,20 @@ Every chart takes a spec with `data` (rows) plus field names for each role. The
21
21
  | `xDomain`, `yDomain` | Fixed `[min, max]`. |
22
22
  | `refLines` | `[{ y: 600, label: "Target" }]` (or `x`, or `value` for bars). |
23
23
  | `facet` (`col`), `columns`, `sharey`, `sharex` | Small multiples, one panel per value. |
24
- | `time`, `timeValue`, `slider`, `play` | One period at a time with a slider in the browser; `slider: false` removes it, `play: false` hides only the play button. |
24
+ | `time`, `timeValue` | One period at a time: the latest by default, or `timeValue`. The period is appended to the subtitle. |
25
+ | `slider`, `play` | Browser time controls, **off by default**. `slider: true` adds period buttons when there are few periods (≤ 8) and a minimal slider when there are many; line and area charts get a two-handle range. `"buttons"` or `"slider"` force one; `play: false` hides the play button. |
25
26
  | `compare` | With `time`: compare against `"previous"` (default), `"first"`, a given period, or `false`. |
26
27
  | `theme`, `palette`, `ramp` | See [themes](themes.md). |
28
+ | `colorScale` (`cmap`) | Any matplotlib colormap (`"viridis"`, `"RdBu_r"`…) or your own list of colors. See [color scales](colorscales.md). |
27
29
  | `font`, `textScale` | Typeface (Google Fonts name, CSS stack or `{ family, import }`) and a text-size multiplier (aliases `fontScale`, `labelScale`). Set them for every chart with `configure()`. |
28
30
  | `width`, `height`, `padding` | Pixels. Height is automatic for horizontal bars, heatmaps, KPI tiles, and narrow treemaps, sunbursts and radars. |
29
- | `legend` | `false` hides the legend. |
31
+ | `legend` | Position: `"top"` (default), `"bottom"`, `"left"`, `"right"`, or `false` to hide it. Side legends move to the bottom below 520 px. Setting a position also forces a legend where the chart would otherwise label lines directly. |
32
+ | `exclude` | One name or a list: drop these legend entries (series, first-level groups) and recompute axes, shares and layout. Colors stay fixed and the excluded entries stay in the legend, faded and struck through. |
33
+
34
+ ### In the browser
35
+
36
+ - **Tooltips** sit next to the hovered mark, never over the title or subtitle, and stay inside the chart. Their header takes the mark's color, with white or dark text depending on contrast.
37
+ - **Legend**: hovering a key highlights its marks; a click pins the highlight. **Shift + click** excludes that entry (click it again to bring it back). In treemaps and sunbursts a plain click zooms the whole plot to that category; click it again, a breadcrumb, or press Esc to go back.
30
38
 
31
39
  ## Compare
32
40
 
@@ -62,7 +70,48 @@ Every chart takes a spec with `data` (rows) plus field names for each role. The
62
70
  | `points` | Markers on every point (automatic for short series). |
63
71
  | `stack` | Areas: `true` (default with series), `"percent"`, or `false` to overlap. |
64
72
  | `fill` | Single line: `false` removes the soft gradient. |
65
- | `xRange` | Visible range `[from, to]`; this is what the browser slider sets. |
73
+ | `xRange` | Visible range `[from, to]`; this is what the range slider sets (`slider: true`). |
74
+
75
+ ## Projects
76
+
77
+ ### `gantt` (`timeline`, `roadmap`, `schedule`, `cronograma`)
78
+
79
+ One row per task: `task`, `start` and `end` (or `duration`). Dates like `"2026-03-02"` or `"2026-03"`, or plain numbers for weeks or sprints. Day dates are **inclusive**: a task from March 3 to March 5 lasts 3 days. A row without `end`, or with `end` equal to `start`, is a milestone, drawn as a diamond with its date.
80
+
81
+ The time axis sits on top, like a calendar. With `group`, each phase opens with its name and a thin summary bar in its color, so no legend is needed. The filled part of each bar is the progress; the rest stays in a light tint of the same color. Dependencies are thin right-angle arrows behind the bars.
82
+
83
+ ```js
84
+ plot({
85
+ type: 'gantt',
86
+ title: 'The project goes live on June 15; the ERP integration is the critical path',
87
+ data: [
88
+ { phase: 'Design', task: 'Data model', start: '2026-02-09', end: '2026-03-06', progress: 80 },
89
+ { phase: 'Build', task: 'ERP integration', start: '2026-03-16', end: '2026-04-30', progress: 10, after: 'Data model' },
90
+ { phase: 'Launch', task: 'Go-live', start: '2026-06-15' },
91
+ ],
92
+ task: 'task', start: 'start', end: 'end', group: 'phase', progress: 'progress', dependsOn: 'after',
93
+ today: '2026-03-12', locale: 'en',
94
+ });
95
+ ```
96
+
97
+ | Option | Meaning |
98
+ |---|---|
99
+ | `task`, `start`, `end` | Field names (detected when they are called `task`/`tarea`, `start`/`inicio`, `end`/`fin`). |
100
+ | `duration` | Length instead of `end`: days on a date axis, units on a numeric one. |
101
+ | `group` (`series`, `hue`) | Phase or team: one colored section per value. `groupHeaders: false` uses a color legend instead. |
102
+ | `progress` | 0–1 or 0–100. Shown as the filled part, with the percentage inside the bar when it fits. |
103
+ | `dependsOn` | Field with the predecessor's name, or several: `"A, B"`. |
104
+ | `milestone` | Boolean field that forces milestones. |
105
+ | `today` | A date (or `true`) for the "today" line, labeled on the axis. `refLines: [{ x, label }]` adds more. |
106
+ | `dashed`, `dashedLabel` | Hatch tentative tasks: a field (`dashed: "tentative"`), names or phases. |
107
+ | `highlight` | Emphasize a task or a whole phase. |
108
+ | `sort` | `"start"` orders tasks by date (default: data order, grouped by phase). |
109
+ | `rowHeight`, `xLabel`, `unit` | Row height in px; axis title and unit for numeric axes. |
110
+ | `collapsed`, `collapsible` | Phases drawn as one summary row (duration-weighted progress, milestone diamonds): a list of names or `true`. In the browser a click on a phase toggles it (Alt + click toggles all); the time axis and name column stay put. `collapsible: false` hides the chevrons. |
111
+ | `critical`, `criticalColor` | Outline the critical path: tasks with zero slack along `dependsOn` (working days on day dates), with accent arrows. |
112
+ | `weekends` | Weekend bands on day-date plans of up to 120 days; `false` turns them off. |
113
+
114
+ `lint` warns about tasks that end before they start and suggests a `today` line. The alt text names the span, the longest task, the milestones and the overall progress weighted by duration.
66
115
 
67
116
  ## Relationships
68
117
 
@@ -128,7 +177,8 @@ Long data (`axis`, `value`, `series`) or wide data (`axes: [fields]`, one row pe
128
177
  | Option | Meaning |
129
178
  |---|---|
130
179
  | `depth` | Show only the first N levels. |
131
- | `focus` | Path to drill into, e.g. `["Mining"]`. In the browser, clicking a group does it and breadcrumbs go back. |
180
+ | `focus` | Path to drill into, e.g. `["Mining"]`. In the browser, clicking a group or its legend key does it (the category fills the whole plot) and breadcrumbs or Esc go back. |
181
+ | `exclude` | Level-1 groups to leave out; the rest fill the plane and shares are recomputed. Shift + click on a legend key does it in the browser. |
132
182
  | `showValues` | Append values to the labels. |
133
183
  | `colorBy` | `"change"` colors tiles by % change vs the comparison period (growth cool, decline warm); `"label"` colors a single-level treemap by item. |
134
184
  | `shade` | Subtle lightness steps inside each category. Off by default: one solid color per category. |
@@ -157,26 +207,74 @@ Age groups (`age` or `y`), two sides (`sex`, `series` or `hue`) and a value. Bac
157
207
 
158
208
  ### `choropleth` (`map`) · `symbolmap` (`bubblemap`)
159
209
 
160
- Chilean regions (`geo: "chile"`, default), US states (`geo: "usa"`) and world countries (`geo: "world"`) ship with the library; any GeoJSON FeatureCollection works too. `key` is the data field to match and `value` the measure. Matching is forgiving: names, codes and aliases, ignoring accents, case and "Región de" ("RM", "Biobio", "XIII", "Aisén", "ARG", "EE.UU.", "TX", "Nueva York", "48").
210
+ Chilean regions (`geo: "chile"`, default), US states (`geo: "usa"`) and world countries (`geo: "world"`) ship with the library, and **any geometry of your own** works too (see below). `key` is the data field to match and `value` the measure; a text `value` (a party, a language, a zone) colors by category with a legend of swatches. Matching is forgiving: names, codes and aliases, ignoring accents, case and "Región de" ("RM", "Biobio", "XIII", "Aisén", "ARG", "EE.UU.", "TX", "Nueva York", "48").
161
211
 
162
212
  **United States.** 50 states and DC in an Albers equal-area projection (`albersUsa`, the same composite as `d3.geoAlbersUsa`): Alaska (at 0.35×) and Hawaii sit in insets at the bottom left with a fine dashed frame; Puerto Rico gets its own inset only when the data includes it (`territories: true` forces it). Each state shows its postal code, plus the value when it fits; the small northeastern states (VT, NH, MA, RI, CT, NJ, DE, MD, DC) are labeled in a column on the right with leader lines. `values: false` keeps only the codes; `labels: false` removes them.
163
213
 
164
214
  | Option | Meaning |
165
215
  |---|---|
166
- | `geo` | `"chile"`, `"usa"`, `"world"`, a name registered with `registerGeo()`, or a GeoJSON object. |
167
- | `featureKey` | Feature property to match (default: tries code, id, ISO-3, name and aliases). |
216
+ | `geo` | `"chile"`, `"usa"`, `"world"`, a name registered with `registerGeo()`, a GeoJSON or TopoJSON object, or (browser) a URL. |
217
+ | `featureKey`, `nameKey` | Feature property to match, and the one with the display name (both detected when omitted). |
218
+ | `fitTo` | Zoom to regions, a `[lonMin, latMin, lonMax, latMax]` box or `"markers"`. |
219
+ | `markers`, `layers` | Points by lon/lat on top; extra GeoJSON lines, polygons or points over or under the map. |
168
220
  | `scale`, `classes` | `"quantile"` (default), `"equal"`, `"continuous"` or a list of breaks; 5 classes by default. |
169
221
  | `midpoint` / `diverging` | Diverging classes around a value (e.g. 0 for changes). |
170
222
  | `missing` | `"plain"` drops the hatch and the "No data" legend entry. |
171
223
  | `labels`, `bars` | Chile gets a north-to-south column with name, bar and value; `bars: false` hides the bars. World maps label the top N. |
172
- | `projection` | `"mercator"`, `"equalEarth"` or `"albersUsa"` (picked automatically). |
224
+ | `projection` | `"mercator"`, `"equalEarth"`, `"albersUsa"`, `"equirectangular"`, `"conic"`, `"identity"`, `"planar"` or `{ type, parallels, rotate }` (picked automatically). |
225
+ | `borderColor`, `borderWidth` | Region borders. |
173
226
  | `dashed`, `dashedLabel` | Hatch some regions (preliminary figures, disputed areas): names, codes or a data field. They keep their class color. |
174
227
  | `maxRadius` | `symbolmap`: largest circle radius. |
175
228
 
229
+ #### Your own geometry: GeoJSON and TopoJSON
230
+
231
+ `geo` takes whatever you have: a GeoJSON `FeatureCollection`, a single `Feature`, an array of features or a bare geometry, or a TopoJSON `Topology` (`geoObject` picks the object; default the first). Polygons are filled, lines are stroked and points become dots colored by value. In the browser, `render()` also accepts a URL and downloads it once: `geo: "/maps/comunas.topojson"`. In Node, read the file and pass the object.
232
+
233
+ ```js
234
+ import { readFileSync } from 'node:fs';
235
+ const comunas = JSON.parse(readFileSync('comunas-biobio.geojson', 'utf8'));
236
+ plot({
237
+ type: 'choropleth',
238
+ geo: comunas, // properties: { CUT_COM: "08101", NOM_COM: "Concepción", … }
239
+ data: [{ comuna: 'Concepción', poverty: 7.1 }, { comuna: 'Coronel', poverty: 12.4 }],
240
+ key: 'comuna', value: 'poverty', // NOM_COM is found automatically
241
+ markers: { data: hospitals, lon: 'lon', lat: 'lat', label: 'name', legend: 'Hospital' },
242
+ layers: [{ geo: rivers, label: 'Biobío river', stroke: '#5BA3E0', width: 2 }],
243
+ fitTo: ['Concepción', 'Talcahuano', 'Hualpén'],
244
+ });
245
+ ```
246
+
247
+ - **Matching without configuration.** Rows are matched against the usual keys (code, id, ISO-3, name, aliases); if some rows don't match, plotagents finds the feature property whose values do (`NOM_COM`, `CUT_REG`, `GEOID`, `ADMIN`…). `featureKey` forces one. Display names come from `nameKey`, or the first of `name`, `NOMBRE`, `NAME_1`, `ADMIN` and similar.
248
+ - **Projections.** Picked from the geometry: Mercator for a country or region, Equal Earth for the world, and a plain cartesian plane when coordinates are not longitudes and latitudes (UTM metres, for instance). Set `projection` to `"equirectangular"`, `"conic"` (or `{ type: "conic", parallels: [-30, -45], rotate: 72 }`), `"identity"` (pixel coordinates, y down, for hand-drawn or tile maps) or `"planar"` (y up).
249
+ - **Framing.** `fitTo` zooms to some regions, a box `[lonMin, latMin, lonMax, latMax]` or `"markers"`; the rest of the map stays as clipped context. `tall: false` turns off the side column for elongated shapes.
250
+ - **Markers.** `markers: [{ lon, lat, label, value }]` or `{ data, lon, lat, label, size, color, legend, maxRadius }`: dots on top, area-proportional when there is a `size`, labels placed where they don't collide.
251
+ - **Layers.** `layers: [{ geo, label, stroke, width, fill, dash, opacity, below }]` overlays rivers, roads, routes, faults or district lines (or draws them under the regions with `below: true`). A `label` adds a legend key.
252
+ - **Style.** `borderColor`, `borderWidth`, `pointRadius`, `lineWidth`, `ramp` (a named or custom sequential ramp), `palette` and `colors` (categories).
253
+ - **Register once, use by name.** `registerGeo('biobio', comunas, ['comunas-biobio'])` and then `geo: 'biobio'` in every spec, which keeps tool calls short for an agent.
254
+
176
255
  In the browser (`render()` or `toHTML()`), maps are interactive: hovering outlines the shape; clicking a region zooms to fit it (click again, double-click, Esc or the reset button to go back); Ctrl/⌘ + scroll zooms around the pointer (plain scroll keeps scrolling the page, as `chartjs-plugin-zoom` recommends with its `modifierKey`); pinch zooms on touch screens; drag pans when zoomed; +/− buttons work from the keyboard. Each color class in the legend is a key: hover it to light up its regions, click to pin. With `time`, every region's color transitions into the next period's instead of jumping, and the zoom is kept while the slider plays.
177
256
 
178
257
  `lint` warns about rows that match no region or country. Sources: Natural Earth 1:110m (public domain) for the world; Biblioteca del Congreso Nacional for Chilean regions (attribution in `src/geo/data/chile-regiones.js`); U.S. Census Bureau cartographic boundaries 1:10m (public domain) via us-atlas 3.0.1 for US states. In the browser, `dist/plotagents-geo.js` adds the geometries; `toHTML()` includes it only when a page has a map.
179
258
 
259
+ ### `pointmap` (`dotmap`, `tilemap`)
260
+
261
+ Dots at longitude/latitude over a real basemap in Web Mercator. Fields `lon`/`lat` are found by name (lon, lng, longitude, longitud / lat, latitude, latitud). `size` → circle area (key in the corner, largest labeled); `hue` → category colors with a legend; numeric `value` → sequential ramp (`midpoint` for diverging, `colorScale` for any matplotlib map). Zoom and center fit the points (`zoom`, `center: [lon, lat]` to fix them).
262
+
263
+ ```js
264
+ plot({ type: 'pointmap', data: cities, lon: 'lon', lat: 'lat', size: 'population', label: 'city',
265
+ title: 'Santiago concentrates 40% of the population', source: 'INE' });
266
+ ```
267
+
268
+ | Option | Meaning |
269
+ |---|---|
270
+ | `tiles` | `"osm"` (default, OpenStreetMap), `"esri-gray"`, `"esri-dark"`, `"carto-light"` / `"carto-dark"` (free key: carto.com/basemaps/apikey), `"mapbox-light"` / `"mapbox-dark"` (public `pk.` token in `tilesKey`; `sk.` tokens are refused), `{ url, attribution, subdomains, maxZoom, retina }`, or `false` (vector world outline only). |
271
+ | `tilesKey` | Key or token for CARTO, Mapbox or Esri. |
272
+ | `tileStyle` | `"muted"` (default) desaturates and tints the tiles to the theme (a navy night map on `noche`); `"original"` keeps them as they are. |
273
+ | `tileOpacity`, `zoom`, `center` | Tile opacity, fixed zoom level and `[lon, lat]` center. |
274
+ | `labels`, `labelValues` | How many dots to name (8 largest by default), or a list; `labelValues: false` hides values. |
275
+
276
+ The attribution is always drawn. Tiles are external images: they show in inline SVG, `render()` and `toHTML()`, **not** when the SVG is used as `<img>`, opened from a PDF converter, or offline; then the vector world outline underneath still gives context. `await embedTiles(svg)` inlines them as data: URIs (not for OSM, whose tile policy forbids bulk downloads; use `esri-gray` or your own server). For heavy traffic, use a provider with a key or your own tiles: OSM's servers are donated.
277
+
180
278
  ## Dashed shapes: estimates, projections, provisional figures
181
279
 
182
280
  Some numbers are not firm yet. Instead of a new color (which reads as a new category), plotagents keeps the shape's color and changes its texture: a hatched fill (stripes of the color over a light tint of it) and a dashed outline at 3:1 contrast. Lines get a dashed stroke with hollow points. It works in `bar`, `line`, `area`, `donut`/`pie`, `choropleth` and `symbolmap`.
@@ -193,7 +291,21 @@ This follows the Chart.js advice of using patterns (as `patternomaly` does) and
193
291
 
194
292
  ### `network` (`graph`)
195
293
 
196
- Edges in `data` (`from`, `to`, optional `value`), optional `nodes` with `id`, `group` and `size`. Deterministic force layout fitted to the whole area, or `layout: "circle"`. `labels` (number or list), `highlight` (a node and its neighbors), `focus` (a node id: it is pinned at the center and highlighted with its neighbors, all of them labeled; in the browser a click does it with a smooth transition, and a second click or Esc returns), `iterations`, `maxRadius`.
294
+ Edges in `data` (`from`, `to`, optional `value`), optional `nodes` with `id`, `group` and `size`. Node area ∝ weight (weighted degree by default), color by group; edges curve in the color of their source node (`curveness`, default 0.3; `0` = straight), 1 px wide without a value field. The largest nodes are named (ink with a halo, never overlapping each other or a node); every name is in the tooltip. Hovering a node lights up its neighbors and their edges.
295
+
296
+ `layout` picks the placement (all deterministic, all fitted to the area):
297
+
298
+ | `layout` | Placement |
299
+ |---|---|
300
+ | `force` (default) | Force-directed (Barnes–Hut above 150 nodes). |
301
+ | `circular` (`circle`) | Around a circle by group, a gap between groups, names rotated outward. |
302
+ | `radial` | A tree from `focus` (or the most connected node); each ring is one step further. Links outside the tree are faint. |
303
+ | `concentric` | Rings by weight, the largest in the centre, by group within each ring. |
304
+ | `clusters` | One sunflower cluster per group, space ∝ √size. |
305
+ | `tree` | The radial tree laid flat (left → right on wide areas), S-shaped connectors. |
306
+ | `grid` | A grid ordered by group and weight, every node named. |
307
+
308
+ `labels` (number, list or `true`), `highlight` (a node and its neighbors), `focus` (a node id: it goes to the center, or the root in `radial`/`tree`, and is highlighted with its neighbors, all labeled; in the browser a click does it with a smooth transition, and a second click or Esc returns), `iterations`, `maxRadius`, `curveness`.
197
309
 
198
310
  ### `chord`
199
311
 
@@ -213,6 +325,28 @@ Edges in `data` (`from`, `to`, optional `value`), optional `nodes` with `id`, `g
213
325
 
214
326
  `from`, `to`, `value` (also detects `source`/`target`). Color starts at the first stage with more than one node and flows downstream.
215
327
 
328
+ ### `wordcloud` (`words`, `tagcloud`, `nube`)
329
+
330
+ Words sized by frequency, done carefully: every word horizontal; each word's **area** proportional to its count (font size grows with the square root); a deterministic spiral layout from the most frequent word outwards, with no collisions; one hue that darkens with frequency, or one color per `series` with a legend. If the important words don't fit, the whole cloud shrinks; the words that still don't fit are named in the alt text, never cut off.
331
+
332
+ ```js
333
+ plot({ type: 'wordcloud', data, text: 'word', value: 'mentions', title: '“Data” dominates what companies ask for' });
334
+ plot({ type: 'wordcloud', text: openAnswers, title: '…', subtitle: 'Mentions in 312 open answers' }); // raw text
335
+ ```
336
+
337
+ | Option | Meaning |
338
+ |---|---|
339
+ | `text` | Field with the words, or the raw text to count (a string or a list of strings). Raw text drops Spanish and English stopwords and numbers. |
340
+ | `textField` | A field of free text in each row (open survey answers): words are counted across rows. |
341
+ | `value` | Weight of each word (default: 1 per row, summed). |
342
+ | `series` | Color by group, with a legend. |
343
+ | `top` | Most words to show (default 80). |
344
+ | `stopwords`, `minLength` | Extra words to ignore; shortest word kept (default 3). |
345
+ | `minFont`, `maxFont` | Font size range. |
346
+ | `highlight` | Words to emphasize; the rest are muted. |
347
+
348
+ A cloud is good at "what comes up most" and bad at "by how much". `lint` suggests a bar chart (`top: 15`) when there are few words, and asks for a `subtitle` that says what was counted.
349
+
216
350
  ### `kpi`
217
351
 
218
352
  `items: [{ label, value, delta, previous, period, good: "up" | "down", trend: [...], target, format }]`. Delta is a fraction (`0.12` → +12%); `good: "down"` for costs. Tiles reflow into balanced rows.
@@ -0,0 +1,46 @@
1
+ # Color scales
2
+
3
+ Every colormap that ships with matplotlib is available by name, plus your own. Use them wherever a chart maps a number to a color: heatmaps, choropleths, point maps, treemaps colored by change and word clouds.
4
+
5
+ ```js
6
+ plot({ type: 'heatmap', data, x: 'hour', y: 'day', value: 'trips', colorScale: 'viridis' });
7
+ plot({ type: 'choropleth', geo: 'chile', data, key: 'region', value: 'change', colorScale: 'RdBu' });
8
+ plot({ ...spec, colorScale: 'magma_r' }); // _r reverses any map
9
+ plot({ ...spec, colorScale: ['#FFF7EC', '#FC8D59', '#7F0000'] }); // your own stops
10
+ plot({ ...spec, colorScale: { name: 'coolwarm', mid: 100 } }); // diverging around 100
11
+ ```
12
+
13
+ - **Names are case-insensitive** (`'rdbu'` works) and `_r` reverses any map.
14
+ - **Diverging maps center themselves**: on 0 when the data crosses zero, otherwise on the middle of the data. Set `mid` (or `midpoint`) to choose the center.
15
+ - **Your own scale** is a list of two or more colors, evenly spaced from low to high, or `{ colors, reverse, mid }`.
16
+ - Interpolation happens in OKLab, so steps look even. The stops were sampled from matplotlib 3.9 and stay within 2 ΔE (×100) of the original.
17
+ - `cmap` and `colormap` are aliases of `colorScale`. The older `ramp` option still works.
18
+
19
+ ## Categorical palettes
20
+
21
+ `palette` takes a list of colors, a plotagents palette, or one of matplotlib's qualitative maps:
22
+
23
+ ```js
24
+ plot({ ...spec, palette: 'tab10' });
25
+ plot({ ...spec, palette: 'Set2' });
26
+ plot({ ...spec, palette: 'viridis' }); // a continuous map, sampled into distinct colors
27
+ ```
28
+
29
+ The default palette is validated for color-vision deficiencies; `lint` checks any other palette you bring (see [Themes](themes.md#validating-a-palette)).
30
+
31
+ ## Which one?
32
+
33
+ | Data | Use | Avoid |
34
+ |---|---|---|
35
+ | Ordered values, low → high | `viridis`, `cividis`, `magma`, `Blues`, `YlGnBu` | `jet`, `rainbow`, `hsv` (they invent edges where the data has none) |
36
+ | Values around a meaningful center (0, a target, an average) | `RdBu`, `coolwarm`, `PuOr`, `BrBG` | a sequential map |
37
+ | Angles, hours of the day, months | `twilight`, `hsv` | a sequential map |
38
+ | Categories | `palette: 'tab10'`, `'Set2'`, `'Dark2'` | any continuous map as `colorScale` |
39
+
40
+ `lint` flags the rainbow maps with a suggestion, and refuses a qualitative map used as a continuous scale.
41
+
42
+ ## All scales
43
+
44
+ {{colorscales}}
45
+
46
+ The list is also available in code: `colorScaleNames()` returns the names grouped by kind, and `resolveColorScale(name)` returns `{ name, stops, kind }`.
@@ -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.
@@ -5,10 +5,14 @@ plotagents turns one JSON object into a finished, accessible SVG chart. You pass
5
5
  ## Install
6
6
 
7
7
  ```bash
8
- npm install github:carlosnavarreteliz/plotagents
8
+ npm install plotagents
9
9
  ```
10
10
 
11
- plotagents is not on the npm registry yet, so install it from GitHub for now. Once it is published, `npm install plotagents` will work too. After installing, `npx plotagents` runs the CLI from your project.
11
+ Or straight from GitHub, for the latest unreleased changes:
12
+
13
+ ```bash
14
+ npm install github:carlosnavarreteliz/plotagents
15
+ ```
12
16
 
13
17
  Node 18 or later, Deno and Bun work out of the box. The package has no dependencies.
14
18
 
package/docs/themes.md CHANGED
@@ -15,7 +15,34 @@ plot({ ...spec, ramp: 'teal' }); // shortcut for them
15
15
  configure({ font: 'Poppins', textScale: 1.4, theme: { extends: 'papel', marks: { lineWidth: 2.5 } } });
16
16
  ```
17
17
 
18
- Every later `plot()`, `render()` and `toHTML()` starts from these values; the spec always wins. `configure()` returns the current defaults and `configure(null)` clears them.
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.
19
46
 
20
47
  ## Built-in themes
21
48
 
@@ -30,17 +57,20 @@ Both share the same eight hues, stepped separately for each surface and validate
30
57
 
31
58
  | Key | Type | Meaning |
32
59
  |---|---|---|
33
- | `extends` | `'papel' \| 'noche'` | Base to merge over (default `papel`, or `noche` when `mode: 'dark'`). |
60
+ | `extends` | theme name or object | Base to merge over (default `papel`, or `noche` when `mode: 'dark'`). |
34
61
  | `mode` | `'light' \| 'dark'` | Affects tooltips and text-on-fill contrast. |
35
- | `surface` | color | Chart background. Also used for 2 px gaps and rings around marks. |
36
- | `ink`, `lede`, `muted` | color | Primary text, secondary text, axis and source text. |
37
- | `grid`, `axis` | color | Gridlines and baselines (1 px, solid). |
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. |
38
68
  | `tint`, `tint2` | color | Quiet fills: KPI tiles, slider tracks, meters. |
39
69
  | `dim` | color | Muted marks when `highlight` is used. |
40
70
  | `other` | color | The "Other" bucket and series past the 8th. |
41
- | `categorical` | colors or palette name | Identity colors, assigned in this order. |
42
- | `sequential` | colors or ramp name | Light → dark, for heatmaps. |
43
- | `diverging` | colors or ramp name | Two poles with a neutral midpoint. |
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. |
44
74
  | `status` | `{ good, warning, critical }` | KPI deltas and meters. Never used for series. |
45
75
  | `font` | `{ family, mono, import }` | Font stack and optional Google Fonts URL (`fontImport: true` embeds it in the SVG). |
46
76
  | `type` | `{ title, subtitle, label, axis, legend, source, titleWeight }` | Type sizes in px. Titles shrink automatically on narrow widths. |
@@ -52,7 +82,7 @@ Both share the same eight hues, stepped separately for each surface and validate
52
82
 
53
83
  | Palettes (`categorical`) | Ramps (`sequential` / `diverging`) |
54
84
  |---|---|
55
- | `cdia` (default), `cdia-noche`, `okabe-ito`, `tableau10`, `udec` | `indigo`, `magenta`, `teal`, `amber`, `indigo-magenta`, `teal-rust` |
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)) |
56
86
 
57
87
  ## Validating a palette
58
88