plotagents 0.5.0 → 0.8.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/AGENTS.md +29 -5
- package/README.md +44 -15
- package/bin/plotagents.js +40 -14
- package/dist/plotagents-geo.js +1 -1
- package/dist/plotagents.js +6161 -757
- package/docs/api.md +23 -5
- package/docs/charts.md +229 -10
- package/docs/colorscales.md +46 -0
- package/docs/dashboards.md +79 -0
- package/docs/es/agents.md +102 -0
- package/docs/es/api.md +172 -0
- package/docs/es/charts.md +436 -0
- package/docs/es/colorscales.md +46 -0
- package/docs/es/dashboards.md +79 -0
- package/docs/es/getting-started.md +142 -0
- package/docs/es/rendering.md +47 -0
- package/docs/es/themes.md +116 -0
- package/docs/getting-started.md +6 -2
- package/docs/themes.md +39 -9
- package/index.d.ts +275 -20
- package/package.json +11 -4
- package/schema.json +1382 -53
- package/skills/plotagents/SKILL.md +105 -0
- package/src/axes.js +11 -3
- package/src/charts/bar.js +40 -21
- package/src/charts/categorical.js +31 -7
- package/src/charts/chord.js +6 -4
- package/src/charts/choropleth.js +287 -37
- package/src/charts/corr.js +512 -0
- package/src/charts/dist.js +39 -21
- package/src/charts/donut.js +4 -2
- package/src/charts/gantt.js +460 -0
- package/src/charts/heatmap.js +2 -2
- package/src/charts/kpi.js +11 -8
- package/src/charts/line.js +187 -43
- package/src/charts/network.js +557 -189
- package/src/charts/pointmap.js +602 -0
- package/src/charts/radar.js +7 -1
- package/src/charts/regplot.js +16 -4
- package/src/charts/sankey.js +42 -6
- package/src/charts/scatter.js +103 -23
- package/src/charts/sunburst.js +7 -6
- package/src/charts/table.js +882 -0
- package/src/charts/treemap.js +12 -9
- package/src/charts/wordcloud.js +177 -0
- package/src/colorscales.js +193 -0
- package/src/dashboard.js +451 -0
- package/src/dashed.js +1 -1
- package/src/data.js +4 -2
- package/src/facet.js +4 -3
- package/src/format.js +80 -25
- package/src/frame.js +101 -29
- package/src/geo/projection.js +76 -9
- package/src/geo/registry.js +87 -8
- package/src/hierarchy.js +6 -0
- package/src/i18n.js +107 -0
- package/src/index.js +92 -14
- package/src/infer.js +18 -1
- package/src/interact.js +157 -36
- package/src/lint.js +353 -11
- package/src/runtime.js +301 -101
- package/src/scale.js +82 -11
- package/src/schema.js +235 -36
- package/src/stats.js +76 -17
- package/src/svg.js +4 -1
- package/src/text.js +5 -0
- package/src/theme.js +101 -14
package/src/scale.js
CHANGED
|
@@ -10,21 +10,37 @@ export function niceStep(span, count = 5) {
|
|
|
10
10
|
return f * mag;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
export
|
|
13
|
+
/** Paso entero (1, 2, 5, 10, 20, 25…): nunca < 1 ni 2,5. */
|
|
14
|
+
export const intStep = (step) => (!(step >= 1) ? 1 : Math.abs(step - 2.5) < 1e-9 ? 2 : step);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Dominio "bonito": extremos redondeados al paso de las marcas.
|
|
18
|
+
* `integer: true` (valores enteros, como conteos) garantiza paso ≥ 1 y sin 2,5
|
|
19
|
+
* (como `minInterval: 1` de ECharts): nunca marcas en 0,5.
|
|
20
|
+
*/
|
|
21
|
+
export function niceDomain(min, max, count = 5, { integer = false } = {}) {
|
|
22
|
+
const st = (span) => (integer ? intStep(niceStep(span, count)) : niceStep(span, count));
|
|
23
|
+
// Sin valores finitos (todo nulo o texto): dominio [0, 1] en vez de un bucle infinito.
|
|
24
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
25
|
+
if (Number.isFinite(min)) max = min;
|
|
26
|
+
else if (Number.isFinite(max)) min = max;
|
|
27
|
+
else return integer ? { min: 0, max: 1, step: 1 } : { min: 0, max: 1, step: 0.2 };
|
|
28
|
+
}
|
|
29
|
+
if (min > max) [min, max] = [max, min];
|
|
15
30
|
if (min === max) {
|
|
16
|
-
if (min === 0) return { min: 0, max: 1, step: 0.2 };
|
|
17
|
-
const pad = Math.abs(min) * 0.1;
|
|
31
|
+
if (min === 0) return integer ? { min: 0, max: 1, step: 1 } : { min: 0, max: 1, step: 0.2 };
|
|
32
|
+
const pad = integer ? 0.5 : Math.abs(min) * 0.1;
|
|
18
33
|
min -= pad;
|
|
19
34
|
max += pad;
|
|
20
35
|
}
|
|
21
|
-
let step =
|
|
36
|
+
let step = st(max - min);
|
|
22
37
|
let lo = Math.floor(min / step + 1e-9) * step;
|
|
23
38
|
let hi = Math.ceil(max / step - 1e-9) * step;
|
|
24
|
-
step =
|
|
39
|
+
step = st(hi - lo);
|
|
25
40
|
lo = Math.floor(min / step + 1e-9) * step;
|
|
26
41
|
hi = Math.ceil(max / step - 1e-9) * step;
|
|
27
|
-
|
|
42
|
+
// Sin ruido de punto flotante (0,30000000000000004 → 0,3).
|
|
43
|
+
return { min: +lo.toPrecision(15), max: +hi.toPrecision(15), step: +step.toPrecision(15) };
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
export function linear(d0, d1, r0, r1) {
|
|
@@ -35,9 +51,14 @@ export function linear(d0, d1, r0, r1) {
|
|
|
35
51
|
s.range = [r0, r1];
|
|
36
52
|
s.ticks = (step) => {
|
|
37
53
|
const out = [];
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
54
|
+
if (!Number.isFinite(d0) || !Number.isFinite(d1)) return out;
|
|
55
|
+
let st = step > 0 && Number.isFinite(step) ? step : niceStep(Math.abs(d1 - d0), 5);
|
|
56
|
+
const lo = Math.min(d0, d1), hi = Math.max(d0, d1);
|
|
57
|
+
// Nunca más de ~1000 marcas (paso diminuto frente al dominio, o paso que no avanza).
|
|
58
|
+
if ((hi - lo) / st > 1000) st = niceStep(hi - lo, 5);
|
|
59
|
+
const start = Math.ceil(lo / st - 1e-9) * st;
|
|
60
|
+
if (start + st === start) return [lo];
|
|
61
|
+
for (let i = 0, v = start; v <= hi + st * 1e-9 && i <= 1000; i++, v = start + i * st) out.push(Math.abs(v) < st * 1e-9 ? 0 : +v.toPrecision(15));
|
|
41
62
|
return out;
|
|
42
63
|
};
|
|
43
64
|
return s;
|
|
@@ -53,6 +74,7 @@ export function log(d0, d1, r0, r1) {
|
|
|
53
74
|
s.range = [r0, r1];
|
|
54
75
|
// Marcas 1-2-5 cuando hay pocas décadas; solo potencias de 10 si hay muchas.
|
|
55
76
|
s.ticks = (max = 7) => {
|
|
77
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [];
|
|
56
78
|
const cap = Number.isInteger(max) && max >= 2 ? max : 7;
|
|
57
79
|
const gen = (mult) => {
|
|
58
80
|
const out = [];
|
|
@@ -71,6 +93,15 @@ export function log(d0, d1, r0, r1) {
|
|
|
71
93
|
const every = Math.ceil(t.length / cap);
|
|
72
94
|
return t.filter((_, i) => i % every === 0);
|
|
73
95
|
};
|
|
96
|
+
// Rango estrecho (120–180 → solo "100, 200"): marcas lineales dentro del dominio.
|
|
97
|
+
const logTicks = s.ticks;
|
|
98
|
+
s.ticks = (max = 7) => {
|
|
99
|
+
const t = logTicks(max);
|
|
100
|
+
if (t.length >= 3 || !(d1 > d0)) return t;
|
|
101
|
+
const st = niceStep(d1 - d0, 5);
|
|
102
|
+
const lin = linear(d0, d1, 0, 1).ticks(st).filter((v) => v > 0);
|
|
103
|
+
return lin.length > t.length ? lin : t;
|
|
104
|
+
};
|
|
74
105
|
return s;
|
|
75
106
|
}
|
|
76
107
|
|
|
@@ -79,6 +110,8 @@ export function logDomain(min, max) {
|
|
|
79
110
|
const steps = [1, 2, 5, 10];
|
|
80
111
|
const down = (v) => { const e = Math.floor(Math.log10(v)); const m = v / 10 ** e; const k = [...steps].reverse().find((s) => s <= m + 1e-9) || 1; return k * 10 ** e; };
|
|
81
112
|
const up = (v) => { const e = Math.floor(Math.log10(v)); const m = v / 10 ** e; const k = steps.find((s) => s >= m - 1e-9) || 10; return k * 10 ** e; };
|
|
113
|
+
if (!Number.isFinite(min) || !(min > 0)) min = Number.isFinite(max) && max > 0 ? max / 10 : 1;
|
|
114
|
+
if (!Number.isFinite(max) || !(max > 0)) max = min * 10;
|
|
82
115
|
const lo = down(Math.max(min, 1e-12)), hi = up(Math.max(max, 1e-12));
|
|
83
116
|
return { min: lo, max: hi === lo ? lo * 10 : hi };
|
|
84
117
|
}
|
|
@@ -101,9 +134,19 @@ export function band(domain, r0, r1, { inner = 0.3, outer = 0.15 } = {}) {
|
|
|
101
134
|
// ---------------------------------------------------------------------------
|
|
102
135
|
// Tiempo
|
|
103
136
|
|
|
104
|
-
const DAY = 864e5;
|
|
137
|
+
const MIN = 6e4, HOUR = 36e5, DAY = 864e5;
|
|
105
138
|
const UNITS = [
|
|
139
|
+
['minute', 1, MIN],
|
|
140
|
+
['minute', 5, 5 * MIN],
|
|
141
|
+
['minute', 15, 15 * MIN],
|
|
142
|
+
['minute', 30, 30 * MIN],
|
|
143
|
+
['hour', 1, HOUR],
|
|
144
|
+
['hour', 3, 3 * HOUR],
|
|
145
|
+
['hour', 6, 6 * HOUR],
|
|
146
|
+
['hour', 12, 12 * HOUR],
|
|
106
147
|
['day', 1, DAY],
|
|
148
|
+
['day', 2, 2 * DAY],
|
|
149
|
+
['day', 3, 3 * DAY],
|
|
107
150
|
['day', 7, 7 * DAY],
|
|
108
151
|
['month', 1, 30.44 * DAY],
|
|
109
152
|
['month', 3, 91.3 * DAY],
|
|
@@ -114,10 +157,14 @@ const UNITS = [
|
|
|
114
157
|
['year', 10, 3652 * DAY],
|
|
115
158
|
['year', 25, 9131 * DAY],
|
|
116
159
|
['year', 50, 18262 * DAY],
|
|
160
|
+
['year', 100, 36525 * DAY],
|
|
161
|
+
['year', 250, 91312 * DAY],
|
|
162
|
+
['year', 500, 182625 * DAY],
|
|
117
163
|
];
|
|
118
164
|
|
|
119
165
|
/** Marcas de tiempo legibles: elige la unidad que da ≤ `count` marcas. */
|
|
120
166
|
export function timeTicks(t0, t1, count = 6, locale = 'es') {
|
|
167
|
+
if (!Number.isFinite(t0) || !Number.isFinite(t1)) return { ticks: [], unit: 'day', every: 1 };
|
|
121
168
|
const span = Math.max(1, t1 - t0);
|
|
122
169
|
let [unit, every] = UNITS.find(([, , ms]) => span / ms <= count) || UNITS[UNITS.length - 1];
|
|
123
170
|
const out = [];
|
|
@@ -140,6 +187,14 @@ export function timeTicks(t0, t1, count = 6, locale = 'es') {
|
|
|
140
187
|
const c = new Date(cur);
|
|
141
188
|
cur = Date.UTC(c.getUTCFullYear(), c.getUTCMonth() + every, 1);
|
|
142
189
|
}
|
|
190
|
+
} else if (unit === 'hour' || unit === 'minute') {
|
|
191
|
+
// Múltiplos exactos del paso en UTC (las horas 1/3/6/12 dividen el día).
|
|
192
|
+
const ms = every * (unit === 'hour' ? HOUR : MIN);
|
|
193
|
+
cur = Math.ceil(t0 / ms - 1e-9) * ms;
|
|
194
|
+
while (cur <= t1) {
|
|
195
|
+
out.push(cur);
|
|
196
|
+
cur += ms;
|
|
197
|
+
}
|
|
143
198
|
} else {
|
|
144
199
|
cur = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
145
200
|
if (cur < t0) cur += DAY;
|
|
@@ -148,9 +203,18 @@ export function timeTicks(t0, t1, count = 6, locale = 'es') {
|
|
|
148
203
|
cur += every * DAY;
|
|
149
204
|
}
|
|
150
205
|
}
|
|
206
|
+
const dayOf = (t) => Math.floor(t / DAY);
|
|
151
207
|
const label = (t, i) => {
|
|
152
208
|
const dt = new Date(t);
|
|
153
209
|
if (unit === 'year') return formatDate(t, 'year', locale);
|
|
210
|
+
// Hora "HH:mm"; la fecha solo cuando cambia el día (a medianoche, solo la fecha).
|
|
211
|
+
if (unit === 'hour' || unit === 'minute') {
|
|
212
|
+
const newDay = i === 0 || dayOf(out[i - 1]) !== dayOf(t);
|
|
213
|
+
if (!newDay) return formatDate(t, unit, locale, false);
|
|
214
|
+
const prevY = i > 0 ? new Date(out[i - 1]).getUTCFullYear() : null;
|
|
215
|
+
const date = formatDate(t, 'day', locale, i > 0 && prevY !== dt.getUTCFullYear());
|
|
216
|
+
return t % DAY === 0 && i > 0 ? date : `${date} ${formatDate(t, unit, locale, false)}`;
|
|
217
|
+
}
|
|
154
218
|
if (unit === 'month') {
|
|
155
219
|
const first = i === 0 || dt.getUTCMonth() < every;
|
|
156
220
|
if (every === 3) return formatDate(t, 'quarter', locale, first);
|
|
@@ -165,7 +229,14 @@ export function timeTicks(t0, t1, count = 6, locale = 'es') {
|
|
|
165
229
|
/** Unidad natural de una serie de fechas (para tooltips). */
|
|
166
230
|
export function dateUnit(times) {
|
|
167
231
|
const ds = times.map((t) => new Date(t));
|
|
232
|
+
if (ds.some((d) => d.getUTCSeconds() || d.getUTCMinutes())) return 'minute';
|
|
233
|
+
if (ds.some((d) => d.getUTCHours())) return 'hour';
|
|
168
234
|
if (ds.every((d) => d.getUTCMonth() === 0 && d.getUTCDate() === 1)) return 'year';
|
|
169
235
|
if (ds.every((d) => d.getUTCDate() === 1)) return 'month';
|
|
170
236
|
return 'day';
|
|
171
237
|
}
|
|
238
|
+
|
|
239
|
+
/** Mitad de la unidad natural (para centrar una sola fecha en el eje). */
|
|
240
|
+
export function halfUnit(unit) {
|
|
241
|
+
return { minute: MIN / 2, hour: HOUR / 2, day: DAY / 2, month: 15 * DAY, year: 182.6 * DAY }[unit] ?? DAY / 2;
|
|
242
|
+
}
|
package/src/schema.js
CHANGED
|
@@ -6,21 +6,39 @@ import { TYPES, ALIASES } from './infer.js';
|
|
|
6
6
|
const field = (d) => ({ type: 'string', description: d });
|
|
7
7
|
const fieldOrList = (d) => ({ anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: d });
|
|
8
8
|
const strOrList = { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] };
|
|
9
|
+
const bool = (d) => ({ type: 'boolean', description: d });
|
|
10
|
+
const number = (d) => ({ type: 'number', description: d });
|
|
11
|
+
const numPair = (d) => ({ type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: d });
|
|
12
|
+
const formatSpec = (d) => ({
|
|
13
|
+
description: d,
|
|
14
|
+
anyOf: [
|
|
15
|
+
{ type: 'string', enum: ['number', 'compact', 'percent', 'currency', 'integer'] },
|
|
16
|
+
{ type: 'object', properties: { type: { type: 'string', enum: ['number', 'compact', 'percent', 'currency', 'integer'] }, prefix: { type: 'string' }, suffix: { type: 'string' }, decimals: { type: 'integer', minimum: 0, maximum: 20 }, currency: { type: 'string' }, scale: { type: 'number' } } },
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
/** Escala de color continua: nombre de matplotlib, lista de colores u objeto. */
|
|
20
|
+
const colorScaleSpec = (d) => ({
|
|
21
|
+
description: d,
|
|
22
|
+
anyOf: [
|
|
23
|
+
{ type: 'string' },
|
|
24
|
+
{ type: 'array', items: { type: 'string' }, minItems: 2 },
|
|
25
|
+
{ type: 'object', properties: { name: { type: 'string' }, colors: { type: 'array', items: { type: 'string' } }, reverse: { type: 'boolean' }, mid: { type: 'number', description: 'Center of a diverging scale.' } } },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
9
28
|
|
|
10
29
|
export const schema = {
|
|
11
30
|
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
12
31
|
$id: 'https://unpkg.com/plotagents/schema.json',
|
|
13
32
|
title: 'plotagents spec',
|
|
14
33
|
description:
|
|
15
|
-
'Declarative chart spec. Data as rows (tidy) plus the field name for each role. Everything else has well-designed defaults.',
|
|
34
|
+
'Declarative chart spec. Data as rows (tidy) plus the field name for each role. Everything else has well-designed defaults. `data` is required except for kpi (`items`), flowchart (`nodes` + `edges`), chord (`matrix`) and wordcloud with raw `text`.',
|
|
16
35
|
type: 'object',
|
|
17
|
-
required: ['data'],
|
|
18
36
|
properties: {
|
|
19
37
|
type: {
|
|
20
38
|
type: 'string',
|
|
21
|
-
enum: ['auto', ...TYPES, ...Object.keys(ALIASES)],
|
|
39
|
+
enum: [...new Set(['auto', ...TYPES, 'pointmap', ...Object.keys(ALIASES)])],
|
|
22
40
|
description:
|
|
23
|
-
'Compare: bar, count. Time: line, area. Relationships: scatter, bubble, regplot, residplot, jointplot, radar. Distributions (seaborn): hist, kde, ecdf, box, violin, strip, swarm, point. Part-to-whole: treemap, sunburst, donut, pie. Other: heatmap, sankey, kpi. seaborn names are accepted (barplot, countplot, histplot, kdeplot, boxplot, violinplot, lmplot…). "auto" (default) picks from the data.',
|
|
41
|
+
'Compare: bar, count. Time: line, area. Schedules: gantt. Relationships: scatter, bubble, regplot, residplot, jointplot, radar, corr (correlation matrix; clustermap), pairplot (scatter-plot matrix). Distributions (seaborn): hist, kde, ecdf, box, violin, strip, swarm, point. Part-to-whole: treemap, sunburst, donut, pie. Other: heatmap, sankey, kpi, network, chord, pyramid, flowchart, wordcloud. Maps: choropleth, symbolmap, pointmap (lat/lon dots over a tile basemap). seaborn names are accepted (barplot, countplot, histplot, kdeplot, boxplot, violinplot, lmplot…). "auto" (default) picks from the data.',
|
|
24
42
|
},
|
|
25
43
|
data: {
|
|
26
44
|
description: 'Array of rows, e.g. [{ "region": "Biobío", "sales": 1240 }]. For kpi it can be omitted in favor of `items`.',
|
|
@@ -29,14 +47,15 @@ export const schema = {
|
|
|
29
47
|
title: { type: 'string', description: 'The finding as a full sentence, ideally with the key number, e.g. "Biobío accounts for 32% of sales".' },
|
|
30
48
|
subtitle: { type: 'string', description: 'How to read the chart: measure, unit and period, e.g. "Sales by region, CLP millions, 2025".' },
|
|
31
49
|
source: { type: 'string', description: 'Data source, shown at the bottom right.' },
|
|
32
|
-
locale: { type: 'string', enum: ['es', 'en'], description: 'Language and number format. Default "es" (1.234,5);
|
|
50
|
+
locale: { type: 'string', enum: ['es', 'en', 'pt', 'fr', 'de', 'auto'], description: 'Language and number format. Default "es" (1.234,5); "en" for 1,234.5; also "pt", "fr", "de". "auto" recognizes the language from the title, subtitle, column names and categories (tables do this when `locale` is omitted).' },
|
|
33
51
|
x: field('x-axis or category field (bar, line, area, scatter, heatmap, box, violin…).'),
|
|
34
52
|
y: fieldOrList('Value field. For line/bar a list of fields (wide table) turns each one into a series.'),
|
|
35
53
|
series: field('Field that splits the data into colored series. Alias: hue.'),
|
|
54
|
+
color: field('Alias of series when it names a field (a "#hex" value is a fixed color instead).'),
|
|
36
55
|
hue: field('Alias of series (seaborn naming).'),
|
|
37
56
|
value: field('Value field for treemap, sunburst, donut, heatmap, sankey and radar.'),
|
|
38
57
|
label: field('Name field (treemap, donut, point labels in scatter/regplot).'),
|
|
39
|
-
group: field('Level-1 group field for a two-level treemap (shortcut for path).'),
|
|
58
|
+
group: field('Level-1 group field for a two-level treemap (shortcut for path). gantt: phase/workstream field (one colored section per phase). table: rows grouped under a header per value.'),
|
|
40
59
|
path: { type: 'array', items: { type: 'string' }, description: 'Hierarchy for treemap/sunburst, any depth: [level1, level2, …].' },
|
|
41
60
|
size: field('Bubble size field (area-proportional).'),
|
|
42
61
|
from: field('Sankey source field.'),
|
|
@@ -63,17 +82,70 @@ export const schema = {
|
|
|
63
82
|
},
|
|
64
83
|
},
|
|
65
84
|
|
|
85
|
+
// Word clouds
|
|
86
|
+
text: { description: 'wordcloud: field with the words, OR the raw text to count (a string or a list of strings, e.g. open survey answers). Raw text drops Spanish/English stopwords.', anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
87
|
+
textField: field('wordcloud: field holding free text in each row; words are counted across rows.'),
|
|
88
|
+
stopwords: { type: 'array', items: { type: 'string' }, description: 'wordcloud: extra words to ignore when counting raw text.' },
|
|
89
|
+
minFont: { type: 'number', description: 'wordcloud: smallest font size (default 11).' },
|
|
90
|
+
maxFont: { type: 'number', description: 'wordcloud: largest font size (default from the panel size).' },
|
|
91
|
+
|
|
92
|
+
// Schedules
|
|
93
|
+
task: field('gantt: task name field (one row per task).'),
|
|
94
|
+
start: field('gantt: start date field ("2026-03-02", "2026-03") or a number (e.g. week).'),
|
|
95
|
+
end: field('gantt: end date field. Day dates are inclusive. Missing end, or end equal to start, makes a milestone.'),
|
|
96
|
+
duration: field('gantt: duration field (days, or units of a numeric axis) instead of `end`.'),
|
|
97
|
+
progress: field('gantt: completion field, 0–1 or 0–100. The filled part of each bar is the progress.'),
|
|
98
|
+
dependsOn: field('gantt: field with the name(s) of the predecessor task(s) ("A" or "A, B"): draws arrows.'),
|
|
99
|
+
milestone: field('gantt: boolean field that marks milestones (drawn as diamonds).'),
|
|
100
|
+
today: { description: 'gantt: date of the "today" line ("2026-03-12"), or true for the current date.', anyOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'number' }] },
|
|
101
|
+
groupHeaders: { description: 'gantt: false draws phases as a color legend instead of section headers.', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['compact'] }] },
|
|
102
|
+
rowHeight: { type: 'number', description: 'gantt: row height in px.' },
|
|
103
|
+
collapsed: { description: 'gantt: phases (values of `group`) drawn collapsed into one summary row; true collapses all. In the browser a click on a phase header toggles it.', anyOf: [{ type: 'boolean' }, { type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
104
|
+
collapsible: bool('gantt: false disables click-to-collapse of phases in the browser.'),
|
|
105
|
+
critical: bool('gantt: highlight the critical path, the zero-slack chain along `dependsOn`.'),
|
|
106
|
+
criticalColor: { type: 'string', description: 'gantt: color of the critical path.' },
|
|
107
|
+
weekends: bool('gantt: shade weekends on day plans of up to 120 days (default true).'),
|
|
108
|
+
todayColor: { type: 'string', description: 'gantt: color of the "today" line.' },
|
|
109
|
+
sort: { type: 'string', enum: ['desc', 'asc', 'none', 'alpha', 'start'], description: 'bar: category order (desc by default unless the categories are time). gantt: "start" orders tasks by start date. donut/chord: "none" keeps the data order.' },
|
|
110
|
+
|
|
66
111
|
// Maps, networks, flows
|
|
67
|
-
geo: { description: 'choropleth/symbolmap: "chile" (16 regions, default), "usa" (50 states + DC, Albers with Alaska/Hawaii insets; Puerto Rico when it has data), "world" (countries), a name registered with registerGeo(), or a GeoJSON FeatureCollection.', anyOf: [{ type: 'string' }, { type: 'object' }] },
|
|
112
|
+
geo: { description: 'choropleth/symbolmap: "chile" (16 regions, default), "usa" (50 states + DC, Albers with Alaska/Hawaii insets; Puerto Rico when it has data), "world" (countries), a name registered with registerGeo(), or YOUR OWN geometry: a GeoJSON FeatureCollection, Feature, list of Features or bare geometry, or a TopoJSON Topology. Polygons are filled, LineStrings stroked and Points drawn as dots. In the browser, render() also accepts a URL to a .geojson/.topojson file.', anyOf: [{ type: 'string' }, { type: 'object' }, { type: 'array', items: { type: 'object' } }] },
|
|
113
|
+
geoObject: field('TopoJSON: name of the object inside `objects` to draw (default: the first one).'),
|
|
68
114
|
key: field('choropleth/symbolmap: data field with the region, state or country name/code to match (US: "TX", "Texas", "06").'),
|
|
69
|
-
projection: { type: 'string', enum: ['mercator', 'equalEarth', 'albersUsa'],
|
|
115
|
+
projection: { description: 'Map projection (picked automatically): "mercator", "equalEarth", "albersUsa", "equirectangular", "conic" (or { type: "conic", parallels: [-30, -45], rotate: 72 }), "identity" (coordinates already in pixels, y down) or "planar" (projected coordinates such as UTM metres, y up; chosen automatically when coordinates are outside lon/lat range).', anyOf: [{ type: 'string', enum: ['mercator', 'equalEarth', 'albersUsa', 'equirectangular', 'conic', 'identity', 'planar'] }, { type: 'object', properties: { type: { type: 'string' }, parallels: { type: 'array', items: { type: 'number' } }, rotate: { type: 'number' }, center: { type: 'array', items: { type: 'number' } } } }] },
|
|
70
116
|
values: { type: 'boolean', description: 'choropleth with state codes (usa): false shows only the codes, without values.' },
|
|
71
|
-
featureKey: field('choropleth: feature property to match against
|
|
117
|
+
featureKey: field('choropleth: feature property to match `key` against. Default tries code, id, ISO-3, name and aliases, then any property whose values match the data (e.g. "CUT_REG", "GEOID").'),
|
|
118
|
+
nameKey: field('choropleth: feature property with the display name (default: name, NOMBRE, NAME_1, ADMIN…).'),
|
|
119
|
+
fitTo: { description: 'Map framing: zoom to some regions (names or codes), a bounding box [lonMin, latMin, lonMax, latMax], or "markers". The rest of the map stays as context, clipped.', anyOf: [{ type: 'string' }, { type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'number' }] } }] },
|
|
120
|
+
markers: { description: 'Map: points on top, from [{ lon, lat, label, value?, color? }] or { data, lon, lat, label, size, color, legend, maxRadius, labels }. With a size field, circles are area-proportional.', anyOf: [{ type: 'array', items: { type: 'object' } }, { type: 'object' }] },
|
|
121
|
+
layers: { type: 'array', description: 'Map: extra GeoJSON drawn over (or `below: true` under) the regions: rivers, roads, routes, borders, points. Each { geo, label?, stroke?, width?, fill?, dash?, opacity?, below? }; a `label` adds a legend key.', items: { type: 'object' } },
|
|
122
|
+
borderColor: { type: 'string', description: 'Map: color of the borders between regions.' },
|
|
123
|
+
borderWidth: { type: 'number', description: 'Map: width of the borders between regions.' },
|
|
124
|
+
pointRadius: { type: 'number', description: 'Map: radius of Point features in your own GeoJSON (default 5).' },
|
|
125
|
+
missing: { type: 'string', enum: ['hatch', 'plain'], description: 'choropleth: how regions without data look: hatched (default) or "plain" (flat tint).' },
|
|
126
|
+
thresholds: { type: 'array', items: { type: 'number' }, description: 'choropleth: class breaks (same as a list in `scale`).' },
|
|
127
|
+
lat: field('pointmap: latitude field (auto-detected: lat, latitude, latitud).'),
|
|
128
|
+
lon: field('pointmap: longitude field (auto-detected: lon, lng, longitude, longitud).'),
|
|
129
|
+
tiles: { description: 'pointmap: basemap. "osm" (default), "esri-gray", "esri-dark", "carto-light", "carto-dark", "mapbox-light", "mapbox-dark" (needs `tilesKey`), your own { url, attribution, subdomains, maxZoom, retina } with {z}/{x}/{y}, or false for none (vector world outline).', anyOf: [{ type: 'string', enum: ['osm', 'esri-gray', 'esri-dark', 'carto-light', 'carto-dark', 'mapbox-light', 'mapbox-dark'] }, { type: 'boolean', enum: [false] }, { type: 'object', properties: { url: { type: 'string' }, attribution: { type: 'string' }, subdomains: { type: 'string' }, maxZoom: { type: 'number' }, retina: { type: 'boolean' } }, required: ['url'] }] },
|
|
130
|
+
tilesKey: { type: 'string', description: 'pointmap: API key or PUBLIC token for the tile provider (Mapbox "pk.…"; a secret "sk." token is rejected because it would be visible in the SVG).' },
|
|
131
|
+
tileStyle: { type: 'string', enum: ['muted', 'original'], description: 'pointmap: "muted" (default) desaturates the basemap so the data stands out; "original" keeps its colors.' },
|
|
132
|
+
tileOpacity: { type: 'number', minimum: 0, maximum: 1, description: 'pointmap: basemap opacity.' },
|
|
133
|
+
zoom: { type: 'number', description: 'pointmap: tile zoom level (automatic from the points by default).' },
|
|
134
|
+
center: { type: 'array', items: { type: 'number' }, minItems: 2, maxItems: 2, description: 'pointmap: map center [lon, lat] (default: fitted to the points).' },
|
|
135
|
+
tall: { type: 'boolean', description: 'Map: false prevents the tall layout (map on the left, ranked label column on the right) for elongated territories.' },
|
|
72
136
|
midpoint: { type: 'number', description: 'choropleth/heatmap: diverging color scale around this value (e.g. 0 for changes).' },
|
|
73
137
|
classes: { type: 'integer', description: 'choropleth: number of color classes (default 5).' },
|
|
138
|
+
scale: { description: 'choropleth: "quantile" (default), "equal", "continuous", "categorical" (text values get one color each; automatic when `value` holds text) or a list of thresholds.', anyOf: [{ type: 'string', enum: ['quantile', 'equal', 'continuous', 'categorical'] }, { type: 'array', items: { type: 'number' } }] },
|
|
74
139
|
nodes: { type: 'array', items: { type: 'object' }, description: 'network: node rows (id, group, size). flowchart: steps (id, label, type: start|end|process|decision|data|note, lane).' },
|
|
75
140
|
edges: { type: 'array', items: { type: 'object' }, description: 'flowchart: connections (from, to, label). Same as `data`.' },
|
|
76
|
-
layout: { type: 'string', enum: ['force', 'circle'], description: 'network layout.' },
|
|
141
|
+
layout: { type: 'string', enum: ['force', 'circular', 'radial', 'concentric', 'clusters', 'tree', 'grid', 'circle'], description: 'network layout: "force" (default) deterministic force-directed; "circular" around a circle by group with names rotated outward ("circle" = circular); "radial" tree rings from `focus` or the most connected node; "concentric" rings by weight; "clusters" one cluster per group; "tree" the radial tree laid flat; "grid" ordered by group.' },
|
|
142
|
+
curveness: { type: 'number', minimum: 0, description: 'network: edge bend as a share of its length (default 0.3; 0 = straight).' },
|
|
143
|
+
maxRadius: { type: 'number', description: 'network/symbolmap/pointmap/bubble: largest node or circle radius in px.' },
|
|
144
|
+
iterations: { type: 'integer', description: 'network: force-layout iterations (automatic by size).' },
|
|
145
|
+
nodeId: field('network: id field in `nodes` (default id/name).'),
|
|
146
|
+
nodeLabel: field('network: label field in `nodes` (default label, else the id).'),
|
|
147
|
+
labelOrientation: { type: 'string', enum: ['horizontal', 'radial'], description: 'chord: label orientation (radial by default above 10 groups).' },
|
|
148
|
+
ribbonColor: { type: 'string', enum: ['source', 'smaller', 'larger'], description: 'chord: ribbon color: "source" (default when directed), "smaller" group (default) or "larger" flow.' },
|
|
77
149
|
matrix: { type: 'array', items: { type: 'array', items: { type: 'number' } }, description: 'chord: square flow matrix (with `names`).' },
|
|
78
150
|
names: { type: 'array', items: { type: 'string' }, description: 'chord: group names for `matrix`.' },
|
|
79
151
|
directed: { type: 'boolean', description: 'chord: treat flows as directed (default false).' },
|
|
@@ -84,17 +156,53 @@ export const schema = {
|
|
|
84
156
|
|
|
85
157
|
// Layout and interaction
|
|
86
158
|
facet: field('One panel per value of this field (small multiples, seaborn col). Shared scales and colors.'),
|
|
87
|
-
|
|
159
|
+
col: field('Alias of facet (seaborn naming).'),
|
|
160
|
+
facetOrder: { type: 'array', items: { type: 'string' }, description: 'facet: panel order (values of the facet field; the rest follow).' },
|
|
161
|
+
columns: {
|
|
162
|
+
description: 'table: the columns, in order: field names or objects { field, label, type, format, align, width, good, target, colorScale, total, lines }. Column types: text, number, bar (data bar), heat (cell color), delta (▲/▼ change, colored by `good`), sparkline (field holds a list of numbers), progress (meter vs `target` field/number, or 0–1 / 0–100), badge (status pill; status words in es/en/pt/fr/de get good/warning/critical colors), check (booleans as ✓), date, link, rank. Omit to infer everything. facet: panels per row (an integer, seaborn col_wrap). kpi: tiles per row.',
|
|
163
|
+
anyOf: [
|
|
164
|
+
{ type: 'integer' },
|
|
165
|
+
{ type: 'array', items: { anyOf: [{ type: 'string' }, { type: 'object', properties: {
|
|
166
|
+
field: { type: 'string' }, label: { type: 'string' },
|
|
167
|
+
type: { type: 'string', enum: ['text', 'number', 'bar', 'heat', 'delta', 'sparkline', 'progress', 'badge', 'check', 'date', 'link', 'rank'] },
|
|
168
|
+
format: { anyOf: [{ type: 'string' }, { type: 'object' }] }, align: { type: 'string', enum: ['left', 'right', 'center'] }, width: { type: 'number' },
|
|
169
|
+
good: { type: 'string', enum: ['up', 'down'] }, target: { anyOf: [{ type: 'string' }, { type: 'number' }] }, colorScale: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }, { type: 'object' }] },
|
|
170
|
+
total: { anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['sum', 'mean', 'min', 'max', 'count'] }] }, lines: { type: 'integer', minimum: 1, maximum: 6 }, text: { type: 'string', description: 'link: field with the text to show instead of the URL.' }, sortable: { type: 'boolean' },
|
|
171
|
+
} }] } },
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
sortBy: field('table: column to sort by (field or label). Numbers sort high→low by default, text A→Z. In the browser a click on a header sorts.'),
|
|
175
|
+
sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'table: sort direction (same as `sort: "asc" | "desc"`).' },
|
|
176
|
+
pageSize: { anyOf: [{ type: 'integer', minimum: 1 }, { type: 'boolean', enum: [false] }], description: 'table: rows per page. Default: all rows up to 15, then pages of 10. false shows every row.' },
|
|
177
|
+
page: { type: 'integer', minimum: 1, description: 'table: page to show (1-based). In the browser the pager changes it.' },
|
|
178
|
+
query: { type: 'string', description: 'table: show only rows that contain these words (accent- and case-insensitive).' },
|
|
179
|
+
search: { type: 'boolean', description: 'table: search box in the browser (default: on above 20 rows).' },
|
|
180
|
+
total: { description: 'table: totals row. true sums numbers (and averages percents and progress); "mean"/"sum"; or { field: "sum" | "mean" | false, label }.', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['sum', 'mean', 'min', 'max'] }, { type: 'object' }] },
|
|
181
|
+
totalLabel: { type: 'string', description: 'table: label of the totals row (default "Total"/"Gesamt").' },
|
|
182
|
+
rank: { type: 'boolean', description: 'table: add a "#" column with the position under the current sort.' },
|
|
183
|
+
cards: { description: 'table: one card per row. "auto" (default) switches when the columns do not fit (phones); true always; false never (columns shrink and truncate).', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['auto'] }] },
|
|
184
|
+
density: { type: 'string', enum: ['compact', 'normal', 'comfortable'], description: 'table: row height.' },
|
|
185
|
+
striped: { type: 'boolean', description: 'table: tint every other row (helps on very wide tables).' },
|
|
186
|
+
bars: { type: 'boolean', description: 'table: false turns off the automatic data bar on the main numeric column.' },
|
|
187
|
+
sortable: { type: 'boolean', description: 'table: false disables click-to-sort on headers in the browser.' },
|
|
188
|
+
sharex: bool('facet: same x scale in every panel (default true).'),
|
|
189
|
+
panelHeight: number('facet: height of each panel in px.'),
|
|
88
190
|
sharey: { type: 'boolean', description: 'facet: same y scale in every panel (default true).' },
|
|
89
|
-
time: field('Time field (d3plus-style): shows one period at a time (the latest by default
|
|
191
|
+
time: field('Time field (d3plus-style): shows one period at a time (the latest by default, or `timeValue`). Add `slider: true` only when the person asks to move through periods.'),
|
|
90
192
|
timeValue: { description: 'Period to show when `time` is set.' },
|
|
91
|
-
slider: {
|
|
92
|
-
play: { type: 'boolean', description: 'Browser: false keeps the time
|
|
193
|
+
slider: { description: 'Browser time controls, OFF by default; set them only when the person asks. true: period buttons when there are few periods (≤ 8), a minimal slider otherwise; line/area get a range slider. "buttons" or "slider" force one.', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['buttons', 'slider'] }] },
|
|
194
|
+
play: { type: 'boolean', description: 'Browser: false keeps the time controls but hides the play button.' },
|
|
195
|
+
playInterval: number('Browser: milliseconds per period while playing.'),
|
|
196
|
+
legend: { description: 'Legend: true/false, or its position "top" (default), "bottom", "left", "right" (left/right fall back to bottom under 520px wide), or { position }.', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['top', 'bottom', 'left', 'right'] }, { type: 'object', properties: { position: { type: 'string', enum: ['top', 'bottom', 'left', 'right'] } } }] },
|
|
197
|
+
legendTitle: { type: 'string', description: 'Title of a color-ramp or size legend (heatmap, maps).' },
|
|
198
|
+
exclude: { description: 'Drop legend entries (series, first hierarchy level, group or donut slice) and recompute axes and shares with the rest. Colors stay fixed; excluded entries remain in the legend, faded, so a click re-adds them. In the browser shift+click on a legend key toggles it.', ...strOrList },
|
|
93
199
|
compare: { description: 'With `time`: period to compute changes against: "previous" (default), "first", a specific period, or false.', anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }] },
|
|
94
200
|
colorBy: { type: 'string', enum: ['group', 'change', 'label'], description: 'treemap: "change" colors tiles by % change vs the comparison period (diverging, growth cool / decline warm); "label" colors a single-level treemap by item.' },
|
|
95
201
|
shade: { type: 'boolean', description: 'treemap/sunburst: subtle lightness steps inside each category (default false: one solid color per category).' },
|
|
96
|
-
xRange: { type: 'array', description: 'line/area: visible x range [from, to] (what the range slider sets).' },
|
|
97
|
-
|
|
202
|
+
xRange: { type: 'array', description: 'line/area: visible x range [from, to] (what the range slider sets with `slider: true`).' },
|
|
203
|
+
showValues: bool('treemap: show each tile value next to its label.'),
|
|
204
|
+
hole: { type: 'number', minimum: 0, maximum: 0.9, description: 'sunburst: size of the center hole as a fraction of the radius (default 0.3).' },
|
|
205
|
+
focus: { description: 'treemap/sunburst: path to drill into, e.g. ["Mining"] (in the browser a click on a tile or legend key zooms to it). network: a node id to center and highlight with its neighbors.', anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
98
206
|
depth: { type: 'integer', description: 'treemap/sunburst: how many levels to show.' },
|
|
99
207
|
|
|
100
208
|
// Scales and axes
|
|
@@ -102,31 +210,30 @@ export const schema = {
|
|
|
102
210
|
yScale: { type: 'string', enum: ['linear', 'log'] },
|
|
103
211
|
xDomain: { type: 'array', items: { type: 'number' }, description: 'Fixed x domain [min, max].' },
|
|
104
212
|
yDomain: { type: 'array', items: { type: 'number' }, description: 'Fixed value-axis domain [min, max].' },
|
|
213
|
+
valueDomain: numPair('box/violin/strip/swarm/point/radar: fixed value-axis domain [min, max] whatever the orientation.'),
|
|
214
|
+
orient: { type: 'string', enum: ['v', 'h'], description: 'box/violin/strip/swarm/point: "v" vertical or "h" horizontal (same as `horizontal`).' },
|
|
105
215
|
zero: { type: 'boolean', description: 'line: force (or prevent) a y axis starting at zero.' },
|
|
106
216
|
xLabel: { type: 'string', description: 'x-axis title.' },
|
|
107
217
|
yLabel: { type: 'string', description: 'y-axis title (drawn horizontally above the axis).' },
|
|
108
|
-
format: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{ type: 'string', enum: ['number', 'compact', 'percent', 'currency', 'integer'] },
|
|
112
|
-
{ type: 'object', properties: { type: { type: 'string' }, prefix: { type: 'string' }, suffix: { type: 'string' }, decimals: { type: 'integer' }, currency: { type: 'string' } } },
|
|
113
|
-
],
|
|
114
|
-
},
|
|
218
|
+
format: formatSpec('Value format: "number" | "compact" | "percent" | "currency" | "integer", or { type, prefix, suffix, decimals (0–20), currency }.'),
|
|
219
|
+
xFormat: formatSpec('Format of x values when x is a measure (scatter, regplot, hist).'),
|
|
220
|
+
yFormat: formatSpec('Format of y values (overrides `format` on the y axis).'),
|
|
115
221
|
|
|
116
222
|
// Emphasis and annotation
|
|
117
|
-
highlight: { description: 'Emphasize one or more categories/series and mute the rest.', ...strOrList },
|
|
223
|
+
highlight: { description: 'Emphasize one or more categories/series and mute the rest. table: the rows whose label matches.', ...strOrList },
|
|
118
224
|
dashed: { description: 'Draw some shapes dashed (hatched fill + dashed outline; dashed stroke for lines) to mark estimates, projections or provisional figures. A category/series/region name or a list of them, a data field whose truthy rows are dashed, { field, equals }, or true for all.', anyOf: [{ type: 'string' }, { type: 'boolean' }, { type: 'array', items: { type: 'string' } }, { type: 'object', properties: { field: { type: 'string' }, equals: {} } }] },
|
|
119
225
|
dashedFrom: { description: 'line/area/bar on a time axis: x value from which marks are dashed (a projection). Lines turn dashed from the last firm point, over a soft labeled band.', anyOf: [{ type: 'string' }, { type: 'number' }] },
|
|
120
226
|
dashedLabel: { description: 'What the dashed shapes mean ("Projection", "Preliminary"): legend entry, band label and tooltip line. false hides the band label.', anyOf: [{ type: 'string' }, { type: 'boolean' }] },
|
|
121
|
-
labels: { description: 'Value labels: true, false or "auto". In scatter/regplot: a number of points or a list of names to label.', anyOf: [{ type: 'boolean' }, { type: 'string' }, { type: 'number' }, { type: 'array', items: { type: 'string' } }] },
|
|
227
|
+
labels: { description: 'Value labels: true, false or "auto". In scatter/regplot: a number of points or a list of names to label. network: true labels every node.', anyOf: [{ type: 'boolean' }, { type: 'string' }, { type: 'number' }, { type: 'array', items: { type: 'string' } }] },
|
|
122
228
|
refLines: { type: 'array', description: 'Reference lines (targets, averages).', items: { type: 'object', properties: { y: { type: 'number' }, x: { type: 'number' }, value: { type: 'number' }, label: { type: 'string' } } } },
|
|
123
229
|
annotations: { type: 'array', description: 'line/area: events on the x axis.', items: { type: 'object', properties: { x: {}, label: { type: 'string' } } } },
|
|
124
230
|
|
|
125
231
|
// Bars and lines
|
|
126
232
|
stack: { description: 'bar/area: true stacks; "percent" stacks to 100%.', anyOf: [{ type: 'boolean' }, { type: 'string', enum: ['percent'] }] },
|
|
127
233
|
horizontal: { type: 'boolean', description: 'Force horizontal orientation (otherwise chosen from label length and count).' },
|
|
128
|
-
|
|
129
|
-
|
|
234
|
+
top: { type: 'integer', description: 'bar: keep the N largest and fold the rest into "Other". table: keep the first N rows after sorting.' },
|
|
235
|
+
endLabels: bool('line/regplot: false removes the series names at the end of each line (a legend is drawn instead).'),
|
|
236
|
+
labelsFor: { type: 'object', additionalProperties: { type: 'string' }, description: 'Wide table (y as a list): display names per column, e.g. { "rev": "Revenue" }.' },
|
|
130
237
|
index: { type: 'boolean', description: 'line: rebase each series to 100 at its first value (compare growth without a dual axis).' },
|
|
131
238
|
curve: { type: 'string', enum: ['linear', 'smooth', 'step'] },
|
|
132
239
|
aggregate: { type: 'string', enum: ['sum', 'mean', 'max', 'min', 'count', 'last', 'first'], description: 'How to combine repeated rows. Default sum.' },
|
|
@@ -141,40 +248,132 @@ export const schema = {
|
|
|
141
248
|
kde: { type: 'boolean', description: 'hist: overlay a density curve.' },
|
|
142
249
|
multiple: { type: 'string', enum: ['layer', 'stack', 'dodge', 'fill'], description: 'hist/kde with hue: how groups are combined.' },
|
|
143
250
|
fill: { type: 'boolean', description: 'kde: fill the area. line: false removes the gradient under a single line.' },
|
|
144
|
-
order: { description: 'regplot: polynomial degree (number). box/violin/strip/swarm/point: category order (list).', anyOf: [{ type: 'integer' }, { type: 'array', items: { type: 'string' } }] },
|
|
251
|
+
order: { description: 'regplot: polynomial degree (number). box/violin/strip/swarm/point: category order (list). corr: variable order (list).', anyOf: [{ type: 'integer' }, { type: 'array', items: { type: 'string' } }] },
|
|
145
252
|
lowess: { type: 'boolean', description: 'regplot: local regression instead of a straight line.' },
|
|
146
253
|
logx: { type: 'boolean', description: 'regplot: fit y ~ log(x) with a log x axis.' },
|
|
147
254
|
trend: { type: 'boolean', description: 'scatter: add a least-squares line and R².' },
|
|
148
255
|
annotate: { type: 'boolean', description: 'regplot: show R² and n.' },
|
|
149
|
-
kind: { type: 'string', enum: ['scatter', 'reg'], description: 'jointplot: center plot.' },
|
|
256
|
+
kind: { type: 'string', enum: ['scatter', 'reg'], description: 'jointplot: center plot. pairplot: "reg" adds a least-squares line to every scatter panel.' },
|
|
150
257
|
marginal: { type: 'string', enum: ['hist', 'kde'], description: 'jointplot: marginal plots.' },
|
|
151
|
-
inner: { type: 'string', enum: ['box', 'quartiles', 'points', 'none'], description: 'violin: what goes inside.' },
|
|
258
|
+
inner: { type: 'string', enum: ['box', 'quartiles', 'points', 'stick', 'none'], description: 'violin: what goes inside.' },
|
|
259
|
+
bandwidth: number('violin: KDE bandwidth (default Scott\'s rule).'),
|
|
260
|
+
bwAdjust: number('kde/violin/hist kde: multiply the bandwidth (seaborn bw_adjust).'),
|
|
261
|
+
cut: number('kde/violin: how far the density extends past the data, in bandwidths.'),
|
|
262
|
+
jitter: { description: 'strip: spread of the points across the category (fraction of the band, default 0.6; 0 or false = none).', anyOf: [{ type: 'number' }, { type: 'boolean' }] },
|
|
263
|
+
dodge: bool('strip/swarm/point with hue: place groups side by side.'),
|
|
264
|
+
hueOrder: { type: 'array', items: { type: 'string' }, description: 'Order of the hue groups.' },
|
|
265
|
+
showMeans: bool('box/strip: mark the mean.'),
|
|
266
|
+
cumulative: bool('hist/kde: cumulative distribution.'),
|
|
267
|
+
rug: bool('kde: tick marks for each observation.'),
|
|
268
|
+
complementary: bool('ecdf: show 1 − F(x) (share above each value).'),
|
|
269
|
+
radius: number('Point radius in px (scatter, regplot, strip, swarm).'),
|
|
152
270
|
split: { type: 'boolean', description: 'violin: with two hue levels, one half each.' },
|
|
153
271
|
points: { type: 'boolean', description: 'box: overlay the observations. line: markers on every point.' },
|
|
154
272
|
normalize: { type: 'boolean', description: 'radar: scale each axis 0–1 by its own maximum.' },
|
|
155
273
|
|
|
156
274
|
// Part-to-whole
|
|
157
275
|
half: { type: 'boolean', description: 'donut: semicircle.' },
|
|
276
|
+
thickness: { type: 'number', minimum: 0, maximum: 1, description: 'donut: ring thickness as a fraction of the radius (1 = pie).' },
|
|
277
|
+
maxSlices: { type: 'integer', minimum: 2, description: 'donut/pie: slices before the rest fold into "Other" (default 6).' },
|
|
278
|
+
centerValue: { type: 'string', description: 'donut/sunburst: text in the center (default: the total).' },
|
|
279
|
+
centerLabel: { type: 'string', description: 'donut/sunburst: caption under the center value.' },
|
|
280
|
+
cellHeight: number('heatmap: maximum cell height in px.'),
|
|
281
|
+
vars: { type: 'array', items: { type: 'string' }, description: 'corr/pairplot: the numeric fields to relate, in order (seaborn `vars`). Default: every numeric field (pairplot: the first 6). `y: [...]` works too.' },
|
|
282
|
+
method: { type: 'string', enum: ['pearson', 'spearman'], description: 'corr: correlation coefficient. "pearson" (default, linear) or "spearman" (ranks: monotonic relations, robust to outliers).' },
|
|
283
|
+
triangle: { anyOf: [{ type: 'string', enum: ['lower', 'full'] }, { type: 'boolean' }], description: 'corr: "lower" (default) shows each pair once, below the diagonal; "full" shows the whole symmetric matrix (default with `cluster`).' },
|
|
284
|
+
cluster: bool('corr: reorder variables by hierarchical clustering (average linkage on 1 − r) and draw the dendrogram on top, like sns.clustermap. `type: "clustermap"` sets it.'),
|
|
285
|
+
dendrogram: bool('corr with `cluster`: false hides the dendrogram but keeps the clustered order.'),
|
|
286
|
+
alpha: { type: 'number', exclusiveMinimum: 0, exclusiveMaximum: 1, description: 'corr: significance level (e.g. 0.05). Cells with p ≥ alpha are greyed out and a legend key explains it. p-values use Fisher\'s z.' },
|
|
287
|
+
cellSize: number('corr: maximum cell size in px (cells are square).'),
|
|
288
|
+
diag: { type: 'string', enum: ['hist', 'kde'], description: 'pairplot: what goes on the diagonal. Default "hist", or "kde" when `hue` is set.' },
|
|
289
|
+
upper: { anyOf: [{ type: 'string', enum: ['corr', 'scatter'] }, { type: 'boolean', enum: [false] }], description: 'pairplot: above the diagonal, "corr" (default: Pearson r, tinted by strength, per `hue` group when there is room), "scatter" (mirror), or false (empty).' },
|
|
290
|
+
corner: bool('pairplot: draw only the lower triangle and the diagonal (seaborn corner=True).'),
|
|
291
|
+
panelSize: number('pairplot: maximum panel size in px.'),
|
|
292
|
+
diverging: bool('heatmap/choropleth: diverging color scale around 0 (or `midpoint`).'),
|
|
158
293
|
|
|
159
294
|
// Appearance
|
|
160
295
|
colors: { type: 'object', additionalProperties: { type: 'string' }, description: 'Pin colors to entities: { "Biobío": "#3A47B8" }.' },
|
|
161
|
-
palette: { description: '
|
|
162
|
-
ramp: { description: 'Sequential ramp: a name ("indigo", "magenta", "teal", "amber") or a list of colors from light to dark.', ...strOrList },
|
|
296
|
+
palette: { description: 'Categorical colors: a name ("cdia", "okabe-ito", "tableau10", "udec"), a matplotlib qualitative palette ("tab10", "tab20", "Set1", "Set2", "Set3", "Pastel1", "Pastel2", "Paired", "Accent", "Dark2"), any colormap name (sampled, e.g. "viridis"), or a list of colors in fixed order.', ...strOrList },
|
|
297
|
+
ramp: { description: 'Sequential ramp: a name ("indigo", "magenta", "teal", "amber"), any colormap name, or a list of colors from light to dark.', ...strOrList },
|
|
298
|
+
colorScale: colorScaleSpec('Continuous color scale (heatmap, choropleth, treemap change, pointmap values): any matplotlib colormap name, case-insensitive, `_r` reverses ("viridis", "plasma", "inferno", "magma", "cividis", "Blues", "Greens", "YlOrRd", "RdBu", "coolwarm", "Spectral", "turbo", "twilight"…); diverging names imply a diverging scale. Or a list of hex colors, or { name | colors, reverse, mid }. Aliases: cmap, colormap. Qualitative palettes (tab10…) go in `palette`; avoid rainbow maps (jet, hsv).'),
|
|
299
|
+
cmap: colorScaleSpec('Alias of colorScale (matplotlib naming).'),
|
|
300
|
+
colormap: colorScaleSpec('Alias of colorScale.'),
|
|
163
301
|
theme: {
|
|
164
|
-
description: 'Theme: "papel" (light, default), "noche" (dark), or a JSON object deep-merged over `extends`.
|
|
165
|
-
anyOf: [
|
|
302
|
+
description: 'Theme: "papel" (light, default), "noche" (dark), a name registered with registerTheme(), or a JSON object deep-merged over `extends`. Simple tokens: background (alias of surface), text (alias of ink), accent (first series color), grid (a color or { color, dash, width }), axis, tooltip { bg, ink }, radius / barRadius (corner radius), font. Full keys: surface, ink, lede, muted, tint, dim, other, categorical (list, palette or colormap name), sequential, diverging, status {good, warning, critical}, font {family, mono, import}, type {title, subtitle, label, axis, legend, source, titleWeight}, marks {barRadius, barMax, lineWidth, pointRadius, gap, ring, areaOpacity, bandOpacity, dotOpacity}, axes {inside, grid}, layout {padding}. configure({ theme }) applies it to every chart.',
|
|
303
|
+
anyOf: [
|
|
304
|
+
{ type: 'string', description: '"papel", "noche", "light", "dark" or a registered theme name.' },
|
|
305
|
+
{ type: 'object', properties: {
|
|
306
|
+
extends: { type: 'string' }, mode: { type: 'string', enum: ['light', 'dark'] },
|
|
307
|
+
background: { type: 'string' }, surface: { type: 'string' }, text: { type: 'string' }, ink: { type: 'string' }, lede: { type: 'string' }, muted: { type: 'string' }, accent: { type: 'string' },
|
|
308
|
+
grid: { anyOf: [{ type: 'string' }, { type: 'object', properties: { color: { type: 'string' }, dash: { type: 'string' }, width: { type: 'number' } } }] },
|
|
309
|
+
axis: { type: 'string' }, tooltip: { type: 'object', properties: { bg: { type: 'string' }, ink: { type: 'string' } } },
|
|
310
|
+
radius: { type: 'number' }, barRadius: { type: 'number' },
|
|
311
|
+
categorical: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
312
|
+
sequential: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
313
|
+
diverging: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
314
|
+
font: { anyOf: [{ type: 'string' }, { type: 'object' }] },
|
|
315
|
+
status: { type: 'object' }, type: { type: 'object' }, marks: { type: 'object' }, axes: { type: 'object' }, layout: { type: 'object' },
|
|
316
|
+
} },
|
|
317
|
+
],
|
|
166
318
|
},
|
|
167
319
|
font: { description: 'Typeface for the whole chart: a Google Fonts name ("Poppins"), a CSS stack ("\'Inter\', sans-serif") or { family, import }. configure({ font }) sets it for every chart.', anyOf: [{ type: 'string' }, { type: 'object' }] },
|
|
168
320
|
textScale: { type: 'number', description: 'Multiplies every text size (1.5, 2…); the layout is recomputed so nothing overlaps. Aliases: fontScale, labelScale. configure({ textScale }) sets it for every chart.' },
|
|
169
321
|
width: { type: 'number', description: 'Width in px (default 720; render() uses the container width).' },
|
|
170
322
|
height: { type: 'number', description: 'Height in px. Automatic for horizontal bars, heatmaps, KPI tiles, and narrow treemaps, sunbursts and radars.' },
|
|
323
|
+
padding: number('Outer padding in px (default from the width).'),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** JSON Schema of a dashboard: dashboard({ title, kpis, charts, filters, … }). */
|
|
328
|
+
export const dashboardSchema = {
|
|
329
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
330
|
+
title: 'plotagents dashboard',
|
|
331
|
+
description: 'A dashboard with best-practice defaults: KPIs on top, the main chart wide, the rest in balanced full rows of a 12-column grid, one color per entity across charts, shared filters, one source line.',
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
title: { type: 'string', description: 'The question the dashboard answers, e.g. "How are southern sales doing?"' },
|
|
335
|
+
subtitle: { type: 'string', description: 'Scope: measure, unit, period.' },
|
|
336
|
+
source: { type: 'string', description: 'Data source, shown once at the bottom.' },
|
|
337
|
+
updated: { type: 'string', description: 'Last update, e.g. "2026-01-05".' },
|
|
338
|
+
locale: { type: 'string', enum: ['es', 'en', 'pt', 'fr', 'de', 'auto'] },
|
|
339
|
+
theme: { description: 'One theme for every chart: "papel" (light) or "noche" (dark), or a theme object.', anyOf: [{ type: 'string' }, { type: 'object' }] },
|
|
340
|
+
data: { type: 'array', items: { type: 'object' }, description: 'Shared rows: charts without their own `data` use these, and computed KPIs and filters work on them.' },
|
|
341
|
+
kpis: {
|
|
342
|
+
type: 'array', description: 'Up to 5 key figures. Fixed ({ label, value, delta, period }) or computed from `data` ({ label, field, agg, compare, trendBy }).',
|
|
343
|
+
items: { type: 'object', properties: {
|
|
344
|
+
label: { type: 'string' }, value: { type: 'number' }, delta: { type: 'number', description: 'Change as a fraction (0.12 = +12%).' }, period: { type: 'string' },
|
|
345
|
+
field: { type: 'string' }, agg: { type: 'string', enum: ['sum', 'mean', 'median', 'min', 'max', 'count', 'distinct', 'ratio'] }, over: { type: 'string', description: 'agg "ratio": denominator field.' },
|
|
346
|
+
scale: { type: 'number', description: 'Multiply the computed value (e.g. 1000 to show thousands).' },
|
|
347
|
+
compare: { type: 'object', description: 'Current vs previous period: { field: "year", current: 2025, previous: 2024 }.', properties: { field: { type: 'string' }, current: {}, previous: {} } },
|
|
348
|
+
trendBy: { type: 'string', description: 'Field to draw a sparkline by (e.g. "month").' },
|
|
349
|
+
previous: { type: 'number', description: 'Previous value: the change is computed from it.' }, display: { type: 'string', description: 'Text shown instead of the formatted value.' },
|
|
350
|
+
format: {}, good: { type: 'string', enum: ['up', 'down'] }, trend: { type: 'array', items: { type: 'number' } },
|
|
351
|
+
} },
|
|
352
|
+
},
|
|
353
|
+
charts: { type: 'array', items: { type: 'object' }, description: 'Chart specs (same as plot()). Optional per chart: `primary: true` (the main, wide chart), `size: "full" | "large" | "medium" | "small"` or `span` (of 12).' },
|
|
354
|
+
filters: { type: 'array', description: 'Fields to filter every chart and computed KPI by: "region" or { field, label, control: "buttons" | "select", value }.', items: { anyOf: [{ type: 'string' }, { type: 'object' }] } },
|
|
355
|
+
layout: { type: 'string', enum: ['auto', 'executive', 'grid'], description: 'auto (default): balanced rows by chart type. executive: main chart full width, the rest in pairs. grid: equal panels.' },
|
|
356
|
+
colors: { type: 'object', additionalProperties: { type: 'string' }, description: 'Pin colors to entities across every chart.' },
|
|
357
|
+
order: { type: 'string', enum: ['given'], description: '"given" keeps the charts in the order you wrote them (otherwise the main time series goes first).' },
|
|
358
|
+
note: { type: 'string', description: 'Short note next to the source, at the bottom.' },
|
|
359
|
+
font: { description: 'Font for every chart: a Google Fonts name, a CSS stack or { family, import }.', anyOf: [{ type: 'string' }, { type: 'object' }] },
|
|
360
|
+
palette: { description: 'Categorical palette for every chart: a name ("tab10", "okabe-ito") or a list of colors.', anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
361
|
+
ramp: { description: 'Sequential ramp for every chart.', anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
362
|
+
textScale: { type: 'number', description: 'Multiplies every text (1.5 for presentations).' },
|
|
171
363
|
},
|
|
172
364
|
};
|
|
173
365
|
|
|
174
366
|
/** Tool definition ready for Claude (Anthropic) or OpenAI. */
|
|
175
|
-
export function toolDefinition({ name = '
|
|
367
|
+
export function toolDefinition({ name, flavor = 'anthropic', kind = 'chart' } = {}) {
|
|
368
|
+
if (kind === 'dashboard') {
|
|
369
|
+
const description = 'Builds a self-contained, interactive HTML dashboard with best-practice defaults: up to 5 KPIs on top (fixed or computed from data, with year-over-year change), the main chart wide, the rest in balanced rows, the same color for the same entity in every chart, and filters as buttons. Each chart is a plotagents spec with a `title` that states its finding.';
|
|
370
|
+
const { $schema: _s, title: _t, ...params } = dashboardSchema;
|
|
371
|
+
const n = name ?? 'dashboard';
|
|
372
|
+
return flavor === 'openai' ? { type: 'function', function: { name: n, description, parameters: params } } : { name: n, description, input_schema: params };
|
|
373
|
+
}
|
|
374
|
+
name = name ?? 'plot';
|
|
176
375
|
const description =
|
|
177
|
-
'Draws an elegant, accessible chart from tabular data. Pass rows in `data`, name the fields (x, y, series/hue…) and write a `title` that states the finding. Returns an SVG. Types: bar, count, line, area, scatter, bubble, regplot, residplot, jointplot, hist, kde, ecdf, box, violin, strip, swarm, point, radar, treemap, sunburst, donut, pie, heatmap, sankey, kpi, choropleth (Chile regions
|
|
376
|
+
'Draws an elegant, accessible chart from tabular data. Pass rows in `data`, name the fields (x, y, series/hue…) and write a `title` that states the finding. Returns an SVG. Types: bar, count, line, area, wordcloud (words sized by frequency, or raw text to count), gantt (tasks with start/end, phases, progress, dependencies, milestones), scatter, bubble, regplot, residplot, jointplot, corr (correlation matrix of numeric fields, optional clustering: clustermap), pairplot (scatter-plot matrix with distributions and r), hist, kde, ecdf, box, violin, strip, swarm, point, radar, treemap, sunburst, donut, pie, heatmap, sankey, kpi, table (exact values with per-column indicators: data bars, ▲/▼ changes, cell colors, sparklines, progress meters, status pills; paginated, cards on phones), choropleth (Chile regions, US states and world countries built in, or any GeoJSON/TopoJSON in `geo`, with markers, layers and fitTo), symbolmap, pointmap (lat/lon dots over a tile basemap), network, chord, pyramid, flowchart (seaborn names accepted). `facet` makes panels; `time` shows one period (add `slider: true` for controls). `data` is required except for kpi (`items`), flowchart (`nodes`/`edges`), chord (`matrix`) and wordcloud raw `text`. Set `locale: "en"` for English number formats.';
|
|
178
377
|
const { $schema, $id, title, ...params } = schema;
|
|
179
378
|
if (flavor === 'openai') return { type: 'function', function: { name, description, parameters: params } };
|
|
180
379
|
return { name, description, input_schema: params };
|