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.
Files changed (67) hide show
  1. package/AGENTS.md +29 -5
  2. package/README.md +44 -15
  3. package/bin/plotagents.js +40 -14
  4. package/dist/plotagents-geo.js +1 -1
  5. package/dist/plotagents.js +6161 -757
  6. package/docs/api.md +23 -5
  7. package/docs/charts.md +229 -10
  8. package/docs/colorscales.md +46 -0
  9. package/docs/dashboards.md +79 -0
  10. package/docs/es/agents.md +102 -0
  11. package/docs/es/api.md +172 -0
  12. package/docs/es/charts.md +436 -0
  13. package/docs/es/colorscales.md +46 -0
  14. package/docs/es/dashboards.md +79 -0
  15. package/docs/es/getting-started.md +142 -0
  16. package/docs/es/rendering.md +47 -0
  17. package/docs/es/themes.md +116 -0
  18. package/docs/getting-started.md +6 -2
  19. package/docs/themes.md +39 -9
  20. package/index.d.ts +275 -20
  21. package/package.json +11 -4
  22. package/schema.json +1382 -53
  23. package/skills/plotagents/SKILL.md +105 -0
  24. package/src/axes.js +11 -3
  25. package/src/charts/bar.js +40 -21
  26. package/src/charts/categorical.js +31 -7
  27. package/src/charts/chord.js +6 -4
  28. package/src/charts/choropleth.js +287 -37
  29. package/src/charts/corr.js +512 -0
  30. package/src/charts/dist.js +39 -21
  31. package/src/charts/donut.js +4 -2
  32. package/src/charts/gantt.js +460 -0
  33. package/src/charts/heatmap.js +2 -2
  34. package/src/charts/kpi.js +11 -8
  35. package/src/charts/line.js +187 -43
  36. package/src/charts/network.js +557 -189
  37. package/src/charts/pointmap.js +602 -0
  38. package/src/charts/radar.js +7 -1
  39. package/src/charts/regplot.js +16 -4
  40. package/src/charts/sankey.js +42 -6
  41. package/src/charts/scatter.js +103 -23
  42. package/src/charts/sunburst.js +7 -6
  43. package/src/charts/table.js +882 -0
  44. package/src/charts/treemap.js +12 -9
  45. package/src/charts/wordcloud.js +177 -0
  46. package/src/colorscales.js +193 -0
  47. package/src/dashboard.js +451 -0
  48. package/src/dashed.js +1 -1
  49. package/src/data.js +4 -2
  50. package/src/facet.js +4 -3
  51. package/src/format.js +80 -25
  52. package/src/frame.js +101 -29
  53. package/src/geo/projection.js +76 -9
  54. package/src/geo/registry.js +87 -8
  55. package/src/hierarchy.js +6 -0
  56. package/src/i18n.js +107 -0
  57. package/src/index.js +92 -14
  58. package/src/infer.js +18 -1
  59. package/src/interact.js +157 -36
  60. package/src/lint.js +353 -11
  61. package/src/runtime.js +301 -101
  62. package/src/scale.js +82 -11
  63. package/src/schema.js +235 -36
  64. package/src/stats.js +76 -17
  65. package/src/svg.js +4 -1
  66. package/src/text.js +5 -0
  67. package/src/theme.js +101 -14
@@ -4,18 +4,26 @@
4
4
  import { el, g, text, tip, barPath } from '../svg.js';
5
5
  import { measure } from '../text.js';
6
6
  import { makeFormat, formatNumber } from '../format.js';
7
- import { niceDomain, niceStep, linear, log, logDomain } from '../scale.js';
7
+ import { niceDomain, niceStep, linear, log, logDomain, intStep } from '../scale.js';
8
8
  import { colorMap } from '../theme.js';
9
- import { ensureData, requireField, num, fieldsOf, fieldKind, uniq, humanize } from '../data.js';
9
+ import { ensureData, requireField, num, fieldsOf, fieldKind, uniq, humanize, PlotError } from '../data.js';
10
10
  import { yAxisMargin, yAxisTop, yGrid, yTickLabels, gridX, ticksWidth } from '../axes.js';
11
11
  import { createFrame } from '../frame.js';
12
12
  import { ctx } from '../context.js';
13
- import { histogram as binValues, kde as kdeEst, scottBandwidth, ecdf as ecdfEst, quantile, finite, median } from '../stats.js';
13
+ import { histogram as binValues, kde as kdeEst, scottBandwidth, ecdf as ecdfEst, quantile, finite, median, extent } from '../stats.js';
14
14
 
15
15
  // ── Utilidades compartidas (también las usa regplot.js) ───────────────────
16
16
 
17
17
  const EN = (spec) => String(spec.locale || 'es').startsWith('en');
18
18
 
19
+ /** Error con pista cuando el campo no tiene ningún número graficable. */
20
+ function noNumbers(field, en, isLog) {
21
+ const logNote = isLog ? (en ? ' (on a log scale only values > 0 count)' : ' (en escala log solo cuentan valores > 0)') : '';
22
+ return en
23
+ ? new PlotError(`No numeric values in \`${field}\`${logNote}.`, `\`${field}\` must hold numbers (empty, null and text values are skipped), or pick another field with x: "field".`)
24
+ : new PlotError(`No hay valores numéricos en \`${field}\`${logNote}.`, `\`${field}\` debe tener números (se omiten vacíos, nulos y textos), o elige otro campo con x: "campo".`);
25
+ }
26
+
19
27
  /** Campo de grupo: `hue` (seaborn), `series` o `color` (si no es un hex). */
20
28
  export function hueField(spec) {
21
29
  const h = spec.hue ?? spec.series ?? (typeof spec.color === 'string' && !spec.color.startsWith('#') ? spec.color : undefined);
@@ -38,15 +46,15 @@ export function groupValues(data, field, hue) {
38
46
  * Dominio de un eje numérico (lineal o log), con override `[min, max]`.
39
47
  * Devuelve { min, max, step, isLog, ticks, scale(r0, r1) }.
40
48
  */
41
- export function axisDomain(values, { domain, type, count = 5, zero = false, nice = true } = {}) {
49
+ export function axisDomain(values, { domain, type, count = 5, zero = false, nice = true, integer = false } = {}) {
42
50
  const fin = finite(values);
43
51
  if (type === 'log') {
44
52
  const pos = fin.filter((v) => v > 0);
45
- const d = domain ? { min: domain[0], max: domain[1] } : logDomain(Math.min(...pos), Math.max(...pos));
53
+ const d = domain ? { min: domain[0], max: domain[1] } : logDomain(...extent(pos));
46
54
  const ticks = log(d.min, d.max, 0, 1).ticks();
47
55
  return { min: d.min, max: d.max, step: null, isLog: true, ticks, scale: (r0, r1) => log(d.min, d.max, r0, r1) };
48
56
  }
49
- let lo = fin.length ? Math.min(...fin) : 0, hi = fin.length ? Math.max(...fin) : 1;
57
+ let [lo, hi] = fin.length ? extent(fin) : [0, 1];
50
58
  if (zero) { lo = Math.min(0, lo); hi = Math.max(0, hi); }
51
59
  let d;
52
60
  if (domain) {
@@ -59,16 +67,18 @@ export function axisDomain(values, { domain, type, count = 5, zero = false, nice
59
67
  d = { min: a, max: hi + span * 0.04, step: niceStep(span * 1.08, count) };
60
68
  } else if (!nice) {
61
69
  d = { min: lo, max: hi === lo ? lo + 1 : hi, step: niceStep((hi === lo ? 1 : hi - lo), count) };
62
- } else d = niceDomain(lo, hi, count);
70
+ } else d = niceDomain(lo, hi, count, { integer });
63
71
  // Sin pasos de 2,5: con decimales redondeados se leen mal (2,5 → "3").
64
72
  const mag = 10 ** Math.floor(Math.log10(d.step));
65
73
  if (Math.abs(d.step / mag - 2.5) < 1e-9) d.step = 2 * mag;
74
+ // Conteos: paso entero (nunca marcas en 0,5).
75
+ if (integer) d.step = intStep(d.step);
66
76
  const ticks = linear(d.min, d.max, 0, 1).ticks(d.step);
67
77
  return { min: d.min, max: d.max, step: d.step, isLog: false, ticks, scale: (r0, r1) => linear(d.min, d.max, r0, r1) };
68
78
  }
69
79
 
70
80
  /** Formateador de marcas para un eje (lineal o log). */
71
- export const tickFmt = (fmt, ax) => (ax.isLog ? fmt : fmt.tick(ax.step));
81
+ export const tickFmt = (fmt, ax) => (ax.isLog ? fmt : fmt.tick(ax.step, ax.ticks));
72
82
 
73
83
  /** Títulos de eje horizontales: y arriba a la izquierda, x abajo a la derecha. */
74
84
  export function axisTitles(body, theme, area, inner, { x, y }) {
@@ -106,9 +116,10 @@ export function hist(spec, theme) {
106
116
  const locale = spec.locale || 'es';
107
117
  const isLog = (horizontal ? spec.yScale : spec.xScale) === 'log';
108
118
  const tr = T(isLog);
109
- const groups = groupValues(data, field, hue).map((gr) => ({ ...gr, values: isLog ? gr.values.filter((v) => v > 0) : gr.values }));
119
+ // Sin valores > 0 en log, un grupo queda vacío: fuera (leyenda fantasma y 0/0 al normalizar).
120
+ const groups = groupValues(data, field, hue).map((gr) => ({ ...gr, values: isLog ? gr.values.filter((v) => v > 0) : gr.values })).filter((gr) => gr.values.length);
110
121
  const all = groups.flatMap((gr) => gr.values);
111
- if (!all.length) return emptyChart(spec, theme, en);
122
+ if (!all.length) throw noNumbers(field, en, isLog);
112
123
  const N = all.length;
113
124
  const stat = spec.stat || 'count';
114
125
  const multiple = groups.length > 1 ? spec.multiple || 'layer' : 'layer';
@@ -174,7 +185,7 @@ export function hist(spec, theme) {
174
185
  if (kdes && multiple === 'stack') {
175
186
  for (let i = 0; i < kdes[0].length; i++) { let acc = 0; for (const k of kdes) { acc += k[i][1]; k[i][1] = acc; } }
176
187
  }
177
- const kdeMax = kdes ? Math.max(...kdes.flatMap((k) => k.map((p) => p[1]))) : 0;
188
+ const kdeMax = kdes ? extent(kdes.flatMap((k) => k.map((p) => p[1])))[1] : 0;
178
189
 
179
190
  const cmap = colorMap(groups.map((gr) => gr.key), theme, spec.colors);
180
191
  const single = K === 1;
@@ -185,9 +196,9 @@ export function hist(spec, theme) {
185
196
  const area = frame.layout(legend);
186
197
  const body = [];
187
198
  const M = theme.marks || {};
188
- const vMax = Math.max(...tops, kdeMax);
199
+ const vMax = Math.max(extent(tops)[1], kdeMax);
189
200
  const valueOverride = horizontal ? spec.xDomain : spec.yDomain;
190
- const vAx = axisDomain([0, vMax], { domain: multiple === 'fill' ? [0, 1] : valueOverride, count: Math.max(3, Math.round((horizontal ? area.w / 110 : area.h / 64))), zero: true });
201
+ const vAx = axisDomain([0, vMax], { domain: multiple === 'fill' ? [0, 1] : valueOverride, count: Math.max(3, Math.round((horizontal ? area.w / 110 : area.h / 64))), zero: true, integer: stat === 'count' && multiple !== 'fill' });
191
202
  const binDom = dom ? [dom[0], dom[1]] : [bins[0].x0, bins[bins.length - 1].x1];
192
203
  const clipId = 'pa-clip-h' + Math.abs(hashStr((spec.title || '') + field + area.w)).toString(36);
193
204
  const writeDomains = (bin, val) => { if (spec._domains && typeof spec._domains === 'object') { spec._domains[horizontal ? 'y' : 'x'] = bin; spec._domains[horizontal ? 'x' : 'y'] = val; } };
@@ -314,7 +325,9 @@ export function kde(spec, theme) {
314
325
  const locale = spec.locale || 'es';
315
326
  const isLog = spec.xScale === 'log';
316
327
  const tr = T(isLog);
317
- const groups = groupValues(data, field, hue).map((gr) => ({ ...gr, values: isLog ? gr.values.filter((v) => v > 0) : gr.values })).filter((gr) => gr.values.length > 1);
328
+ const groups0 = groupValues(data, field, hue).map((gr) => ({ ...gr, values: isLog ? gr.values.filter((v) => v > 0) : gr.values }));
329
+ if (!groups0.some((gr) => gr.values.length)) throw noNumbers(field, en, isLog);
330
+ const groups = groups0.filter((gr) => gr.values.length > 1);
318
331
  if (!groups.length) return emptyChart(spec, theme, en);
319
332
  const all = groups.flatMap((gr) => gr.values);
320
333
  const N = all.length;
@@ -326,8 +339,9 @@ export function kde(spec, theme) {
326
339
  const adjust = spec.bwAdjust ?? spec.bw_adjust ?? 1;
327
340
  // Grilla común
328
341
  const bws = groups.map((gr) => scottBandwidth(gr.values.map(tr.f)) * adjust);
329
- let lo = Math.min(...groups.map((gr, i) => Math.min(...gr.values.map(tr.f)) - cut * bws[i]));
330
- let hi = Math.max(...groups.map((gr, i) => Math.max(...gr.values.map(tr.f)) + cut * bws[i]));
342
+ const ext = groups.map((gr) => extent(gr.values.map(tr.f)));
343
+ let lo = Math.min(...ext.map((e, i) => e[0] - cut * bws[i]));
344
+ let hi = Math.max(...ext.map((e, i) => e[1] + cut * bws[i]));
331
345
  if (spec.clip) { lo = Math.max(lo, tr.f(spec.clip[0])); hi = Math.min(hi, tr.f(spec.clip[1])); }
332
346
  if (spec.xDomain) { lo = tr.f(spec.xDomain[0]); hi = tr.f(spec.xDomain[1]); }
333
347
  const G = 200;
@@ -349,7 +363,7 @@ export function kde(spec, theme) {
349
363
  }
350
364
  }
351
365
  }
352
- const yMax = Math.max(...curves.flatMap((c) => c.map((p) => p[1])));
366
+ const yMax = extent(curves.flatMap((c) => c.map((p) => p[1])))[1];
353
367
  const cmap = colorMap(groups.map((gr) => gr.key), theme, spec.colors);
354
368
  const hl = spec.highlight === undefined ? null : new Set([].concat(spec.highlight).map(String));
355
369
  const colorOf = (k) => (hl && !hl.has(groups[k].key) ? theme.dim : K === 1 ? (spec.colors && spec.colors[groups[0].key]) || theme.categorical[0] : cmap.get(groups[k].key));
@@ -445,7 +459,7 @@ export function ecdf(spec, theme) {
445
459
  const locale = spec.locale || 'es';
446
460
  const isLog = spec.xScale === 'log';
447
461
  const groups = groupValues(data, field, hue).map((gr) => ({ ...gr, values: isLog ? gr.values.filter((v) => v > 0) : gr.values })).filter((gr) => gr.values.length);
448
- if (!groups.length) return emptyChart(spec, theme, en);
462
+ if (!groups.length) throw noNumbers(field, en, isLog);
449
463
  const all = groups.flatMap((gr) => gr.values);
450
464
  const K = groups.length;
451
465
  const cmap = colorMap(groups.map((gr) => gr.key), theme, spec.colors);
@@ -476,12 +490,16 @@ export function ecdf(spec, theme) {
476
490
  const comp = !!spec.complementary;
477
491
  const Y = (p) => (comp ? 1 - p : p);
478
492
  let d = `M${xS(Math.max(xAx.min, pts[0][0])).toFixed(2)},${yS(Y(0)).toFixed(2)}`;
479
- let prev = 0;
493
+ // Escalones a menos de medio píxel se funden en uno (50 mil valores → ~1 por columna).
494
+ let prev = 0, colX = NaN, colP0 = 0, colP1 = 0;
495
+ const flush = () => { if (Number.isFinite(colX)) { const px = colX.toFixed(2); d += `L${px},${yS(Y(colP0)).toFixed(2)}L${px},${yS(Y(colP1)).toFixed(2)}`; } };
480
496
  for (const [x, p] of pts) {
481
- const px = xS(Math.min(xAx.max, Math.max(xAx.min, x))).toFixed(2);
482
- d += `L${px},${yS(Y(prev)).toFixed(2)}L${px},${yS(Y(p)).toFixed(2)}`;
497
+ const fx0 = xS(Math.min(xAx.max, Math.max(xAx.min, x)));
498
+ if (Number.isFinite(colX) && Math.abs(fx0 - colX) < 0.5) colP1 = p;
499
+ else { flush(); colX = fx0; colP0 = prev; colP1 = p; }
483
500
  prev = p;
484
501
  }
502
+ flush();
485
503
  d += `L${xS(xAx.max).toFixed(2)},${yS(Y(1)).toFixed(2)}`;
486
504
  const vs = [...groups[k].values].sort((a, b) => a - b);
487
505
  body.push(g({ class: 'pa-m', 'data-tip': tip(groups[k].key || humanize(field), `P25: ${fx(quantile(vs, 0.25))}`, `${en ? 'Median' : 'Mediana'}: ${fx(quantile(vs, 0.5))}`, `P75: ${fx(quantile(vs, 0.75))}`, `P90: ${fx(quantile(vs, 0.9))}`) }, [
@@ -5,7 +5,7 @@ import { el, g, text, tip } from '../svg.js';
5
5
  import { measure, truncate } from '../text.js';
6
6
  import { makeFormat, formatNumber } from '../format.js';
7
7
  import { colorMap, textOn } from '../theme.js';
8
- import { ensureData, requireField, num, fieldsOf, fieldKind, humanize } from '../data.js';
8
+ import { ensureData, requireField, num, fieldsOf, fieldKind, humanize, PlotError } from '../data.js';
9
9
  import { createFrame, hash } from '../frame.js';
10
10
  import { dashedMatcher, hatcher, dashedDefaultLabel, DASH } from '../dashed.js';
11
11
  import { ctx } from '../context.js';
@@ -35,10 +35,12 @@ export function donut(spec, theme) {
35
35
  const sums = new Map();
36
36
  for (const r of data) {
37
37
  const v = num(r[valueF]);
38
- if (!(v > 0)) continue;
38
+ if (!(v > 0) || !Number.isFinite(v)) continue; // ≤ 0, vacíos e Infinity no tienen ángulo
39
39
  const k = String(r[labelF]);
40
40
  sums.set(k, (sums.get(k) || 0) + v);
41
41
  }
42
+ // Sin ningún valor positivo no hay nada que repartir: error claro en vez de un anillo vacío con "0".
43
+ if (!sums.size) throw new PlotError(en ? `No positive values in \`${valueF}\`.` : `No hay valores positivos en \`${valueF}\`.`, en ? 'A donut or pie needs at least one value greater than 0 (zero, negative and empty values are skipped).' : 'La dona o torta necesita al menos un valor mayor que 0 (se omiten ceros, negativos y vacíos).');
42
44
  let slices = [...sums.entries()].map(([k, v]) => ({ k, v }));
43
45
  if ((spec.sort ?? 'desc') === 'desc') slices.sort((a, b) => b.v - a.v);
44
46
  const max = spec.maxSlices ?? 6;