plotagents 0.5.0 → 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.
- package/AGENTS.md +24 -4
- package/README.md +26 -14
- package/bin/plotagents.js +38 -14
- package/dist/plotagents-geo.js +1 -1
- package/dist/plotagents.js +4294 -637
- package/docs/api.md +23 -5
- package/docs/charts.md +143 -9
- package/docs/colorscales.md +46 -0
- package/docs/dashboards.md +79 -0
- package/docs/getting-started.md +6 -2
- package/docs/themes.md +39 -9
- package/index.d.ts +188 -17
- package/package.json +9 -3
- package/schema.json +1025 -46
- package/src/axes.js +11 -3
- package/src/charts/bar.js +28 -17
- package/src/charts/categorical.js +31 -7
- package/src/charts/chord.js +3 -3
- package/src/charts/choropleth.js +284 -36
- package/src/charts/dist.js +37 -20
- package/src/charts/donut.js +1 -1
- package/src/charts/gantt.js +460 -0
- package/src/charts/heatmap.js +2 -2
- package/src/charts/kpi.js +9 -6
- package/src/charts/line.js +173 -41
- package/src/charts/network.js +552 -189
- package/src/charts/pointmap.js +599 -0
- package/src/charts/radar.js +7 -1
- package/src/charts/regplot.js +7 -1
- package/src/charts/sankey.js +41 -5
- package/src/charts/scatter.js +100 -22
- package/src/charts/sunburst.js +5 -4
- package/src/charts/treemap.js +10 -7
- package/src/charts/wordcloud.js +163 -0
- package/src/colorscales.js +182 -0
- package/src/dashboard.js +438 -0
- package/src/dashed.js +1 -1
- package/src/data.js +2 -2
- package/src/format.js +46 -14
- package/src/frame.js +95 -26
- package/src/geo/projection.js +74 -9
- package/src/geo/registry.js +87 -8
- package/src/index.js +76 -11
- package/src/infer.js +10 -1
- package/src/interact.js +157 -36
- package/src/lint.js +289 -9
- package/src/runtime.js +209 -97
- package/src/scale.js +80 -10
- package/src/schema.js +187 -31
- package/src/stats.js +71 -14
- package/src/theme.js +101 -14
package/src/axes.js
CHANGED
|
@@ -10,7 +10,7 @@ export function gridY(ticks, y, area, theme, fmt, { labels = true, baseline = 0
|
|
|
10
10
|
for (const t of ticks) {
|
|
11
11
|
const py = Math.round(y(t)) + 0.5;
|
|
12
12
|
const isBase = t === baseline;
|
|
13
|
-
out.push(el('line', { x1: area.x, x2: area.x + area.w, y1: py, y2: py, stroke: isBase ? theme.axis : theme.grid,
|
|
13
|
+
out.push(el('line', { x1: area.x, x2: area.x + area.w, y1: py, y2: py, stroke: isBase ? theme.axis : theme.grid, ...gridAttrs(theme, isBase) }));
|
|
14
14
|
if (labels) out.push(text(area.x - 8, py + 4, fmt(t), { fill: theme.muted, fontSize: SIZES.axis, textAnchor: 'end' }));
|
|
15
15
|
}
|
|
16
16
|
return g({ class: 'pa-axis pa-axis-y' }, out);
|
|
@@ -21,7 +21,7 @@ export function gridX(ticks, x, area, theme, fmt, { labels = true, baseline = 0,
|
|
|
21
21
|
const out = [];
|
|
22
22
|
const items = ticks.map((t) => ({ pos: Math.round(x(t)) + 0.5, label: fmt(t), t }));
|
|
23
23
|
for (const it of items) {
|
|
24
|
-
if (lines) out.push(el('line', { x1: it.pos, x2: it.pos, y1: area.y, y2: area.y + area.h, stroke: it.t === baseline ? theme.axis : theme.grid,
|
|
24
|
+
if (lines) out.push(el('line', { x1: it.pos, x2: it.pos, y1: area.y, y2: area.y + area.h, stroke: it.t === baseline ? theme.axis : theme.grid, ...gridAttrs(theme, it.t === baseline) }));
|
|
25
25
|
}
|
|
26
26
|
if (labels) out.push(xLabels(items, area, theme));
|
|
27
27
|
return g({ class: 'pa-axis pa-axis-x' }, out);
|
|
@@ -76,7 +76,7 @@ export const yAxisTop = (theme, inside) => (isInside(theme, inside) ? 14 * ctx.t
|
|
|
76
76
|
export function yGrid(ticks, y, area, theme, { baseline = 0 } = {}) {
|
|
77
77
|
return g({ class: 'pa-grid' }, ticks.map((t) => {
|
|
78
78
|
const py = Math.round(y(t)) + 0.5;
|
|
79
|
-
return el('line', { x1: area.x, x2: area.x + area.w, y1: py, y2: py, stroke: t === baseline ? theme.axis : theme.grid,
|
|
79
|
+
return el('line', { x1: area.x, x2: area.x + area.w, y1: py, y2: py, stroke: t === baseline ? theme.axis : theme.grid, ...gridAttrs(theme, t === baseline) });
|
|
80
80
|
}));
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -142,3 +142,11 @@ export function quietCorner(points, inner, w, h = 14, pad = 8) {
|
|
|
142
142
|
}
|
|
143
143
|
return { x: best.tx, y: best.ty, anchor: best.anchor };
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
/** Trazo de grilla y base según el tema (theme.grid { dash, width }, theme.axis { width }). */
|
|
147
|
+
export function gridAttrs(theme, base = false) {
|
|
148
|
+
const st = base ? theme.axisStyle : theme.gridStyle;
|
|
149
|
+
const w = st && Number.isFinite(+st.width) ? +st.width : 1;
|
|
150
|
+
const dash = !base && st && st.dash && /^[\d.\s,]+$/.test(String(st.dash)) ? String(st.dash) : null;
|
|
151
|
+
return { strokeWidth: w, strokeDasharray: dash, shapeRendering: dash ? null : 'crispEdges' };
|
|
152
|
+
}
|
package/src/charts/bar.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
import { el, g, text, barPath, tip } from '../svg.js';
|
|
5
5
|
import { measure, truncate } from '../text.js';
|
|
6
6
|
import { makeFormat } from '../format.js';
|
|
7
|
-
import { niceDomain, niceStep, linear, band } from '../scale.js';
|
|
7
|
+
import { niceDomain, niceStep, linear, band, dateUnit } from '../scale.js';
|
|
8
8
|
import { colorMap } from '../theme.js';
|
|
9
9
|
import { ensureData, inferXY, requireField, melt, uniq, aggregate, num, fieldKind, humanize } from '../data.js';
|
|
10
10
|
import { gridY, gridX, xLabels, ticksWidth, refLinesY, refLinesX } from '../axes.js';
|
|
11
11
|
import { mean, sd, meanCI } from '../stats.js';
|
|
12
12
|
import { createFrame, SIZES, hash } from '../frame.js';
|
|
13
|
-
import { parseDate } from '../format.js';
|
|
13
|
+
import { parseDate, formatDate } from '../format.js';
|
|
14
14
|
import { dashedMatcher, fromTest, hatcher, dashedAttrs, dashedDefaultLabel } from '../dashed.js';
|
|
15
15
|
import { ctx } from '../context.js';
|
|
16
16
|
|
|
@@ -63,6 +63,7 @@ export function prepareBar(spec) {
|
|
|
63
63
|
}));
|
|
64
64
|
const total = (c) => values.get(c).reduce((a, b) => a + b, 0);
|
|
65
65
|
if (kind === 'year' || kind === 'number') cats.sort((a, b) => num(a) - num(b));
|
|
66
|
+
else if (kind === 'date') cats.sort((a, b) => parseDate(a) - parseDate(b));
|
|
66
67
|
const nominal = kind === 'category';
|
|
67
68
|
const sort = spec.sort ?? (nominal ? 'desc' : 'none');
|
|
68
69
|
if (sort === 'desc') cats.sort((a, b) => total(b) - total(a));
|
|
@@ -70,7 +71,8 @@ export function prepareBar(spec) {
|
|
|
70
71
|
else if (sort === 'alpha') cats.sort((a, b) => a.localeCompare(b, 'es'));
|
|
71
72
|
|
|
72
73
|
// Cola larga → "Otros" (nunca 40 barras ilegibles).
|
|
73
|
-
|
|
74
|
+
// Solo categorías nominales: los periodos (años, meses) nunca se pliegan en "Otros".
|
|
75
|
+
const top = spec.top ?? (nominal && cats.length > 40 ? 25 : null);
|
|
74
76
|
let folded = 0;
|
|
75
77
|
if (top && cats.length > top) {
|
|
76
78
|
const keep = cats.slice(0, top);
|
|
@@ -110,6 +112,8 @@ export function bar(spec, theme) {
|
|
|
110
112
|
if (m.errs.size) for (const e of m.errs.values()) for (const r of e) if (r) all.push(...r);
|
|
111
113
|
let vmin = Math.min(0, ...all), vmax = Math.max(0, ...all);
|
|
112
114
|
if (pct) { vmin = Math.min(0, vmin); vmax = Math.max(vmax, 1); }
|
|
115
|
+
// Conteos y enteros: marcas enteras (nunca 0,5 personas).
|
|
116
|
+
const allInt = !pct && all.length > 0 && all.every((v) => Number.isInteger(v));
|
|
113
117
|
const fmt = pct ? makeFormat({ type: 'percent', scale: 100, decimals: spec.format?.decimals }, locale) : makeFormat(spec.format, locale, all);
|
|
114
118
|
|
|
115
119
|
// Orientación
|
|
@@ -154,6 +158,7 @@ export function bar(spec, theme) {
|
|
|
154
158
|
const maxBar = spec.barWidth || (horizontal ? theme.marks?.barMaxH ?? 22 : theme.marks?.barMax ?? 36);
|
|
155
159
|
|
|
156
160
|
let area = frame.layout(legend);
|
|
161
|
+
const RAD = theme.marks?.barRadius ?? 4;
|
|
157
162
|
const body = [];
|
|
158
163
|
const refs = (spec.refLines || []).map((r) => ({ ...r, y: r.value ?? r.y }));
|
|
159
164
|
|
|
@@ -161,16 +166,21 @@ export function bar(spec, theme) {
|
|
|
161
166
|
// ── Columnas
|
|
162
167
|
if (spec.height === undefined && frame.height < 360) frame.setHeight(360);
|
|
163
168
|
const ticksCount = Math.max(3, Math.round(area.h / 64));
|
|
164
|
-
const nd = spec.yDomain ? { min: spec.yDomain[0], max: spec.yDomain[1], step: niceStep(spec.yDomain[1] - spec.yDomain[0], ticksCount) } : niceDomain(vmin, vmax, ticksCount);
|
|
169
|
+
const nd = spec.yDomain ? { min: spec.yDomain[0], max: spec.yDomain[1], step: niceStep(spec.yDomain[1] - spec.yDomain[0], ticksCount) } : niceDomain(vmin, vmax, ticksCount, { integer: allInt });
|
|
165
170
|
const dom = hideAxis && !spec.yDomain ? { min: vmin, max: vmax || 1 } : nd;
|
|
166
171
|
if (spec._domains) spec._domains.y = [dom.min, dom.max];
|
|
167
|
-
const leftM = hideAxis ? 0 : ticksWidth(linear(nd.min, nd.max, 0, 1).ticks(nd.step), fmt.tick(nd.step)) + 10;
|
|
168
|
-
const top = showLabels ?
|
|
172
|
+
const leftM = hideAxis ? 0 : ticksWidth(linear(nd.min, nd.max, 0, 1).ticks(nd.step), fmt.tick(nd.step, linear(nd.min, nd.max, 0, 1).ticks(nd.step))) + 10;
|
|
173
|
+
const top = showLabels ? 16 : 6;
|
|
169
174
|
const inner = { x: area.x + leftM, y: area.y + top * ctx.textScale, w: area.w - leftM, h: area.h - top * ctx.textScale - 26 * ctx.textScale };
|
|
170
175
|
const yS = linear(dom.min, dom.max, inner.y + inner.h, inner.y);
|
|
171
176
|
const xb = band(cats, inner.x, inner.x + inner.w, { inner: multi && !stack ? 0.2 : 0.3, outer: 0.1 });
|
|
172
|
-
if (!hideAxis) body.push(gridY(yS.ticks(nd.step), yS, inner, theme, fmt.tick(nd.step)));
|
|
177
|
+
if (!hideAxis) body.push(gridY(yS.ticks(nd.step), yS, inner, theme, fmt.tick(nd.step, yS.ticks(nd.step))));
|
|
173
178
|
const y0 = yS(0);
|
|
179
|
+
// Ancho de barra relativo a la banda (≈ 60 % del paso, como Chart.js y
|
|
180
|
+
// ECharts): con pocas categorías las barras no quedan como palitos entre
|
|
181
|
+
// grandes huecos. `barWidth` lo fija; theme.marks.barMax es el piso.
|
|
182
|
+
const capW = spec.barWidth || (stack ? Math.min(120, Math.max(maxBar * 1.4, xb.step * 0.6))
|
|
183
|
+
: k === 1 ? Math.min(120, Math.max(maxBar, xb.step * 0.6)) : Math.max(maxBar, Math.min(56, (xb.step * 0.7) / k)));
|
|
174
184
|
cats.forEach((c) => {
|
|
175
185
|
const bx = xb(c);
|
|
176
186
|
const bw = xb.bandwidth;
|
|
@@ -178,7 +188,7 @@ export function bar(spec, theme) {
|
|
|
178
188
|
const parts = [];
|
|
179
189
|
const hit = el('rect', { x: bx - xb.step * 0.12, y: inner.y, width: bw + xb.step * 0.24, height: inner.h, fill: 'transparent' });
|
|
180
190
|
if (stack) {
|
|
181
|
-
const w = Math.min(
|
|
191
|
+
const w = Math.min(capW, bw);
|
|
182
192
|
const x0 = bx + (bw - w) / 2;
|
|
183
193
|
const sg = segs.get(c);
|
|
184
194
|
const lastPos = sg.map((s, i) => (s.vv > 0 ? i : -1)).filter((i) => i >= 0).pop();
|
|
@@ -190,7 +200,7 @@ export function bar(spec, theme) {
|
|
|
190
200
|
const gap = s.from !== 0 ? 2 : 0;
|
|
191
201
|
if (s.vv > 0) h -= gap; else { top += gap; h -= gap; }
|
|
192
202
|
const outer = s.vv > 0 ? i === lastPos : i === lastNeg;
|
|
193
|
-
const d = barPath(x0, top, w, Math.max(0, h), s.vv > 0 ? 'up' : 'down', outer ?
|
|
203
|
+
const d = barPath(x0, top, w, Math.max(0, h), s.vv > 0 ? 'up' : 'down', outer ? RAD : 0);
|
|
194
204
|
parts.push(g({ class: 'pa-m', 'data-tip': tip(c, `${seriesKeys[i]}: ${fmt(pct ? s.vv : s.v)}`, dl(c, i)) }, [el('path', { d, ...paint(c, i) })]));
|
|
195
205
|
});
|
|
196
206
|
if (showLabels && !pct) {
|
|
@@ -200,14 +210,14 @@ export function bar(spec, theme) {
|
|
|
200
210
|
}
|
|
201
211
|
body.push(g({ 'data-tip': tip(c, ...sg.map((s, i) => `${seriesKeys[i]}: ${fmt(pct ? s.vv : s.v)}` + (isDashed(c, i) && dm ? ` (${dashedLabel})` : '')), pct ? '' : `Total: ${fmt(m.total(c))}`, from && from(xNum(c)) ? dashedLabel : '') }, [hit, ...parts]));
|
|
202
212
|
} else {
|
|
203
|
-
const w = Math.min(
|
|
213
|
+
const w = Math.min(capW, (bw - 2 * (k - 1)) / k);
|
|
204
214
|
const groupW = w * k + 2 * (k - 1);
|
|
205
215
|
const x0 = bx + (bw - groupW) / 2;
|
|
206
216
|
vs.forEach((v, i) => {
|
|
207
217
|
const bxI = x0 + i * (w + 2);
|
|
208
218
|
const yv = yS(v);
|
|
209
219
|
const top = Math.min(yv, y0), h = Math.abs(yv - y0);
|
|
210
|
-
const d = barPath(bxI, top, w, h, v >= 0 ? 'up' : 'down');
|
|
220
|
+
const d = barPath(bxI, top, w, h, v >= 0 ? 'up' : 'down', RAD);
|
|
211
221
|
const er0 = m.errs.get(c)?.[i];
|
|
212
222
|
const label = (multi ? `${seriesKeys[i]}: ${fmt(v)}` : fmt(v)) + (er0 ? ` (IC ${fmt(er0[0])}–${fmt(er0[1])})` : '');
|
|
213
223
|
parts.push(g({ class: 'pa-m', 'data-tip': tip(c, label, dl(c, i)) }, [
|
|
@@ -226,7 +236,8 @@ export function bar(spec, theme) {
|
|
|
226
236
|
// Línea base
|
|
227
237
|
body.push(el('line', { x1: inner.x, x2: inner.x + inner.w, y1: Math.round(y0) + 0.5, y2: Math.round(y0) + 0.5, stroke: theme.axis, strokeWidth: 1 }));
|
|
228
238
|
// Categorías
|
|
229
|
-
const
|
|
239
|
+
const dUnit = m.kind === 'date' ? dateUnit(cats.map((c) => parseDate(c))) : null;
|
|
240
|
+
const items = cats.map((c) => ({ pos: xb.center(c), label: m.nominal ? truncate(c, Math.max(xb.step - 6, 40), 12) : dUnit ? formatDate(parseDate(c), dUnit, locale) : c }));
|
|
230
241
|
body.push(xLabels(items, inner, theme, { y: inner.y + inner.h + 18, size: 12, color: theme.lede }));
|
|
231
242
|
body.push(refLinesY(refs, yS, inner, theme, fmt));
|
|
232
243
|
} else {
|
|
@@ -242,12 +253,12 @@ export function bar(spec, theme) {
|
|
|
242
253
|
const hasNeg = vmin < 0;
|
|
243
254
|
const inner = { x: area.x + catW, y: area.y + 4, w: area.w - catW - rightM - (hasNeg && showLabels ? valueLabelW + 10 : 0), h: area.h - (hideAxis ? 8 : 28) };
|
|
244
255
|
if (hasNeg && showLabels) inner.x += valueLabelW + 10;
|
|
245
|
-
const nd = spec.yDomain ? { min: spec.yDomain[0], max: spec.yDomain[1], step: niceStep(spec.yDomain[1] - spec.yDomain[0], Math.max(3, Math.round(inner.w / 110))) } : niceDomain(vmin, vmax, Math.max(3, Math.round(inner.w / 110)));
|
|
256
|
+
const nd = spec.yDomain ? { min: spec.yDomain[0], max: spec.yDomain[1], step: niceStep(spec.yDomain[1] - spec.yDomain[0], Math.max(3, Math.round(inner.w / 110))) } : niceDomain(vmin, vmax, Math.max(3, Math.round(inner.w / 110)), { integer: allInt });
|
|
246
257
|
const dom = hideAxis && !spec.yDomain ? { min: vmin, max: vmax || 1 } : nd;
|
|
247
258
|
if (spec._domains) spec._domains.y = [dom.min, dom.max];
|
|
248
259
|
const xS = linear(dom.min, dom.max, inner.x, inner.x + inner.w);
|
|
249
260
|
const yb = band(cats, inner.y, inner.y + inner.h, { inner: 0.32, outer: 0.1 });
|
|
250
|
-
if (!hideAxis) body.push(gridX(xS.ticks(nd.step), xS, inner, theme, fmt.tick(nd.step)));
|
|
261
|
+
if (!hideAxis) body.push(gridX(xS.ticks(nd.step), xS, inner, theme, fmt.tick(nd.step, xS.ticks(nd.step))));
|
|
251
262
|
const x0 = xS(0);
|
|
252
263
|
cats.forEach((c) => {
|
|
253
264
|
const by = yb(c);
|
|
@@ -256,7 +267,7 @@ export function bar(spec, theme) {
|
|
|
256
267
|
const parts = [];
|
|
257
268
|
const strong = !hl || hl.has(c);
|
|
258
269
|
parts.push(el('rect', { x: area.x, y: by - yb.step * 0.16, width: area.w, height: yb.step, fill: 'transparent' }));
|
|
259
|
-
parts.push(text(inner.x - 10 - (hasNeg && showLabels ? valueLabelW + 10 : 0), by + bh / 2 + 4.5, truncate(c,
|
|
270
|
+
parts.push(text(inner.x - 10 - (hasNeg && showLabels ? valueLabelW + 10 : 0), by + bh / 2 + 4.5, truncate(c, Math.min(maxCatW, area.w * 0.38) + 0.5, labelSize), { fill: strong ? theme.ink : theme.muted, fontSize: labelSize, fontWeight: hl && strong ? 600 : 400, textAnchor: 'end' }));
|
|
260
271
|
if (stack) {
|
|
261
272
|
const h = Math.min(maxBar, bh);
|
|
262
273
|
const y0b = by + (bh - h) / 2;
|
|
@@ -267,7 +278,7 @@ export function bar(spec, theme) {
|
|
|
267
278
|
const xa = xS(s.from), xb2 = xS(s.to);
|
|
268
279
|
let left = Math.min(xa, xb2), w = Math.abs(xb2 - xa);
|
|
269
280
|
if (s.from !== 0) { if (s.vv > 0) left += 2; w -= 2; }
|
|
270
|
-
const d = barPath(left, y0b, Math.max(0, w), h, s.vv > 0 ? 'right' : 'left', i === lastPos || s.vv < 0 ?
|
|
281
|
+
const d = barPath(left, y0b, Math.max(0, w), h, s.vv > 0 ? 'right' : 'left', i === lastPos || s.vv < 0 ? RAD : 0);
|
|
271
282
|
const fill = colorFor(c, i);
|
|
272
283
|
const lab = pct ? fmt(s.vv) : '';
|
|
273
284
|
const fits = lab && measure(lab, 11, 600) + 10 < w;
|
|
@@ -286,7 +297,7 @@ export function bar(spec, theme) {
|
|
|
286
297
|
vs.forEach((v, i) => {
|
|
287
298
|
const yy = y0b + i * (h + 2);
|
|
288
299
|
const xv = xS(v);
|
|
289
|
-
const d = barPath(Math.min(xv, x0), yy, Math.abs(xv - x0), h, v >= 0 ? 'right' : 'left');
|
|
300
|
+
const d = barPath(Math.min(xv, x0), yy, Math.abs(xv - x0), h, v >= 0 ? 'right' : 'left', RAD);
|
|
290
301
|
const er = m.errs.get(c)?.[i];
|
|
291
302
|
const erTxt = er ? ` (IC ${fmt(er[0])}–${fmt(er[1])})` : '';
|
|
292
303
|
parts.push(g({ class: 'pa-m', 'data-tip': tip(c, (multi ? `${seriesKeys[i]}: ` : '') + fmt(v) + erTxt, dl(c, i)) }, [el('path', { d, ...paint(c, i) }), errBar(er, (e) => [xS(e), yy + h / 2], false, theme)]));
|
|
@@ -7,7 +7,7 @@ import { measure, truncate } from '../text.js';
|
|
|
7
7
|
import { makeFormat } from '../format.js';
|
|
8
8
|
import { niceDomain, niceStep, linear, log, logDomain, band } from '../scale.js';
|
|
9
9
|
import { colorMap, readable } from '../theme.js';
|
|
10
|
-
import { ensureData, requireField, uniq, num, fieldsOf, fieldKind, humanize } from '../data.js';
|
|
10
|
+
import { ensureData, requireField, uniq, num, fieldsOf, fieldKind, humanize, PlotError } from '../data.js';
|
|
11
11
|
import { gridX, xLabels, yAxisMargin, yAxisTop, yGrid, yTickLabels, refLinesY, refLinesX } from '../axes.js';
|
|
12
12
|
import { createFrame } from '../frame.js';
|
|
13
13
|
import { ctx } from '../context.js';
|
|
@@ -45,8 +45,8 @@ function prepare(spec) {
|
|
|
45
45
|
if (cat) requireField(data, cat, 'x (categoría)');
|
|
46
46
|
if (hue) requireField(data, hue, 'hue');
|
|
47
47
|
const catOf = (r) => (cat ? String(r[cat] ?? '') : '');
|
|
48
|
-
const cats = spec.order ? spec.order.map(String) : uniq(data.map(catOf));
|
|
49
|
-
const hues = hue ? (spec.hueOrder ? spec.hueOrder.map(String) : uniq(data.map((r) => String(r[hue])))) : [''];
|
|
48
|
+
const cats = Array.isArray(spec.order) ? spec.order.map(String) : uniq(data.map(catOf));
|
|
49
|
+
const hues = hue ? (Array.isArray(spec.hueOrder) ? spec.hueOrder.map(String) : uniq(data.map((r) => String(r[hue])))) : [''];
|
|
50
50
|
const groups = new Map();
|
|
51
51
|
for (const r of data) {
|
|
52
52
|
const v = num(r[val]);
|
|
@@ -57,6 +57,13 @@ function prepare(spec) {
|
|
|
57
57
|
}
|
|
58
58
|
const get = (c, h) => groups.get(c + '\u0000' + h) || [];
|
|
59
59
|
const all = [...groups.values()].flat();
|
|
60
|
+
// Sin un solo número no hay eje de valores que dibujar.
|
|
61
|
+
if (!all.length) {
|
|
62
|
+
const en = String(spec.locale || 'es').startsWith('en');
|
|
63
|
+
throw en
|
|
64
|
+
? new PlotError(`No numeric values in \`${val}\`.`, `\`${val}\` must hold numbers (empty, null and non-finite values are skipped). To count rows per category, use \`type: "countplot"\`.`)
|
|
65
|
+
: new PlotError(`No hay valores numéricos en \`${val}\`.`, `\`${val}\` debe tener números (se omiten vacíos, nulos y valores no finitos). Para contar filas por categoría usa \`type: "countplot"\`.`);
|
|
66
|
+
}
|
|
60
67
|
return { data, cat, val, hue, cats, hues, get, all, swapped };
|
|
61
68
|
}
|
|
62
69
|
|
|
@@ -134,7 +141,7 @@ function setup(spec, theme, m, extent, { type, zero = false, dodge = true, rowSt
|
|
|
134
141
|
|
|
135
142
|
if (spec._domains && typeof spec._domains === 'object') spec._domains.y = [nd.min, nd.max];
|
|
136
143
|
// Líneas de referencia en el eje de valores (metas, umbrales).
|
|
137
|
-
const refs = (spec.refLines
|
|
144
|
+
const refs = [].concat(spec.refLines ?? []).filter((r) => r && typeof r === 'object').map((r) => ({ ...r, y: r.value ?? r.y ?? r.x }));
|
|
138
145
|
if (refs.length) body.post.push(vertical ? refLinesY(refs, valS, inner, theme, fmt) : refLinesX(refs.map((r) => ({ ...r, x: r.y, y: undefined })), valS, inner, theme, fmt));
|
|
139
146
|
|
|
140
147
|
// Posición en el eje de categorías: centro de cada grupo (con "dodge" por hue).
|
|
@@ -229,7 +236,13 @@ export function violin(spec, theme) {
|
|
|
229
236
|
const tf = logV ? Math.log10 : (v) => v, inv = logV ? (v) => 10 ** v : (v) => v;
|
|
230
237
|
for (const c of m.cats) for (const h of m.hues) {
|
|
231
238
|
const vs = m.get(c, h);
|
|
232
|
-
if (vs.length
|
|
239
|
+
if (!vs.length) continue;
|
|
240
|
+
// Un solo valor (o todos iguales) no tiene densidad: se dibuja como una marca.
|
|
241
|
+
if (vs.length < 2 || Math.min(...vs) === Math.max(...vs)) {
|
|
242
|
+
dens.set(c + '\u0000' + h, { single: true, f: fiveNumber(vs), n: vs.length, max: 0 });
|
|
243
|
+
ext.push(vs[0]);
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
233
246
|
const tv = vs.map(tf);
|
|
234
247
|
const bw = (spec.bandwidth ?? scottBandwidth(tv)) * (spec.bwAdjust ?? 1);
|
|
235
248
|
const mn = Math.min(...tv), mx = Math.max(...tv);
|
|
@@ -239,8 +252,8 @@ export function violin(spec, theme) {
|
|
|
239
252
|
}
|
|
240
253
|
const s = setup(spec, theme, m, extentOf(ext), { type: 'violin', dodge: !split });
|
|
241
254
|
const scaleMode = ['width', 'count', 'area'].includes(spec.scale) ? spec.scale : spec.densityNorm ?? 'width';
|
|
242
|
-
const gMax = Math.max(...[...dens.values()].map((x) => x.max));
|
|
243
|
-
const nMax = Math.max(...[...dens.values()].map((x) => x.n));
|
|
255
|
+
const gMax = Math.max(...[...dens.values()].filter((x) => !x.single).map((x) => x.max), 0);
|
|
256
|
+
const nMax = Math.max(...[...dens.values()].filter((x) => !x.single).map((x) => x.n), 1);
|
|
244
257
|
const inner = spec.inner ?? 'box';
|
|
245
258
|
const op = Math.min(0.5, (theme.marks?.bandOpacity ?? 0.16) * 2.2);
|
|
246
259
|
m.cats.forEach((c) => m.hues.forEach((h, hi2) => {
|
|
@@ -252,6 +265,17 @@ export function violin(spec, theme) {
|
|
|
252
265
|
const norm = scaleMode === 'width' ? D.max : scaleMode === 'count' ? gMax * (nMax / D.n) : gMax;
|
|
253
266
|
const wOf = (d) => (d / (norm || 1)) * half;
|
|
254
267
|
const side = split ? (hi2 === 0 ? -1 : 1) : 0;
|
|
268
|
+
if (D.single) {
|
|
269
|
+
// Sin forma que estimar: una raya a lo ancho del violín y un punto en el valor.
|
|
270
|
+
const v = D.f.median, w = Math.min(half, 18);
|
|
271
|
+
const [x, y] = s.P(cpos, v);
|
|
272
|
+
const parts = [
|
|
273
|
+
seg(s, cpos - (side === 1 ? 0 : w), v, cpos + (side === -1 ? 0 : w), v, { stroke: color, strokeWidth: 2, strokeLinecap: 'round' }),
|
|
274
|
+
el('circle', { cx: x, cy: y, r: 3.5, fill: color, stroke: theme.surface, strokeWidth: 1.5 }),
|
|
275
|
+
];
|
|
276
|
+
s.body.marks.push(g({ class: 'pa-m', 'data-tip': tip(fmtTipLabel(c, h), `n = ${D.n}`, `${T('Value', 'Valor', s.locale)}: ${s.fmt(v)}`) }, parts));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
255
279
|
const right = D.d.map(([v, d]) => s.P(cpos + (side === -1 ? 0 : wOf(d)), v));
|
|
256
280
|
const left = D.d.map(([v, d]) => s.P(cpos - (side === 1 ? 0 : wOf(d)), v)).reverse();
|
|
257
281
|
const pts = side === -1 ? [...D.d.map(([v]) => s.P(cpos, v)), ...left] : side === 1 ? [...right, ...D.d.map(([v]) => s.P(cpos, v)).reverse()] : [...right, ...left];
|
package/src/charts/chord.js
CHANGED
|
@@ -16,7 +16,7 @@ export function prepareChord(spec) {
|
|
|
16
16
|
if (Array.isArray(spec.matrix)) {
|
|
17
17
|
const names = (spec.names || spec.matrix.map((_, i) => String(i + 1))).map(String);
|
|
18
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) =>
|
|
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
20
|
}
|
|
21
21
|
const data = ensureData(spec);
|
|
22
22
|
const fields = fieldsOf(data);
|
|
@@ -33,7 +33,7 @@ export function prepareChord(spec) {
|
|
|
33
33
|
for (const r of data) {
|
|
34
34
|
if (r[from] === null || r[from] === undefined || r[to] === null || r[to] === undefined) continue;
|
|
35
35
|
const v = vf ? num(r[vf]) : 1;
|
|
36
|
-
if (!(v > 0)) continue;
|
|
36
|
+
if (!(v > 0) || !Number.isFinite(v)) continue;
|
|
37
37
|
flows.push([at(String(r[from])), at(String(r[to])), v]);
|
|
38
38
|
}
|
|
39
39
|
const m = names.map(() => names.map(() => 0));
|
|
@@ -78,7 +78,7 @@ export function chord(spec, theme) {
|
|
|
78
78
|
const fmt = makeFormat(spec.format, locale, ribbons.map((r) => r.v).concat(totals));
|
|
79
79
|
const pct = (a, b) => (b > 0 ? formatNumber((a / b) * 100, a / b < 0.1 ? 1 : 0, locale, true) + '%' : '');
|
|
80
80
|
|
|
81
|
-
const legend = spec.legend === true ? sizeOrder.map((i) => ({ label: names[i], color: colorOf(i) })) : [];
|
|
81
|
+
const legend = spec.legend === true || (spec.legend && typeof spec.legend !== 'boolean') ? sizeOrder.map((i) => ({ label: names[i], color: colorOf(i) })) : [];
|
|
82
82
|
const W = spec.width || 720;
|
|
83
83
|
const frame = createFrame(spec, theme, { height: W < 560 ? Math.round(W * 1.02 + 150) : 600 });
|
|
84
84
|
const area = frame.layout(legend);
|