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
@@ -0,0 +1,218 @@
1
+ // Diagrama de cuerdas: flujos entre grupos alrededor de un círculo. El largo
2
+ // de cada arco es el total del grupo; cada cinta une dos grupos y su ancho es
3
+ // el flujo. Útil para migraciones, comercio entre bloques o transferencias.
4
+ import { el, g, text, tip } from '../svg.js';
5
+ import { measure, truncate, wrap } from '../text.js';
6
+ import { makeFormat, formatNumber } from '../format.js';
7
+ import { colorMap, readable } from '../theme.js';
8
+ import { ensureData, requireField, num, fieldsOf, fieldKind, PlotError } from '../data.js';
9
+ import { createFrame } from '../frame.js';
10
+
11
+ const pick = (fields, cands) => cands.find((c) => fields.includes(c));
12
+ const TAU = Math.PI * 2;
13
+
14
+ /** Matriz de flujos m[i][j] (de i hacia j) y nombres. */
15
+ export function prepareChord(spec) {
16
+ if (Array.isArray(spec.matrix)) {
17
+ const names = (spec.names || spec.matrix.map((_, i) => String(i + 1))).map(String);
18
+ if (spec.matrix.length !== names.length) throw new PlotError('`matrix` y `names` deben tener el mismo largo.', 'matrix: [[0, 5], [3, 0]], names: ["A", "B"]');
19
+ return { names, m: spec.matrix.map((row) => names.map((_, j) => { const v = num(row[j]); return Number.isFinite(v) && v > 0 ? v : 0; })) };
20
+ }
21
+ const data = ensureData(spec);
22
+ const fields = fieldsOf(data);
23
+ const from = spec.from ?? pick(fields, ['from', 'source', 'origen', 'desde']) ?? fields[0];
24
+ const to = spec.to ?? pick(fields, ['to', 'target', 'destino', 'hasta']) ?? fields[1];
25
+ requireField(data, from, 'from');
26
+ requireField(data, to, 'to');
27
+ const vf = spec.value ?? pick(fields, ['value', 'valor', 'weight', 'flujo']) ?? fields.find((f) => f !== from && f !== to && fieldKind(data, f) === 'number');
28
+ if (vf) requireField(data, vf, 'value');
29
+ const names = [];
30
+ const idx = new Map();
31
+ const at = (k) => { if (!idx.has(k)) { idx.set(k, names.length); names.push(k); } return idx.get(k); };
32
+ const flows = [];
33
+ for (const r of data) {
34
+ if (r[from] === null || r[from] === undefined || r[to] === null || r[to] === undefined) continue;
35
+ const v = vf ? num(r[vf]) : 1;
36
+ if (!(v > 0) || !Number.isFinite(v)) continue;
37
+ flows.push([at(String(r[from])), at(String(r[to])), v]);
38
+ }
39
+ const m = names.map(() => names.map(() => 0));
40
+ for (const [i, j, v] of flows) m[i][j] += v;
41
+ return { names, m };
42
+ }
43
+
44
+ export function chord(spec, theme) {
45
+ const locale = spec.locale || 'es';
46
+ const en = String(locale).startsWith('en');
47
+ const { names, m } = prepareChord(spec);
48
+ const n = names.length;
49
+ if (n < 2) throw new PlotError('El diagrama de cuerdas necesita al menos dos grupos.', 'Entrega filas { from, to, value } entre grupos distintos.');
50
+ const directed = spec.directed === true;
51
+
52
+ // Segmentos de cada grupo: lo que sale y (si es dirigido) lo que entra.
53
+ // No dirigido: la cinta i–j mide m[i][j] + m[j][i] en ambos extremos.
54
+ const seg = names.map(() => []);
55
+ const ribbons = [];
56
+ for (let i = 0; i < n; i++)
57
+ for (let j = 0; j < n; j++) {
58
+ if (i === j) continue;
59
+ if (directed) {
60
+ if (m[i][j] > 0) { const rb = { i, j, v: m[i][j] }; ribbons.push(rb); seg[i].push({ rb, end: 's', partner: j, v: m[i][j] }); seg[j].push({ rb, end: 't', partner: i, v: m[i][j] }); }
61
+ } else if (i < j) {
62
+ const v = m[i][j] + m[j][i];
63
+ if (v > 0) { const rb = { i, j, v, ij: m[i][j], ji: m[j][i] }; ribbons.push(rb); seg[i].push({ rb, end: 's', partner: j, v }); seg[j].push({ rb, end: 't', partner: i, v }); }
64
+ }
65
+ }
66
+ const totals = seg.map((s) => s.reduce((a, b) => a + b.v, 0));
67
+ const out = m.map((row, i) => row.reduce((a, v, j) => a + (j === i ? 0 : v), 0));
68
+ const inn = names.map((_, j) => m.reduce((a, row, i) => a + (i === j ? 0 : row[j]), 0));
69
+ let order = names.map((_, i) => i).filter((i) => totals[i] > 0);
70
+ if (spec.sort !== 'none') order.sort((a, b) => totals[b] - totals[a]);
71
+ const T = order.reduce((a, i) => a + totals[i], 0);
72
+ if (!(T > 0)) throw new PlotError('No hay flujos positivos entre grupos distintos.', 'Revisa el campo de valor.');
73
+
74
+ // Colores en orden de tamaño (estables por entidad); el resto, "otros".
75
+ const sizeOrder = [...order].sort((a, b) => totals[b] - totals[a]);
76
+ const cmap = colorMap(sizeOrder.map((i) => names[i]), theme, spec.colors);
77
+ const colorOf = (i) => cmap.get(names[i]);
78
+ const fmt = makeFormat(spec.format, locale, ribbons.map((r) => r.v).concat(totals));
79
+ const pct = (a, b) => (b > 0 ? formatNumber((a / b) * 100, a / b < 0.1 ? 1 : 0, locale, true) + '%' : '');
80
+
81
+ const legend = spec.legend === true || (spec.legend && typeof spec.legend !== 'boolean') ? sizeOrder.map((i) => ({ label: names[i], color: colorOf(i) })) : [];
82
+ const W = spec.width || 720;
83
+ const frame = createFrame(spec, theme, { height: W < 560 ? Math.round(W * 1.02 + 150) : 600 });
84
+ const area = frame.layout(legend);
85
+
86
+ // Ángulos: arcos proporcionales al total, con separación.
87
+ const pad = Math.min(0.04, (TAU * 0.18) / order.length);
88
+ const k = (TAU - pad * order.length) / T;
89
+ let a = -Math.PI / 2 + pad / 2;
90
+ const arc = new Map();
91
+ for (const i of order) {
92
+ const a0 = a, a1 = a + totals[i] * k;
93
+ arc.set(i, { a0, a1, mid: (a0 + a1) / 2 });
94
+ a = a1 + pad;
95
+ }
96
+ // Dentro de cada arco, los socios se ordenan para que las cintas no se crucen.
97
+ const pos = new Map(order.map((i, p) => [i, p]));
98
+ for (const i of order) {
99
+ const { a0 } = arc.get(i);
100
+ const p = pos.get(i);
101
+ const list = seg[i].filter((s) => pos.has(s.partner)).sort((x, y) => {
102
+ const dx = (pos.get(x.partner) - p + order.length) % order.length;
103
+ const dy = (pos.get(y.partner) - p + order.length) % order.length;
104
+ return dy - dx || (x.end === 's' ? -1 : 1);
105
+ });
106
+ let c = a0;
107
+ for (const s of list) { s.a0 = c; s.a1 = c + s.v * k; c = s.a1; s.rb[s.end] = s; }
108
+ }
109
+
110
+ // Etiquetas: horizontales si son pocas; radiales si son muchas.
111
+ const narrow = area.w < 480;
112
+ const labelSize = (theme.type?.label ?? 12) + (narrow ? -1 : 0.5);
113
+ const radial = spec.labelOrientation ? spec.labelOrientation === 'radial' : order.length > 10;
114
+ const valueOf = (i) => fmt(totals[i]);
115
+ const thick = Math.max(8, Math.min(14, Math.min(area.w, area.h) * 0.03));
116
+ const gapL = 10;
117
+ const radiusFor = (lw) => radial
118
+ ? Math.min(area.w, area.h) / 2 - thick - gapL - lw - 4
119
+ : Math.min(area.w / 2 - (lw + thick + gapL + 6), area.h / 2 - thick - gapL - labelSize * 2.6);
120
+ // Nombres completos si el círculo conserva un buen tamaño; si no, se acortan.
121
+ const full = Math.max(...order.map((i) => Math.max(measure(names[i], labelSize, 600), radial ? 0 : measure(valueOf(i), labelSize - 1.5))));
122
+ const minR = Math.min(area.w, area.h) * (narrow ? 0.22 : 0.3);
123
+ let maxLabel = full;
124
+ if (radiusFor(full) < minR) {
125
+ maxLabel = Math.max(48, radial ? Math.min(area.w, area.h) / 2 - thick - gapL - 4 - minR : area.w / 2 - thick - gapL - 6 - minR);
126
+ }
127
+ // Horizontales: si no caben en una línea, se parten en dos (no se cortan).
128
+ const linesOf = (i) => (radial ? [truncate(names[i], maxLabel, labelSize, 600)] : wrap(names[i], maxLabel, labelSize, 600, 2));
129
+ const labelOf = (i) => linesOf(i).join(' ');
130
+ const lw = Math.max(...order.map((i) => Math.max(...linesOf(i).map((l) => measure(l, labelSize, 600)), radial ? 0 : measure(valueOf(i), labelSize - 1.5))));
131
+ let R = Math.max(60, radiusFor(lw));
132
+ const cx = area.x + area.w / 2, cy = area.y + area.h / 2;
133
+ const r0 = R, r1 = R + thick;
134
+ const P = (ang, r) => [cx + Math.cos(ang) * r, cy + Math.sin(ang) * r];
135
+ const f = (v) => v.toFixed(2);
136
+
137
+ const hl = spec.highlight === undefined ? null : new Set([].concat(spec.highlight).map(String));
138
+ const isOn = (rb) => !hl || hl.has(names[rb.i]) || hl.has(names[rb.j]);
139
+
140
+ // Cintas: de un arco al otro, curvas por el centro.
141
+ const ribbonG = [];
142
+ const rr = r0 - 1;
143
+ for (const rb of [...ribbons].sort((x, y) => (isOn(x) - isOn(y)) || y.v - x.v)) {
144
+ const s = rb.s, t = rb.t;
145
+ if (!s || !t) continue;
146
+ const [sx0, sy0] = P(s.a0, rr), [sx1, sy1] = P(s.a1, rr), [tx0, ty0] = P(t.a0, rr), [tx1, ty1] = P(t.a1, rr);
147
+ const lg = (x, y) => (y - x > Math.PI ? 1 : 0);
148
+ const d = `M${f(sx0)},${f(sy0)}A${f(rr)},${f(rr)} 0 ${lg(s.a0, s.a1)} 1 ${f(sx1)},${f(sy1)}Q${f(cx)},${f(cy)} ${f(tx0)},${f(ty0)}A${f(rr)},${f(rr)} 0 ${lg(t.a0, t.a1)} 1 ${f(tx1)},${f(ty1)}Q${f(cx)},${f(cy)} ${f(sx0)},${f(sy0)}Z`;
149
+ // Color: dirigido → el origen. No dirigido → por defecto el grupo menor
150
+ // del par (así cada zona muestra su color en su cinta con el centro);
151
+ // ribbonColor: 'larger' usa el lado que más envía.
152
+ const mode = spec.ribbonColor ?? (directed ? 'source' : 'smaller');
153
+ const src = mode === 'source' ? rb.i
154
+ : mode === 'larger' ? (rb.ij >= rb.ji ? rb.i : rb.j)
155
+ : totals[rb.i] <= totals[rb.j] ? rb.i : rb.j;
156
+ const A = names[rb.i], B = names[rb.j];
157
+ const lines = directed
158
+ ? [`${A} → ${B}`, `${fmt(rb.v)} · ${pct(rb.v, out[rb.i])} ${en ? 'of what' : 'de lo que'} ${A} ${en ? 'sends' : 'envía'}`]
159
+ : [`${A} ↔ ${B}`, `${A} → ${B}: ${fmt(rb.ij)}`, `${B} → ${A}: ${fmt(rb.ji)}`, `${en ? 'Total' : 'Total'}: ${fmt(rb.v)} · ${pct(rb.v, totals[rb.i])} ${en ? 'of' : 'de'} ${A}`];
160
+ ribbonG.push(el('path', {
161
+ class: 'pa-m', d, fill: colorOf(src), fillOpacity: isOn(rb) ? (hl ? 0.75 : 0.62) : 0.1,
162
+ stroke: theme.surface, strokeWidth: 0.75, strokeOpacity: 0.9, 'data-tip': tip(...lines),
163
+ }));
164
+ }
165
+
166
+ // Arcos de cada grupo.
167
+ const arcG = [];
168
+ for (const i of order) {
169
+ const { a0, a1 } = arc.get(i);
170
+ const [x0, y0] = P(a0, r0 + 1.5), [x1, y1] = P(a1, r0 + 1.5), [x2, y2] = P(a1, r1), [x3, y3] = P(a0, r1);
171
+ const large = a1 - a0 > Math.PI ? 1 : 0;
172
+ const d = `M${f(x0)},${f(y0)}A${f(r0 + 1.5)},${f(r0 + 1.5)} 0 ${large} 1 ${f(x1)},${f(y1)}L${f(x2)},${f(y2)}A${f(r1)},${f(r1)} 0 ${large} 0 ${f(x3)},${f(y3)}Z`;
173
+ const dim = hl && !hl.has(names[i]);
174
+ const lines = directed
175
+ ? [names[i], `${en ? 'Out' : 'Sale'}: ${fmt(out[i])}`, `${en ? 'In' : 'Entra'}: ${fmt(inn[i])}`]
176
+ : [names[i], `${en ? 'Total flows' : 'Flujos totales'}: ${fmt(totals[i])}`, `${pct(totals[i], T)} ${en ? 'of all' : 'del total'}`];
177
+ arcG.push(el('path', { class: 'pa-m', d, fill: dim ? theme.dim : colorOf(i), stroke: theme.surface, strokeWidth: 1, 'data-tip': tip(...lines) }));
178
+ }
179
+
180
+ // Etiquetas.
181
+ const labG = [];
182
+ const halo = { stroke: theme.surface, strokeWidth: 3, paintOrder: 'stroke', strokeLinejoin: 'round' };
183
+ for (const i of order) {
184
+ const { mid, a0, a1 } = arc.get(i);
185
+ if ((a1 - a0) * r1 < 6 && order.length > 6) continue; // arco demasiado chico
186
+ const dim = hl && !hl.has(names[i]);
187
+ const fill = dim ? theme.muted : readable(colorOf(i), theme);
188
+ const ca = Math.cos(mid), sa = Math.sin(mid);
189
+ if (radial) {
190
+ const [lx, ly] = P(mid, r1 + gapL);
191
+ let deg = (mid * 180) / Math.PI;
192
+ const flip = ca < 0;
193
+ if (flip) deg += 180;
194
+ labG.push(el('text', { x: f(lx), y: f(ly), transform: `rotate(${deg.toFixed(1)} ${f(lx)} ${f(ly)})`, textAnchor: flip ? 'end' : 'start', dominantBaseline: 'middle', fill, fontSize: labelSize, fontWeight: 600, ...halo }, esc(labelOf(i))));
195
+ } else {
196
+ const [lx, ly] = P(mid, r1 + gapL);
197
+ const anchor = Math.abs(ca) < 0.25 ? 'middle' : ca > 0 ? 'start' : 'end';
198
+ const dy = Math.abs(ca) < 0.25 ? (sa < 0 ? -labelSize * 1.2 : labelSize * 0.9) : 0;
199
+ const ls = linesOf(i);
200
+ const lh = labelSize * 1.15;
201
+ // Arriba del círculo el bloque crece hacia arriba; en el resto, hacia abajo.
202
+ const y0 = Math.abs(ca) < 0.25 && sa < 0 ? ly + dy - (ls.length - 1) * lh : ly + dy - (Math.abs(ca) >= 0.25 ? ((ls.length - 1) * lh) / 2 : 0);
203
+ ls.forEach((l, k) => labG.push(text(lx, y0 + k * lh, l, { textAnchor: anchor, fill, fontSize: labelSize, fontWeight: 600, ...halo })));
204
+ labG.push(text(lx, y0 + ls.length * lh, valueOf(i), { textAnchor: anchor, fill: theme.lede, fontSize: labelSize - 1.5, ...halo }));
205
+ }
206
+ }
207
+
208
+ const body = [g({ class: 'pa-ribbons' }, ribbonG), g({ class: 'pa-arcs' }, arcG), g({ class: 'pa-labels' }, labG)];
209
+ const big = [...ribbons].sort((x, y) => y.v - x.v).slice(0, 3);
210
+ const desc = en
211
+ ? `Chord diagram of ${order.length} groups and ${ribbons.length} flows. Largest: ${big.map((r) => `${names[r.i]} ${directed ? '→' : '↔'} ${names[r.j]} (${fmt(r.v)})`).join('; ')}.`
212
+ : `Diagrama de cuerdas con ${order.length} grupos y ${ribbons.length} flujos. Mayores: ${big.map((r) => `${names[r.i]} ${directed ? '→' : '↔'} ${names[r.j]} (${fmt(r.v)})`).join('; ')}.`;
213
+ return frame.finish(body, { desc, type: 'chord' });
214
+ }
215
+
216
+ function esc(s) {
217
+ return String(s).replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]);
218
+ }