plotagents 0.7.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 (53) hide show
  1. package/AGENTS.md +5 -1
  2. package/README.md +18 -1
  3. package/bin/plotagents.js +3 -1
  4. package/dist/plotagents-geo.js +1 -1
  5. package/dist/plotagents.js +1857 -110
  6. package/docs/charts.md +86 -1
  7. package/docs/es/agents.md +102 -0
  8. package/docs/es/api.md +172 -0
  9. package/docs/es/charts.md +436 -0
  10. package/docs/es/colorscales.md +46 -0
  11. package/docs/es/dashboards.md +79 -0
  12. package/docs/es/getting-started.md +142 -0
  13. package/docs/es/rendering.md +47 -0
  14. package/docs/es/themes.md +116 -0
  15. package/index.d.ts +92 -8
  16. package/package.json +3 -2
  17. package/schema.json +359 -9
  18. package/skills/plotagents/SKILL.md +105 -0
  19. package/src/charts/bar.js +13 -5
  20. package/src/charts/chord.js +3 -1
  21. package/src/charts/choropleth.js +3 -1
  22. package/src/charts/corr.js +512 -0
  23. package/src/charts/dist.js +2 -1
  24. package/src/charts/donut.js +3 -1
  25. package/src/charts/kpi.js +2 -2
  26. package/src/charts/line.js +19 -7
  27. package/src/charts/network.js +13 -8
  28. package/src/charts/pointmap.js +9 -6
  29. package/src/charts/regplot.js +9 -3
  30. package/src/charts/sankey.js +1 -1
  31. package/src/charts/scatter.js +4 -2
  32. package/src/charts/sunburst.js +2 -2
  33. package/src/charts/table.js +882 -0
  34. package/src/charts/treemap.js +2 -2
  35. package/src/charts/wordcloud.js +17 -3
  36. package/src/colorscales.js +12 -1
  37. package/src/dashboard.js +21 -8
  38. package/src/data.js +2 -0
  39. package/src/facet.js +4 -3
  40. package/src/format.js +36 -13
  41. package/src/frame.js +8 -5
  42. package/src/geo/projection.js +4 -2
  43. package/src/hierarchy.js +6 -0
  44. package/src/i18n.js +107 -0
  45. package/src/index.js +17 -4
  46. package/src/infer.js +9 -1
  47. package/src/lint.js +66 -4
  48. package/src/runtime.js +94 -6
  49. package/src/scale.js +3 -2
  50. package/src/schema.js +54 -11
  51. package/src/stats.js +5 -3
  52. package/src/svg.js +4 -1
  53. package/src/text.js +5 -0
@@ -276,13 +276,16 @@ function spanningTree(nodes, edges, byId, root) {
276
276
  // Hojas numeradas en orden DFS; cada padre queda en el promedio de sus hijos.
277
277
  const pos = new Map();
278
278
  let leaf = 0;
279
- const visit = (id) => {
280
- const ch = children.get(id);
281
- if (!ch.length) { pos.set(id, leaf++); return; }
282
- for (const c of ch) visit(c);
283
- pos.set(id, ch.reduce((a, c) => a + pos.get(c), 0) / ch.length);
284
- };
285
- visit(root.id);
279
+ // Iterativo: una cadena larga (miles de niveles) desbordaría la pila.
280
+ const stack = [[root.id, 0]];
281
+ while (stack.length) {
282
+ const top = stack[stack.length - 1];
283
+ const ch = children.get(top[0]);
284
+ if (!ch.length) { pos.set(top[0], leaf++); stack.pop(); continue; }
285
+ if (top[1] < ch.length) { stack.push([ch[top[1]++], 0]); continue; }
286
+ pos.set(top[0], ch.reduce((a, c) => a + pos.get(c), 0) / ch.length);
287
+ stack.pop();
288
+ }
286
289
  const maxDepth = Math.max(0, ...depth.values());
287
290
  const rest = nodes.filter((n) => !depth.has(n.id)).sort(byGroup);
288
291
  return { depth, pos, leaves: leaf, maxDepth, rest, treeEdges, children };
@@ -504,7 +507,9 @@ export function network(spec, theme) {
504
507
 
505
508
  // Foco (clic en un nodo): va al centro y se destacan sus vecinos, con nombre.
506
509
  const focusNode = spec.focus !== undefined && spec.focus !== null ? byId.get(String([].concat(spec.focus)[0])) ?? null : null;
507
- const hl = focusNode ? new Set([focusNode.id]) : spec.highlight === undefined || spec.highlight === null ? null : new Set([].concat(spec.highlight).map(String));
510
+ // Un `highlight` que no existe en la red se ignora (si no, todo quedaría atenuado).
511
+ const hlIds = spec.highlight === undefined || spec.highlight === null ? [] : [].concat(spec.highlight).map(String).filter((id) => byId.has(id));
512
+ const hl = focusNode ? new Set([focusNode.id]) : hlIds.length ? new Set(hlIds) : null;
508
513
  const near = new Set();
509
514
  if (hl) { for (const id of hl) near.add(id); for (const e of edges) { if (hl.has(e.s)) near.add(e.t); if (hl.has(e.t)) near.add(e.s); } }
510
515
  const on = (n) => !hl || near.has(n.id);
@@ -23,6 +23,9 @@ const MAX_LAT = 85.0511287798;
23
23
  const TILE = 256;
24
24
  const MAX_TILES = 48;
25
25
  const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
26
+ // Mínimo y máximo sin `...` (con cientos de miles de puntos desborda la pila).
27
+ const lo = (a) => a.reduce((m, v) => (v < m ? v : m), Infinity);
28
+ const hi = (a) => a.reduce((m, v) => (v > m ? v : m), -Infinity);
26
29
 
27
30
  // ── Web Mercator (EPSG:3857) en coordenadas normalizadas [0, 1] ─────────────
28
31
  /** Longitud → x de Web Mercator en [0, 1] (0 = 180° O). */
@@ -235,10 +238,10 @@ export function pointmap(spec, theme) {
235
238
  let unwrapped = false;
236
239
  {
237
240
  const lons = pts.map((p) => p.lon);
238
- const span = Math.max(...lons) - Math.min(...lons);
241
+ const span = hi(lons) - lo(lons);
239
242
  if (span > 180) {
240
243
  const alt = lons.map((l) => (l < 0 ? l + 360 : l));
241
- if (Math.max(...alt) - Math.min(...alt) < span) { for (const p of pts) if (p.lon < 0) p.lon += 360; unwrapped = true; }
244
+ if (hi(alt) - lo(alt) < span) { for (const p of pts) if (p.lon < 0) p.lon += 360; unwrapped = true; }
242
245
  }
243
246
  }
244
247
  for (const p of pts) { p.mx = mercX(p.lon); p.my = mercY(p.lat); }
@@ -267,7 +270,7 @@ export function pointmap(spec, theme) {
267
270
  colorOf = (p) => cmap.get(p.k) || theme.other;
268
271
  if (keys.length > 1) legend = keys.map((k) => ({ label: k, color: cmap.get(k) }));
269
272
  } else if (valueF && values.length) {
270
- const vmin = Math.min(...values), vmax = Math.max(...values);
273
+ const vmin = lo(values), vmax = hi(values);
271
274
  const mid = spec.midpoint ?? (spec.diverging ? 0 : undefined);
272
275
  let stops, at;
273
276
  if (mid !== undefined) {
@@ -295,7 +298,7 @@ export function pointmap(spec, theme) {
295
298
  let area = frame.layout(legend);
296
299
 
297
300
  // Radios: área proporcional a `size` (raíz cuadrada), o uno fijo según la densidad.
298
- const maxS = Math.max(0, ...sizes.map(Math.abs));
301
+ const maxS = sizes.reduce((m, v) => Math.max(m, Math.abs(v)), 0);
299
302
  const wk = Math.max(0.6, Math.min(1.2, area.w / 672));
300
303
  const rMax = sizeF && maxS ? spec.maxRadius ?? Math.round((n <= 40 ? 22 : n <= 150 ? 15 : n <= 600 ? 10 : 6) * wk) : 0;
301
304
  const rBase = spec.radius ?? (dense ? 2.2 : n > 500 ? 3 : n > 120 ? 4 : 5.5);
@@ -513,8 +516,8 @@ function describePointmap({ pts, n, provider, world, hueF, keys, sizeF, valueF,
513
516
  const en = String(locale).startsWith('en');
514
517
  const W = en ? 'W' : 'O';
515
518
  const lats = pts.map((p) => p.lat), lons = pts.map((p) => (p.lon > 180 ? p.lon - 360 : p.lon));
516
- const la0 = Math.min(...lats), la1 = Math.max(...lats);
517
- const lo0 = Math.min(...lons), lo1 = Math.max(...lons);
519
+ const la0 = lo(lats), la1 = hi(lats);
520
+ const lo0 = lo(lons), lo1 = hi(lons);
518
521
  const base = provider ? provider.name || provider.attribution : world ? (en ? 'a world outline' : 'un contorno del mundo') : '';
519
522
  const where = n === 1 || (la1 - la0 < 0.05 && lo1 - lo0 < 0.05)
520
523
  ? (en ? ` at ${deg(la0, 'N', 'S', locale)}, ${deg(lo0, 'E', W, locale)}` : ` en ${deg(la0, 'N', 'S', locale)}, ${deg(lo0, 'E', W, locale)}`)
@@ -56,7 +56,7 @@ function fitGroup(gp, spec, { xLog, xMin, xMax, steps = 90 }) {
56
56
  const pts = lw.map(([t, v]) => [spec.logx ? 10 ** t : t, v]);
57
57
  return { line: pts, band: null, r2: null, n: gp.length };
58
58
  }
59
- const fit = polyfit(xs, ys, { order: spec.order ?? 1, level: (spec.ci ?? 95) / 100 });
59
+ const fit = polyfit(xs, ys, { order: spec.order ?? 1, level: typeof spec.ci === 'number' ? (spec.ci > 1 ? spec.ci / 100 : spec.ci) : 0.95 });
60
60
  const line = grid.map((v) => [v, fit.predict(fx(v))]);
61
61
  const band = spec.ci === false || spec.ci === null || gp.length < (spec.order ?? 1) + 3 ? null : grid.map((v) => [v, ...fit.band(fx(v))]);
62
62
  return { line, band, r2: fit.r2, n: fit.n, fit };
@@ -123,7 +123,7 @@ export function regplot(spec, theme) {
123
123
  });
124
124
  if (spec.scatter !== false) {
125
125
  const order = [...pts].sort((a, b) => (focusKey(a.k) ? 1 : 0) - (focusKey(b.k) ? 1 : 0));
126
- const dots = order.map((p) => ({
126
+ const dots = order.filter((p) => visible(p, xS, yS, inner)).map((p) => ({
127
127
  class: 'pa-m', cx: xS(p.x), cy: yS(p.y), r, fill: colorOf(p.k), fillOpacity: K === 1 ? (M.dotOpacity ?? 0.85) * 0.75 : M.dotOpacity ?? 0.85,
128
128
  stroke: pts.length > 400 ? 'none' : theme.surface, strokeWidth: 1.25,
129
129
  'data-tip': tip(p.name || p.k || '', `${humanize(P.x)}: ${fx(p.x)}`, `${humanize(P.y)}: ${fy(p.y)}`, p.name && p.k ? p.k : ''),
@@ -351,7 +351,7 @@ export function jointplot(spec, theme) {
351
351
  body.push(el('path', { d: L(f.line), fill: 'none', stroke: K === 1 ? theme.ink : colorOf(k), strokeWidth: M.lineWidth ?? 2, opacity: 0.9 }));
352
352
  });
353
353
  }
354
- body.push(g({}, dotLayer(pts.map((p) => ({ class: 'pa-m', cx: xS(p.x), cy: yS(p.y), r, fill: colorOf(p.k), fillOpacity: M.dotOpacity ?? 0.85, stroke: pts.length > 400 ? 'none' : theme.surface, strokeWidth: 1.25, 'data-tip': tip(p.name || p.k, `${humanize(P.x)}: ${fx(p.x)}`, `${humanize(P.y)}: ${fy(p.y)}`, p.name && p.k ? p.k : '') })))));
354
+ body.push(g({}, dotLayer(pts.filter((p) => visible(p, xS, yS, inner)).map((p) => ({ class: 'pa-m', cx: xS(p.x), cy: yS(p.y), r, fill: colorOf(p.k), fillOpacity: M.dotOpacity ?? 0.85, stroke: pts.length > 400 ? 'none' : theme.surface, strokeWidth: 1.25, 'data-tip': tip(p.name || p.k, `${humanize(P.x)}: ${fx(p.x)}`, `${humanize(P.y)}: ${fy(p.y)}`, p.name && p.k ? p.k : '') })))));
355
355
  body.push(pointLabels(pts, spec, xS, yS, r, inner, theme, P));
356
356
  body.push(gridX(xAx.ticks, xS, inner, theme, tickFmt(fx, xAx), { lines: false }));
357
357
  body.push(yTickLabels(yAx.ticks, yS, inner, theme, yt));
@@ -363,6 +363,12 @@ export function jointplot(spec, theme) {
363
363
  return frame.finish(body, { desc, type: 'jointplot' });
364
364
  }
365
365
 
366
+ /** Con xDomain/yDomain, los puntos fuera del rango no se dibujan fuera del área. */
367
+ function visible(p, xS, yS, inner) {
368
+ const cx = xS(p.x), cy = yS(p.y);
369
+ return cx >= inner.x - 1 && cx <= inner.x + inner.w + 1 && cy >= inner.y - 1 && cy <= inner.y + inner.h + 1;
370
+ }
371
+
366
372
  function hashStr(s) {
367
373
  let h = 0;
368
374
  for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
@@ -120,7 +120,7 @@ export function sankey(spec, theme) {
120
120
  const x0 = l.sn.x + NW, x1 = l.tn.x, xm = (x0 + x1) / 2, w = l.v * ky;
121
121
  const d = `M${x0},${l.y0}C${xm},${l.y0} ${xm},${l.y1} ${x1},${l.y1}L${x1},${l.y1 + w}C${xm},${l.y1 + w} ${xm},${l.y0 + w} ${x0},${l.y0 + w}Z`;
122
122
  const share = l.sn.value ? Math.round((l.v / l.sn.value) * 100) : 0;
123
- linkG.push(el('path', { class: 'pa-m', d, fill: linkColor(l), fillOpacity: theme.link, 'data-tip': `${l.s} → ${l.t}\n${fmt(l.v)} · ${share}% de ${l.s}` }));
123
+ linkG.push(el('path', { class: 'pa-m', d, fill: linkColor(l), fillOpacity: theme.link, 'data-tip': `${l.s} → ${l.t}\n${fmt(l.v)} · ${share}% ${en ? 'of' : 'de'} ${l.s}` }));
124
124
  }
125
125
  body.push(g({ class: 'pa-links' }, linkG));
126
126
  for (const [ci, c] of cols.entries()) for (const n of c) {
@@ -138,7 +138,9 @@ export function scatter(spec, theme) {
138
138
  // Tooltips: la capa densa guarda a lo más MAX_TIPS; no se arman los que se descartan.
139
139
  const MAX_TIPS = 20000;
140
140
  const hx = humanize(x), hy = humanize(y), hs = size ? humanize(size) : '';
141
- const circles = sorted.map((p, i) => {
141
+ // Con xDomain/yDomain, los puntos fuera del rango no se dibujan fuera del área.
142
+ const vis = (p) => { const cx = xS(p.x), cy = yS(p.y); return cx >= inner.x - 1 && cx <= inner.x + inner.w + 1 && cy >= inner.y - 1 && cy <= inner.y + inner.h + 1; };
143
+ const circles = sorted.filter(vis).map((p, i) => {
142
144
  let t = '';
143
145
  if (i < MAX_TIPS || sorted.length <= 1500) {
144
146
  const lines = [p.name || '', `${hx}: ${fx(p.x)}`, `${hy}: ${fy(p.y)}`];
@@ -195,7 +197,7 @@ export function scatter(spec, theme) {
195
197
  }
196
198
  const placed = [];
197
199
  for (const p of chosen) {
198
- if (!p.name) continue;
200
+ if (!p.name || !vis(p)) continue;
199
201
  const w = measure(p.name, 12, 500);
200
202
  const r = rOf(p);
201
203
  let lx = xS(p.x) + r + 4, ly = yS(p.y) + 4, anchor = 'start';
@@ -7,7 +7,7 @@ import { makeFormat } from '../format.js';
7
7
  import { textOn } from '../theme.js';
8
8
  import { humanize } from '../data.js';
9
9
  import { createFrame } from '../frame.js';
10
- import { buildHierarchy, findNode, assignColors, shareFmt, breadcrumb, height, SEP } from '../hierarchy.js';
10
+ import { buildHierarchy, focusNode, assignColors, shareFmt, breadcrumb, height, SEP } from '../hierarchy.js';
11
11
 
12
12
  const TAU = Math.PI * 2;
13
13
  const f = (n) => n.toFixed(2);
@@ -24,7 +24,7 @@ export function sunburst(spec, theme) {
24
24
  const locale = spec.locale || 'es';
25
25
  const en = String(locale).startsWith('en');
26
26
  const { root, fields, valueField } = buildHierarchy(spec);
27
- const focus = findNode(root, spec.focus);
27
+ const focus = focusNode(root, spec.focus);
28
28
  const levels = Math.max(1, height(focus));
29
29
  assignColors(root, theme, spec); // colores estables al navegar
30
30
  const vals = [];