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/charts/dist.js
CHANGED
|
@@ -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(
|
|
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 ?
|
|
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
|
-
|
|
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)
|
|
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 ?
|
|
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(
|
|
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
|
|
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
|
-
|
|
330
|
-
let
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
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
|
|
482
|
-
|
|
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))}`) }, [
|
package/src/charts/donut.js
CHANGED
|
@@ -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;
|