plotagents 0.0.0-stage → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/AGENTS.md +98 -0
  2. package/LICENSE +21 -0
  3. package/README.md +217 -2
  4. package/bin/plotagents.js +73 -0
  5. package/dist/plotagents-geo.js +79 -0
  6. package/dist/plotagents.js +11611 -0
  7. package/docs/api.md +172 -0
  8. package/docs/charts.md +352 -0
  9. package/docs/colorscales.md +46 -0
  10. package/docs/dashboards.md +79 -0
  11. package/docs/getting-started.md +142 -0
  12. package/docs/rendering.md +47 -0
  13. package/docs/themes.md +116 -0
  14. package/index.d.ts +384 -0
  15. package/package.json +80 -4
  16. package/schema.json +1874 -0
  17. package/src/axes.js +152 -0
  18. package/src/charts/bar.js +350 -0
  19. package/src/charts/categorical.js +495 -0
  20. package/src/charts/chord.js +218 -0
  21. package/src/charts/choropleth.js +807 -0
  22. package/src/charts/dist.js +530 -0
  23. package/src/charts/donut.js +232 -0
  24. package/src/charts/flowchart.js +407 -0
  25. package/src/charts/gantt.js +460 -0
  26. package/src/charts/heatmap.js +76 -0
  27. package/src/charts/kpi.js +100 -0
  28. package/src/charts/line.js +551 -0
  29. package/src/charts/network.js +707 -0
  30. package/src/charts/pointmap.js +599 -0
  31. package/src/charts/pyramid.js +177 -0
  32. package/src/charts/radar.js +158 -0
  33. package/src/charts/regplot.js +370 -0
  34. package/src/charts/sankey.js +169 -0
  35. package/src/charts/scatter.js +214 -0
  36. package/src/charts/sunburst.js +136 -0
  37. package/src/charts/treemap.js +197 -0
  38. package/src/charts/wordcloud.js +163 -0
  39. package/src/colorscales.js +182 -0
  40. package/src/context.js +10 -0
  41. package/src/dashboard.js +438 -0
  42. package/src/dashed.js +90 -0
  43. package/src/data.js +137 -0
  44. package/src/facet.js +84 -0
  45. package/src/format.js +203 -0
  46. package/src/frame.js +226 -0
  47. package/src/geo/chile.js +7 -0
  48. package/src/geo/data/chile-regiones.js +12 -0
  49. package/src/geo/data/usa-estados.js +12 -0
  50. package/src/geo/data/world.js +12 -0
  51. package/src/geo/projection.js +217 -0
  52. package/src/geo/registry.js +110 -0
  53. package/src/geo/usa.js +8 -0
  54. package/src/geo/world.js +7 -0
  55. package/src/hierarchy.js +139 -0
  56. package/src/index.js +336 -0
  57. package/src/infer.js +58 -0
  58. package/src/interact.js +483 -0
  59. package/src/lint.js +417 -0
  60. package/src/palette.js +59 -0
  61. package/src/runtime.js +416 -0
  62. package/src/scale.js +241 -0
  63. package/src/schema.js +337 -0
  64. package/src/stats.js +303 -0
  65. package/src/svg.js +85 -0
  66. package/src/text.js +61 -0
  67. package/src/theme.js +311 -0
package/src/index.js ADDED
@@ -0,0 +1,336 @@
1
+ // plotagents · gráficos bellos con una sola llamada, pensados para agentes.
2
+ //
3
+ // import { plot } from 'plotagents';
4
+ // const svg = plot({ data, x: 'region', y: 'ventas', title: 'Biobío lidera las ventas' });
5
+ //
6
+ import { getTheme, themeFor, themes, palettes, ramps, colorMap, fontSpec, deepMerge, registerTheme, isColor, FONT_CSS } from './theme.js';
7
+ import { withContext } from './context.js';
8
+ import { validatePalette } from './palette.js';
9
+ import { bar } from './charts/bar.js';
10
+ import { line } from './charts/line.js';
11
+ import { scatter } from './charts/scatter.js';
12
+ import { treemap, squarify } from './charts/treemap.js';
13
+ import { donut, pie } from './charts/donut.js';
14
+ import { sunburst } from './charts/sunburst.js';
15
+ import { hist, kde, ecdf } from './charts/dist.js';
16
+ import { regplot, residplot, jointplot } from './charts/regplot.js';
17
+ import { box, violin, strip, swarm, point } from './charts/categorical.js';
18
+ import { radar } from './charts/radar.js';
19
+ import { network } from './charts/network.js';
20
+ import { chord } from './charts/chord.js';
21
+ import { pyramid } from './charts/pyramid.js';
22
+ import { flowchart } from './charts/flowchart.js';
23
+ import { gantt } from './charts/gantt.js';
24
+ import { wordcloud, countWords } from './charts/wordcloud.js';
25
+ import { choropleth, symbolMap } from './charts/choropleth.js';
26
+ import { pointmap, embedTiles, TILE_PROVIDERS } from './charts/pointmap.js';
27
+ import { registerGeo, getGeo, registeredGeo, toFeatureCollection } from './geo/registry.js';
28
+ // Mapas incluidos (Chile por regiones, estados de EE. UU. y países del mundo): listos sin importar nada más.
29
+ // El bundle del navegador los separa en dist/plotagents-geo.js.
30
+ import './geo/chile.js';
31
+ import './geo/world.js';
32
+ import './geo/usa.js';
33
+ import { heatmap } from './charts/heatmap.js';
34
+ import { sankey } from './charts/sankey.js';
35
+ import { kpi } from './charts/kpi.js';
36
+ import { ensureData, PlotError, fieldKind, uniq, num } from './data.js';
37
+ import { facet } from './facet.js';
38
+ import { parseDate } from './format.js';
39
+ import { inferType, TYPES, ALIASES, normalizeType } from './infer.js';
40
+ import { lint, _setProbe } from './lint.js';
41
+ import { schema, toolDefinition, dashboardSchema } from './schema.js';
42
+ import { attach, attachSource } from './interact.js';
43
+ import { render, _bind, loadGeo } from './runtime.js';
44
+ import { dashboard, renderDashboard, planDashboard, lintDashboard, safeDashboard, mountDashboard, packRows, balancedRows, _bindDashboard } from './dashboard.js';
45
+ import { makeFormat, formatNumber, formatCompact } from './format.js';
46
+ import { esc } from './svg.js';
47
+ import { resolveColorScale, colorScaleNames, interpolate as interpolateColor, COLORMAPS, QUALITATIVE } from './colorscales.js';
48
+
49
+ const RENDERERS = {
50
+ bar: (s, t) => bar(s, t),
51
+ column: (s, t) => bar({ ...s, horizontal: false }, t),
52
+ count: (s, t) => bar({ aggregate: 'count', ...s }, t),
53
+ line: (s, t) => line(s, t, 'line'),
54
+ area: (s, t) => line(s, t, 'area'),
55
+ scatter: (s, t) => scatter(s, t),
56
+ bubble: (s, t) => scatter(s, t),
57
+ treemap: (s, t) => treemap(s, t),
58
+ sunburst: (s, t) => sunburst(s, t),
59
+ donut: (s, t) => donut(s, t),
60
+ pie: (s, t) => pie(s, t),
61
+ heatmap: (s, t) => heatmap(s, t),
62
+ sankey: (s, t) => sankey(s, t),
63
+ kpi: (s, t) => kpi(s, t),
64
+ hist: (s, t) => hist(s, t),
65
+ kde: (s, t) => kde(s, t),
66
+ ecdf: (s, t) => ecdf(s, t),
67
+ regplot: (s, t) => regplot(s, t),
68
+ residplot: (s, t) => residplot(s, t),
69
+ jointplot: (s, t) => jointplot(s, t),
70
+ box: (s, t) => box(s, t),
71
+ violin: (s, t) => violin(s, t),
72
+ strip: (s, t) => strip(s, t),
73
+ swarm: (s, t) => swarm(s, t),
74
+ point: (s, t) => point(s, t),
75
+ radar: (s, t) => radar(s, t),
76
+ network: (s, t) => network(s, t),
77
+ chord: (s, t) => chord(s, t),
78
+ pyramid: (s, t) => pyramid(s, t),
79
+ flowchart: (s, t) => flowchart(s, t),
80
+ gantt: (s, t) => gantt(s, t),
81
+ wordcloud: (s, t) => wordcloud(s, t),
82
+ choropleth: (s, t) => choropleth(s, t),
83
+ symbolmap: (s, t) => symbolMap(s, t),
84
+ pointmap: (s, t) => pointmap(s, t),
85
+ };
86
+
87
+
88
+
89
+ /** Normaliza el spec: tipo inferido y datos como filas. */
90
+ export function resolve(spec = {}) {
91
+ if (!spec || typeof spec !== 'object') throw new PlotError('El spec debe ser un objeto.', 'plot({ data: [...], x: "campo", y: "campo" })');
92
+ const s = withDefaults(spec);
93
+ // refLines / annotations: un objeto suelto vale como lista de uno.
94
+ for (const k of ['refLines', 'annotations']) if (s[k] != null && !Array.isArray(s[k]) && typeof s[k] === 'object') s[k] = [s[k]];
95
+ if (s.color && !s.series && typeof s.color === 'string' && !s.color.startsWith('#')) s.series = s.color;
96
+ let type = s.type && s.type !== 'auto' ? String(s.type).toLowerCase() : null;
97
+ if (type) {
98
+ const [t, extra] = normalizeType(type);
99
+ type = t;
100
+ for (const [k, v] of Object.entries(extra)) if (s[k] === undefined) s[k] = v;
101
+ }
102
+ if (!type) {
103
+ const data = s.items || (!s.data && (typeof s.text === 'string' || Array.isArray(s.text))) ? [] : ensureData(s);
104
+ type = inferType(s, data);
105
+ }
106
+ if (!RENDERERS[type]) throw new PlotError(`Tipo de gráfico "${s.type}" no soportado.`, `Usa uno de: ${TYPES.join(', ')} o "auto".`);
107
+ s.type = type;
108
+ // Colormap divergente (RdBu, coolwarm…): centrada en 0 si los datos lo cruzan; si no, en el medio.
109
+ const cs = resolveColorScale(s.colorScale ?? s.cmap ?? s.colormap);
110
+ if (cs && cs.kind === 'diverging' && s.midpoint === undefined && s.diverging === undefined) {
111
+ const vf = typeof s.colorScale === 'object' && s.colorScale.mid !== undefined ? null : s.value;
112
+ if (typeof s.colorScale === 'object' && s.colorScale?.mid !== undefined) s.midpoint = +s.colorScale.mid;
113
+ else if (vf && Array.isArray(s.data) && s.data.length && typeof s.data[0] === 'object') {
114
+ const vs = s.data.map((r) => num(r[vf])).filter(Number.isFinite);
115
+ if (vs.length) { let lo = Infinity, hi = -Infinity; for (const v of vs) { if (v < lo) lo = v; if (v > hi) hi = v; } s.midpoint = lo < 0 && hi > 0 ? 0 : (lo + hi) / 2; }
116
+ } else s.diverging = true;
117
+ }
118
+ return s;
119
+ }
120
+
121
+ /**
122
+ * Dibuja un gráfico y devuelve un SVG (texto). Funciona en Node y navegador.
123
+ * Lanza PlotError con una pista de corrección si el spec no se puede dibujar.
124
+ */
125
+ export function plot(spec) {
126
+ let s = resolve(spec);
127
+ s = applyTime(s);
128
+ s = applyExclude(s);
129
+ const theme = themeFor(s);
130
+ const textScale = Math.max(0.5, Math.min(4, +(s.textScale ?? s.fontScale ?? s.labelScale ?? 1) || 1));
131
+ return withContext({ textScale }, () => {
132
+ if (s.facet || s.col) return facet(s, theme, (p) => RENDERERS[s.type](p, themeFor(p)));
133
+ return RENDERERS[s.type](s, theme);
134
+ });
135
+ }
136
+
137
+ // ── Configuración global ──────────────────────────────────────────────────
138
+ const DEFAULTS = {};
139
+
140
+ /**
141
+ * Valores por defecto para TODOS los gráficos (el spec siempre gana):
142
+ * configure({ font: 'Poppins', textScale: 1.5, locale: 'en', theme: 'noche', palette: 'okabe-ito' })
143
+ * `font` acepta un nombre de Google Fonts, una pila CSS o { family, import }.
144
+ * `textScale` multiplica todos los textos (1.5, 2…) y el layout se recalcula.
145
+ * configure() sin argumentos devuelve la configuración actual; configure(null) la borra.
146
+ */
147
+ export function configure(options) {
148
+ if (options === null) { for (const k of Object.keys(DEFAULTS)) delete DEFAULTS[k]; return {}; }
149
+ if (options) Object.assign(DEFAULTS, options);
150
+ return { ...DEFAULTS };
151
+ }
152
+
153
+ function withDefaults(spec) {
154
+ const s = { ...DEFAULTS, ...spec };
155
+ // Colores fijados por entidad: solo colores CSS válidos (viajan a atributos y al tooltip).
156
+ if (s.colors && typeof s.colors === 'object') s.colors = Object.fromEntries(Object.entries(s.colors).filter(([, c]) => isColor(c)));
157
+ // Tema global + tema del spec: el del spec se fusiona en profundidad sobre el global.
158
+ if (DEFAULTS.theme && spec.theme && typeof spec.theme === 'object' && spec.theme.extends === undefined) {
159
+ s.theme = typeof DEFAULTS.theme === 'string' ? { ...spec.theme, extends: DEFAULTS.theme } : deepMerge(DEFAULTS.theme, spec.theme);
160
+ }
161
+ // `font`: atajo de theme.font (familia + URL para cargarla).
162
+ const f = fontSpec(s.font);
163
+ if (f) {
164
+ const base = s.theme && typeof s.theme === 'object' ? s.theme : { extends: s.theme || undefined };
165
+ s.theme = { ...base, font: { ...(base.font || {}), ...f } };
166
+ }
167
+ return s;
168
+ }
169
+
170
+ const TIME_AXIS = new Set(['line', 'area', 'ecdf']);
171
+
172
+ /** Valores ordenados del campo `time` (para sliders y animación). */
173
+ export function timeValues(spec) {
174
+ const s = spec.type ? spec : resolve(spec);
175
+ if (!s.time) return [];
176
+ const data = ensureData(s);
177
+ const k = fieldKind(data, s.time);
178
+ const vals = uniq(data.map((r) => r[s.time]).filter((v) => v !== null && v !== undefined && v !== ''));
179
+ return vals.sort((a, b) => (k === 'date' ? parseDate(a) - parseDate(b) : k === 'year' || k === 'number' ? num(a) - num(b) : 0));
180
+ }
181
+
182
+ /**
183
+ * `time` (estilo d3plus): en gráficos que no tienen el tiempo en el eje x,
184
+ * muestra un solo periodo (`timeValue`, por defecto el último). El periodo se
185
+ * agrega a la bajada; en el navegador, render() suma un slider con "play".
186
+ */
187
+ function applyTime(s) {
188
+ if (!s.time || TIME_AXIS.has(s.type) || s.x === s.time) return s;
189
+ const vals = timeValues(s);
190
+ if (!vals.length) return s;
191
+ const cur = s.timeValue ?? vals[vals.length - 1];
192
+ const all = ensureData(s);
193
+ const data = all.filter((r) => String(r[s.time]) === String(cur));
194
+ const label = String(cur);
195
+ // Periodo de comparación para variaciones (treemap y otros): el anterior, o el primero con compare: "first".
196
+ const idx = vals.findIndex((v) => String(v) === String(cur));
197
+ const pv = s.compare === 'first' ? vals[0] : s.compare !== undefined && s.compare !== 'previous' && s.compare !== false ? s.compare : vals[idx - 1];
198
+ const _prev = s.compare !== false && pv !== undefined && String(pv) !== String(cur) ? { data: all.filter((r) => String(r[s.time]) === String(pv)), label: String(pv) } : undefined;
199
+ return { ...s, data, colors: stableColors(s, all), subtitle: s.subtitle ? `${s.subtitle} · ${label}` : label, _time: { field: s.time, value: cur, values: vals }, _prev };
200
+ }
201
+
202
+ /** Campo que define las entradas de la leyenda (series, primer nivel, grupo…). */
203
+ function legendField(s) {
204
+ if (Array.isArray(s.path) && s.path.length) return s.path[0];
205
+ return s.series ?? s.hue ?? s.group ?? (['donut', 'pie'].includes(s.type) ? s.label ?? s.x : null) ?? (s.type === 'treemap' || s.type === 'sunburst' ? s.label ?? s.x : null);
206
+ }
207
+
208
+ /**
209
+ * `exclude: ["Maule"]` (o shift + clic en la leyenda): quita esas entradas y
210
+ * recalcula ejes, participaciones y layout con lo que queda. Los colores no
211
+ * se mueven y las entradas excluidas siguen en la leyenda, apagadas.
212
+ */
213
+ function applyExclude(s) {
214
+ const ex = s.exclude == null || s.exclude === false ? [] : [].concat(s.exclude).map(String);
215
+ if (!ex.length) return s;
216
+ // Tabla ancha: y: ["ingresos", "costos"] → se excluyen columnas.
217
+ if (Array.isArray(s.y) && !s.series && !s.hue) {
218
+ const keep = s.y.filter((k) => !ex.includes(String(k)));
219
+ if (!keep.length || keep.length === s.y.length) return s;
220
+ const cmap = colorMap(s.y, themeFor(s), s.colors);
221
+ return { ...s, y: keep, colors: { ...Object.fromEntries(cmap), ...(s.colors || {}) }, _excluded: s.y.filter((k) => ex.includes(String(k))) };
222
+ }
223
+ const f = legendField(s);
224
+ if (!f || s.items) return s;
225
+ const all = ensureData(s);
226
+ if (!all.length || !(f in all[0])) return s;
227
+ const keep = all.filter((r) => !ex.includes(String(r[f])));
228
+ if (!keep.length || keep.length === all.length) return s;
229
+ const present = new Set(all.map((r) => String(r[f])));
230
+ return { ...s, data: keep, colors: { ...stableColors(s, all), ...(s.colors || {}) }, _excluded: ex.filter((v) => present.has(v)) };
231
+ }
232
+
233
+ /**
234
+ * Colores fijos por entidad a lo largo de TODOS los periodos: al mover el
235
+ * slider, "Minería" sigue siendo índigo aunque cambie su tamaño o su orden.
236
+ */
237
+ function stableColors(s, all) {
238
+ const idf = s.series ?? s.hue ?? (Array.isArray(s.path) ? s.path[0] : null) ?? s.group ?? s.from
239
+ ?? (['donut', 'pie'].includes(s.type) ? s.label ?? s.x : null);
240
+ if (!idf || !all.length || !(idf in all[0])) return s.colors;
241
+ const vf = s.value ?? (typeof s.y === 'string' ? s.y : null);
242
+ const tot = new Map();
243
+ for (const r of all) {
244
+ const k = String(r[idf]);
245
+ tot.set(k, (tot.get(k) || 0) + (vf && Number.isFinite(num(r[vf])) ? Math.abs(num(r[vf])) : 1));
246
+ }
247
+ const keys = [...tot.keys()].sort((a, b) => tot.get(b) - tot.get(a));
248
+ const cmap = colorMap(keys, themeFor(s), s.colors);
249
+ return { ...Object.fromEntries(cmap), ...(s.colors || {}) };
250
+ }
251
+
252
+ /**
253
+ * Versión que nunca lanza: ideal como herramienta de un agente.
254
+ * Devuelve { ok, svg, type, description, errors, warnings, suggestions }.
255
+ */
256
+ export function safePlot(spec) {
257
+ const report = lint(spec || {});
258
+ try {
259
+ const svg = plot(spec);
260
+ // Se dibujó (con valores de respaldo), pero los errores de lint igual se devuelven: ok = sin errores.
261
+ return { ok: report.errors.length === 0, svg, type: report.type, description: describeSvg(svg), errors: report.errors, warnings: report.warnings, suggestions: report.suggestions };
262
+ } catch (e) {
263
+ const errors = report.errors.length ? report.errors : [{ code: 'render', message: e.message.split('\n')[0], fix: e.hint }];
264
+ return { ok: false, svg: null, type: report.type, description: '', errors, warnings: report.warnings, suggestions: report.suggestions };
265
+ }
266
+ }
267
+
268
+ /** Descripción en texto del gráfico (también va en <desc> del SVG). */
269
+ export function describe(spec) {
270
+ return describeSvg(plot(spec));
271
+ }
272
+
273
+ function describeSvg(svg) {
274
+ const m = /<desc[^>]*>([\s\S]*?)<\/desc>/.exec(svg);
275
+ return m ? m[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'") : '';
276
+ }
277
+
278
+ /**
279
+ * Página HTML autónoma (un archivo) con uno o varios gráficos interactivos:
280
+ * tooltips, slider temporal y navegación por niveles. Incluye la biblioteca
281
+ * en línea (dist/plotagents.js) cuando está disponible; si no, solo tooltips.
282
+ */
283
+ export function toHTML(specs, { title = 'Gráficos', lang = 'es', columns = 1 } = {}) {
284
+ const list = Array.isArray(specs) ? specs : [specs];
285
+ const themesOf = list.map((s) => themeFor(withDefaults(s)));
286
+ const dark = themesOf.every((t) => t.mode === 'dark');
287
+ // Fondo de la página = fondo del tema (también uno global de configure()).
288
+ const bg = themesOf.every((t) => t.surface === themesOf[0].surface) ? themesOf[0].surface : dark ? '#0B0F2B' : '#FCFBF9';
289
+ const needsGeo = list.some((s) => { try { return ['choropleth', 'symbolmap', 'pointmap'].includes(resolve(s).type); } catch { return false; } });
290
+ const bundle = bundleSource() && (needsGeo ? bundleSource() + '\n' + (bundleSource('plotagents-geo.js') || '') : bundleSource());
291
+ const safeJSON = (o) => JSON.stringify(o).replace(/</g, '\\u003c');
292
+ const charts = list.map((s, i) => `<figure class="pa" id="pa-${i}">${plot(s)}</figure>` + (bundle ? `<script type="application/json" id="pa-spec-${i}">${safeJSON(s)}</script>` : '')).join('\n');
293
+ const boot = bundle
294
+ ? `<script>${bundle.replace(/<\/(script)/gi, '<\\/$1')}</script>\n<script>document.querySelectorAll('figure.pa').forEach(function(f,i){var s=JSON.parse(document.getElementById('pa-spec-'+i).textContent);plotagents.render(f,s);});</script>`
295
+ : `<script>document.querySelectorAll('figure.pa').forEach(function(f){(${attachSource})(f)});</script>`;
296
+ return `<!doctype html>
297
+ <html lang="${esc(lang)}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
298
+ <title>${esc(title)}</title>
299
+ ${[...new Set([FONT_CSS, ...list.map((s) => themeFor(withDefaults(s)).font?.import).filter(Boolean)])].map((u) => `<link rel="stylesheet" href="${esc(u)}">`).join('\n')}
300
+ <style>
301
+ :root{color-scheme:${dark ? 'dark' : 'light'}}
302
+ body{margin:0;background:${bg};font-family:${esc(themeFor(withDefaults(list[0])).font?.family || "'Mozilla Text',system-ui,sans-serif")}}
303
+ main{max-width:${columns > 1 ? 1500 : 820}px;margin:0 auto;padding:24px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,${columns > 1 ? 560 : 320}px),1fr));gap:24px}
304
+ figure.pa{margin:0;min-width:0}
305
+ figure.pa svg{width:100%;height:auto;display:block}
306
+ </style></head>
307
+ <body><main>
308
+ ${charts}
309
+ </main>
310
+ ${boot}
311
+ </body></html>`;
312
+ }
313
+
314
+ /** Código de la biblioteca en un archivo (para incrustar en HTML). */
315
+ export function bundleSource(file = 'plotagents.js') {
316
+ try {
317
+ const fs = globalThis.process?.getBuiltinModule?.('node:fs');
318
+ if (!fs) return null;
319
+ return fs.readFileSync(new URL('../dist/' + file, import.meta.url), 'utf8');
320
+ } catch {
321
+ return null;
322
+ }
323
+ }
324
+
325
+ _bind(plot, resolve);
326
+ _bindDashboard({ plot, resolve, lint, bundleSource, resolveDefaults: withDefaults });
327
+ _setProbe((s) => plot(s));
328
+ export { render };
329
+
330
+ export { countWords, dashboardSchema };
331
+ export { resolveColorScale, colorScaleNames, interpolateColor, COLORMAPS, QUALITATIVE };
332
+ export { dashboard, renderDashboard, planDashboard, lintDashboard, safeDashboard, mountDashboard, packRows, balancedRows };
333
+ export { RENDERERS, ALIASES, registerGeo, getGeo, registeredGeo, toFeatureCollection, loadGeo };
334
+ export { registerTheme, withDefaults as resolveDefaults, embedTiles, TILE_PROVIDERS };
335
+ export { lint, schema, toolDefinition, themes, palettes, ramps, validatePalette, getTheme, themeFor, attach, PlotError, TYPES, squarify, makeFormat, formatNumber, formatCompact };
336
+ export default plot;
package/src/infer.js ADDED
@@ -0,0 +1,58 @@
1
+ // Elige el tipo de gráfico cuando el agente no lo indica (type: "auto").
2
+ import { fieldsOf, fieldKind } from './data.js';
3
+
4
+ export const TYPES = ['bar', 'column', 'count', 'line', 'area', 'scatter', 'bubble', 'treemap', 'sunburst', 'donut', 'pie', 'heatmap', 'sankey', 'kpi', 'hist', 'kde', 'ecdf', 'regplot', 'residplot', 'jointplot', 'box', 'violin', 'strip', 'swarm', 'point', 'radar', 'network', 'chord', 'pyramid', 'flowchart', 'gantt', 'wordcloud', 'choropleth', 'symbolmap', 'pointmap'];
5
+
6
+ // Nombres de seaborn, d3plus y otros: el agente puede usar el que conozca.
7
+ export const ALIASES = {
8
+ barplot: ['bar', { ci: true }], countplot: 'count', columns: 'column', lineplot: 'line', areaplot: 'area',
9
+ scatterplot: 'scatter', histplot: 'hist', histogram: 'hist', kdeplot: 'kde', density: 'kde', ecdfplot: 'ecdf',
10
+ lmplot: 'regplot', reg: 'regplot', regression: 'regplot', joint: 'jointplot', boxplot: 'box', violinplot: 'violin',
11
+ stripplot: 'strip', swarmplot: 'swarm', beeswarm: 'swarm', pointplot: 'point', spider: 'radar', doughnut: 'donut',
12
+ pointmap: 'pointmap', dotmap: 'pointmap', 'dot-map': 'pointmap', tilemap: 'pointmap', basemap: 'pointmap', 'mapa-puntos': 'pointmap', 'mapa-de-puntos': 'pointmap', latlon: 'pointmap',
13
+ graph: 'network', force: 'network', red: 'network', 'chord-diagram': 'chord', cuerdas: 'chord',
14
+ populationpyramid: 'pyramid', population: 'pyramid', piramide: 'pyramid', 'pirámide': 'pyramid',
15
+ flow: 'flowchart', diagram: 'flowchart', process: 'flowchart', diagrama: 'flowchart',
16
+ map: 'choropleth', mapa: 'choropleth', choroplethmap: 'choropleth', symbol: 'symbolmap', bubblemap: 'symbolmap', 'mapa-simbolos': 'symbolmap',
17
+ words: 'wordcloud', 'word-cloud': 'wordcloud', wordclouds: 'wordcloud', tagcloud: 'wordcloud', 'tag-cloud': 'wordcloud', nube: 'wordcloud', 'nube-de-palabras': 'wordcloud', cloud: 'wordcloud',
18
+ timeline: 'gantt', roadmap: 'gantt', schedule: 'gantt', cronograma: 'gantt', 'carta-gantt': 'gantt', ganttchart: 'gantt', 'gantt-chart': 'gantt', planning: 'gantt',
19
+ tree: 'treemap', stat: 'kpi', stats: 'kpi', number: 'kpi', heat: 'heatmap', flow: 'sankey',
20
+ };
21
+
22
+ /** Tipo canónico + opciones implícitas del alias (p. ej. barplot → bar con ci). */
23
+ export function normalizeType(t) {
24
+ const k = String(t || '').toLowerCase();
25
+ const al = ALIASES[k];
26
+ if (!al) return [k, {}];
27
+ return Array.isArray(al) ? [al[0], al[1]] : [al, {}];
28
+ }
29
+
30
+ export function inferType(spec, data = []) {
31
+ if (spec.items) return 'kpi';
32
+ if (spec.geo) return 'choropleth';
33
+ if (spec.lat || spec.lon || spec.tiles !== undefined) return 'pointmap';
34
+ { const f = fieldsOf(data); if (f.some((k) => /^(lat|latitude|latitud)$/i.test(k)) && f.some((k) => /^(lon|lng|long|longitude|longitud)$/i.test(k))) return 'pointmap'; }
35
+ if ((typeof spec.text === 'string' && !(data[0] && spec.text in data[0])) || (Array.isArray(spec.text) && spec.text.length && typeof spec.text[0] === 'string') || spec.textField) return 'wordcloud';
36
+ if (spec.start !== undefined && (spec.end !== undefined || spec.duration !== undefined) && typeof spec.start === 'string') return 'gantt';
37
+ if (spec.nodes && spec.nodes.some?.((n) => n && n.type)) return 'flowchart';
38
+ if (spec.nodes) return 'network';
39
+ if (spec.matrix) return 'chord';
40
+ if (spec.from || spec.to) return 'sankey';
41
+ if (spec.path || spec.group) return 'treemap';
42
+ if (spec.size) return 'bubble';
43
+ const fields = fieldsOf(data);
44
+ if ((fields.includes('source') && fields.includes('target')) || (fields.includes('from') && fields.includes('to'))) return 'sankey';
45
+ const kinds = Object.fromEntries(fields.map((f) => [f, fieldKind(data, f)]));
46
+ // Dos fechas por fila (inicio y fin) y un nombre: un cronograma.
47
+ if (fields.filter((f) => kinds[f] === 'date').length >= 2 && fields.some((f) => kinds[f] === 'category') && !spec.y && !spec.series) return 'gantt';
48
+ const x = spec.x ?? fields[0];
49
+ const xk = kinds[x];
50
+ if (xk === 'date' || xk === 'year') return 'line';
51
+ if (xk === 'number') {
52
+ const y = spec.y ?? fields.find((f) => f !== x && kinds[f] === 'number');
53
+ if (y && kinds[y] === 'number') return 'scatter';
54
+ }
55
+ const cats = fields.filter((f) => kinds[f] === 'category');
56
+ if (cats.length >= 2 && !spec.series && fields.some((f) => kinds[f] === 'number') && spec.value) return 'heatmap';
57
+ return 'bar';
58
+ }