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/stats.js
CHANGED
|
@@ -6,6 +6,20 @@ export const finite = (vs) => vs.filter((v) => Number.isFinite(v));
|
|
|
6
6
|
export const sum = (vs) => vs.reduce((a, b) => a + b, 0);
|
|
7
7
|
export const mean = (vs) => (vs.length ? sum(vs) / vs.length : NaN);
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* [mín, máx] de los valores finitos con un bucle: `Math.min(...arr)` desborda
|
|
11
|
+
* la pila con ~150 mil elementos. Vacío → [Infinity, -Infinity] (como Math.min()).
|
|
12
|
+
*/
|
|
13
|
+
export function extent(vs) {
|
|
14
|
+
let lo = Infinity, hi = -Infinity;
|
|
15
|
+
for (const v of vs) {
|
|
16
|
+
if (!Number.isFinite(v)) continue;
|
|
17
|
+
if (v < lo) lo = v;
|
|
18
|
+
if (v > hi) hi = v;
|
|
19
|
+
}
|
|
20
|
+
return [lo, hi];
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
export function variance(vs) {
|
|
10
24
|
const n = vs.length;
|
|
11
25
|
if (n < 2) return 0;
|
|
@@ -97,11 +111,13 @@ export function histogram(vs, { bins, binWidth, domain } = {}) {
|
|
|
97
111
|
}
|
|
98
112
|
step = bins ? (hi - lo) / bins : niceBinStep(hi - lo, n);
|
|
99
113
|
}
|
|
100
|
-
const start = bins ? lo : Math.floor(lo / step + 1e-9) * step;
|
|
114
|
+
const start = bins ? lo : +(Math.floor(lo / step + 1e-9) * step).toPrecision(15);
|
|
101
115
|
const count = Math.max(1, Math.ceil((hi - start) / step - 1e-9));
|
|
102
|
-
const out = Array.from({ length: count }, (_, i) => ({ x0: start + i * step, x1: start + (i + 1) * step, count: 0 }));
|
|
116
|
+
const out = Array.from({ length: count }, (_, i) => ({ x0: +(start + i * step).toPrecision(15), x1: +(start + (i + 1) * step).toPrecision(15), count: 0 }));
|
|
103
117
|
for (const v of s) {
|
|
104
|
-
|
|
118
|
+
// Con `domain`, lo que cae fuera no se cuenta (antes se apilaba en la última barra).
|
|
119
|
+
if (domain && (v < domain[0] || v > domain[1])) continue;
|
|
120
|
+
let i = Math.floor((v - start) / step + 1e-9);
|
|
105
121
|
if (i >= count) i = count - 1;
|
|
106
122
|
if (i >= 0) out[i].count++;
|
|
107
123
|
}
|
|
@@ -112,11 +128,16 @@ export function histogram(vs, { bins, binWidth, domain } = {}) {
|
|
|
112
128
|
export function scottBandwidth(vs) {
|
|
113
129
|
const v = finite(vs);
|
|
114
130
|
const s = sd(v);
|
|
115
|
-
const
|
|
131
|
+
const sorted = [...v].sort((a, b) => a - b);
|
|
132
|
+
const iqr = quantile(sorted, 0.75) - quantile(sorted, 0.25);
|
|
116
133
|
const sigma = Math.min(s, iqr / 1.349) || s || 1;
|
|
117
134
|
return 1.06 * sigma * v.length ** -0.2;
|
|
118
135
|
}
|
|
119
136
|
|
|
137
|
+
const extentOf = extent;
|
|
138
|
+
/** Sobre este número de valores, la KDE usa binning lineal. */
|
|
139
|
+
export const KDE_BIN_MIN = 2048;
|
|
140
|
+
|
|
120
141
|
/**
|
|
121
142
|
* Densidad por kernel gaussiano. Devuelve [[x, densidad], ...] en una grilla.
|
|
122
143
|
* `cut` extiende la grilla `cut` anchos de banda más allá de los extremos.
|
|
@@ -125,14 +146,29 @@ export function kde(vs, { bandwidth, adjust = 1, gridSize = 160, extent, cut = 3
|
|
|
125
146
|
const v = finite(vs);
|
|
126
147
|
if (!v.length) return [];
|
|
127
148
|
const bw = (bandwidth ?? scottBandwidth(v)) * adjust || 1;
|
|
128
|
-
|
|
149
|
+
const [vmin, vmax] = extentOf(v);
|
|
150
|
+
let [lo, hi] = extent || [vmin - cut * bw, vmax + cut * bw];
|
|
129
151
|
if (clip) { lo = Math.max(lo, clip[0]); hi = Math.min(hi, clip[1]); }
|
|
130
152
|
const k = 1 / (v.length * bw * Math.sqrt(2 * Math.PI));
|
|
153
|
+
// Muchos valores: binning lineal en ~1024 nodos (cada valor reparte su peso
|
|
154
|
+
// entre los dos nodos vecinos). Error despreciable frente al ancho de banda.
|
|
155
|
+
let xsK = v, ws = null;
|
|
156
|
+
if (v.length > KDE_BIN_MIN && vmax > vmin) {
|
|
157
|
+
const B = 1024, dx = (vmax - vmin) / (B - 1);
|
|
158
|
+
ws = new Float64Array(B);
|
|
159
|
+
for (const xi of v) {
|
|
160
|
+
const t = (xi - vmin) / dx, j = Math.min(B - 2, Math.floor(t)), f = t - j;
|
|
161
|
+
ws[j] += 1 - f; ws[j + 1] += f;
|
|
162
|
+
}
|
|
163
|
+
xsK = Array.from({ length: B }, (_, j) => vmin + j * dx);
|
|
164
|
+
}
|
|
131
165
|
const out = [];
|
|
132
166
|
for (let i = 0; i < gridSize; i++) {
|
|
133
167
|
const x = lo + ((hi - lo) * i) / (gridSize - 1);
|
|
134
168
|
let d = 0;
|
|
135
|
-
|
|
169
|
+
if (ws) {
|
|
170
|
+
for (let j = 0; j < xsK.length; j++) { if (!ws[j]) continue; const u = (x - xsK[j]) / bw; if (u > -6 && u < 6) d += ws[j] * Math.exp(-0.5 * u * u); }
|
|
171
|
+
} else for (const xi of v) { const u = (x - xi) / bw; if (u > -6 && u < 6) d += Math.exp(-0.5 * u * u); }
|
|
136
172
|
out.push([x, d * k]);
|
|
137
173
|
}
|
|
138
174
|
return out;
|
|
@@ -216,28 +252,51 @@ export function pearson(xs, ys) {
|
|
|
216
252
|
return sxx && syy ? sxy / Math.sqrt(sxx * syy) : 0;
|
|
217
253
|
}
|
|
218
254
|
|
|
219
|
-
/**
|
|
220
|
-
|
|
255
|
+
/**
|
|
256
|
+
* LOWESS (Cleveland): suavizado local robusto. Devuelve [[x, y], ...] ordenado.
|
|
257
|
+
* Como x está ordenado, los r vecinos más cercanos forman una ventana contigua
|
|
258
|
+
* (dos punteros, sin ordenar distancias). Con muchos puntos (> 400) se ajusta
|
|
259
|
+
* solo en ~200 anclas y se interpola entre ellas (el `delta` de statsmodels).
|
|
260
|
+
*/
|
|
261
|
+
export function lowess(xs, ys, { frac = 2 / 3, iters = 2, delta } = {}) {
|
|
221
262
|
const pts = xs.map((x, i) => [x, ys[i]]).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y)).sort((a, b) => a[0] - b[0]);
|
|
222
263
|
const n = pts.length;
|
|
223
264
|
if (n < 3) return pts;
|
|
224
|
-
const
|
|
265
|
+
const X = pts.map((p) => p[0]), Y = pts.map((p) => p[1]);
|
|
266
|
+
const r = Math.min(n, Math.max(2, Math.ceil(frac * n)));
|
|
267
|
+
const dl = delta ?? (n > 400 ? (X[n - 1] - X[0]) / 200 : 0);
|
|
268
|
+
// Anclas: el primero, luego el primer punto a más de `dl` del ancla anterior, y el último.
|
|
269
|
+
const anchors = [0];
|
|
270
|
+
if (dl > 0) {
|
|
271
|
+
for (let i = 1; i < n; i++) if (X[i] > X[anchors[anchors.length - 1]] + dl) anchors.push(i);
|
|
272
|
+
if (anchors[anchors.length - 1] !== n - 1) anchors.push(n - 1);
|
|
273
|
+
} else for (let i = 1; i < n; i++) anchors.push(i);
|
|
225
274
|
let robust = new Array(n).fill(1);
|
|
226
|
-
|
|
275
|
+
const fit = new Array(n).fill(0);
|
|
227
276
|
for (let it = 0; it <= iters; it++) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
277
|
+
let lo = 0;
|
|
278
|
+
for (const i of anchors) {
|
|
279
|
+
const x0 = X[i];
|
|
280
|
+
while (lo + r < n && X[lo + r] - x0 < x0 - X[lo]) lo++;
|
|
281
|
+
const hiI = lo + r - 1;
|
|
282
|
+
const h = Math.max(x0 - X[lo], X[hiI] - x0) || 1e-12;
|
|
232
283
|
let sw = 0, swx = 0, swy = 0, swxx = 0, swxy = 0;
|
|
233
|
-
for (let j =
|
|
234
|
-
const u = Math.abs(
|
|
284
|
+
for (let j = lo; j <= hiI; j++) {
|
|
285
|
+
const u = Math.abs(X[j] - x0) / h;
|
|
235
286
|
const w = u < 1 ? (1 - u ** 3) ** 3 * robust[j] : 0;
|
|
236
|
-
sw += w; swx += w *
|
|
287
|
+
sw += w; swx += w * X[j]; swy += w * Y[j]; swxx += w * X[j] ** 2; swxy += w * X[j] * Y[j];
|
|
237
288
|
}
|
|
238
289
|
const den = sw * swxx - swx * swx;
|
|
239
290
|
fit[i] = Math.abs(den) < 1e-12 ? swy / (sw || 1) : ((swxx * swy - swx * swxy) + (sw * swxy - swx * swy) * x0) / den;
|
|
240
291
|
}
|
|
292
|
+
// Entre anclas: interpolación lineal (mismo x → mismo valor).
|
|
293
|
+
for (let a = 0; a < anchors.length - 1; a++) {
|
|
294
|
+
const i0 = anchors[a], i1 = anchors[a + 1];
|
|
295
|
+
for (let i = i0 + 1; i < i1; i++) {
|
|
296
|
+
const span = X[i1] - X[i0];
|
|
297
|
+
fit[i] = span ? fit[i0] + ((fit[i1] - fit[i0]) * (X[i] - X[i0])) / span : fit[i0];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
241
300
|
const res = pts.map(([, y], i) => Math.abs(y - fit[i]));
|
|
242
301
|
const m = median(res) || 1e-12;
|
|
243
302
|
robust = res.map((e) => { const u = e / (6 * m); return u < 1 ? (1 - u * u) ** 2 : 0; });
|
package/src/svg.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
import { ctx } from './context.js';
|
|
4
4
|
|
|
5
5
|
const ESC = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
6
|
-
|
|
6
|
+
// Además de escapar, quita los caracteres que XML 1.0 prohíbe (controles y
|
|
7
|
+
// sustitutos sueltos): un solo \x00 en un dato invalida todo el SVG.
|
|
8
|
+
const BAD_XML = /[\x00-\x08\x0B\x0C\x0E-\x1F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
9
|
+
export const esc = (s) => String(s ?? '').replace(BAD_XML, '').replace(/[&<>"']/g, (c) => ESC[c]);
|
|
7
10
|
|
|
8
11
|
/** Redondea a 2 decimales para SVG compactos. */
|
|
9
12
|
export const r2 = (n) => Math.round(n * 100) / 100;
|
package/src/text.js
CHANGED
|
@@ -24,6 +24,11 @@ set('—', 0.9);
|
|
|
24
24
|
set('▲▼■●', 0.9);
|
|
25
25
|
set('·', 0.3);
|
|
26
26
|
set('→', 0.9);
|
|
27
|
+
set('\u202F', 0.17); // espacio fino (miles en francés)
|
|
28
|
+
set('\u00A0', 0.27);
|
|
29
|
+
set('✓✗', 0.7);
|
|
30
|
+
set('‹›', 0.32);
|
|
31
|
+
set('#', 0.6);
|
|
27
32
|
|
|
28
33
|
export function measure(str, size = 12, weight = 400) {
|
|
29
34
|
let w = 0;
|
package/src/theme.js
CHANGED
|
@@ -14,6 +14,8 @@ export const MONO = "'Fragment Mono', ui-monospace, 'SFMono-Regular', Menlo, mon
|
|
|
14
14
|
export const FONT_CSS =
|
|
15
15
|
'https://fonts.googleapis.com/css2?family=Mozilla+Text:wght@400;500;600;700&display=swap';
|
|
16
16
|
|
|
17
|
+
import { resolveColorScale, interpolate, sample } from './colorscales.js';
|
|
18
|
+
|
|
17
19
|
// Tokens no cromáticos, compartidos por ambos temas y configurables por JSON.
|
|
18
20
|
const STRUCT = {
|
|
19
21
|
font: { family: FONT, mono: MONO, import: FONT_CSS },
|
|
@@ -105,28 +107,111 @@ export function deepMerge(base, over) {
|
|
|
105
107
|
return out;
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
// Colores válidos: hex, rgb()/hsl() y nombres CSS. Cualquier otra cosa se
|
|
111
|
+
// descarta: los colores viajan a atributos, CSS y tooltips (sin inyección).
|
|
112
|
+
const COLOR_RE = /^(#[0-9a-f]{3,8}|(rgba?|hsla?)\([\d\s.,%/+-]+\)|[a-z]{3,20}|transparent|none)$/i;
|
|
113
|
+
export const isColor = (c) => typeof c === 'string' && COLOR_RE.test(c.trim());
|
|
114
|
+
const COLOR_KEYS = ['surface', 'ink', 'lede', 'muted', 'grid', 'axis', 'tint', 'tint2', 'dim', 'other'];
|
|
115
|
+
|
|
116
|
+
/** Alias legibles de los tokens (background → surface, text → ink, …). */
|
|
117
|
+
function normalizeTokens(t) {
|
|
118
|
+
if (!isObj(t)) return t;
|
|
119
|
+
const o = { ...t };
|
|
120
|
+
if (o.background !== undefined && o.surface === undefined) o.surface = o.background;
|
|
121
|
+
if (o.text !== undefined && o.ink === undefined) o.ink = o.text;
|
|
122
|
+
if (o.textMuted !== undefined && o.muted === undefined) o.muted = o.textMuted;
|
|
123
|
+
// grid: color o { color, dash, width }.
|
|
124
|
+
if (isObj(o.grid)) { o.gridStyle = { ...(o.gridStyle || {}), ...o.grid }; if (o.grid.color) o.grid = o.grid.color; else delete o.grid; }
|
|
125
|
+
if (isObj(o.axis)) { o.axisStyle = { ...(o.axisStyle || {}), ...o.axis }; if (o.axis.color) o.axis = o.axis.color; else delete o.axis; }
|
|
126
|
+
if (o.barRadius !== undefined) o.marks = { ...(o.marks || {}), barRadius: o.barRadius };
|
|
127
|
+
if (o.radius !== undefined) o.marks = { ...(o.marks || {}), barRadius: o.marks?.barRadius ?? o.radius, radius: o.radius };
|
|
128
|
+
if (o.lineWidth !== undefined) o.marks = { ...(o.marks || {}), lineWidth: o.lineWidth };
|
|
129
|
+
if (o.padding !== undefined) o.layout = { ...(o.layout || {}), padding: o.padding };
|
|
130
|
+
if (typeof o.font === 'string') o.font = fontSpec(o.font);
|
|
131
|
+
return o;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Descarta colores inválidos (vuelve al del tema base). */
|
|
135
|
+
function sanitize(theme, base) {
|
|
136
|
+
for (const k of COLOR_KEYS) if (theme[k] !== undefined && !isColor(theme[k])) theme[k] = base[k];
|
|
137
|
+
for (const k of ['categorical', 'sequential', 'diverging']) {
|
|
138
|
+
if (Array.isArray(theme[k])) { const ok = theme[k].filter(isColor); theme[k] = ok.length ? ok : base[k]; }
|
|
139
|
+
}
|
|
140
|
+
if (theme.accent !== undefined && !isColor(theme.accent)) delete theme.accent;
|
|
141
|
+
if (isObj(theme.tooltip)) for (const k of ['bg', 'ink']) if (theme.tooltip[k] !== undefined && !isColor(theme.tooltip[k])) delete theme.tooltip[k];
|
|
142
|
+
if (isObj(theme.status)) for (const k of Object.keys(theme.status)) if (!isColor(theme.status[k])) theme.status[k] = base.status?.[k];
|
|
143
|
+
if (theme.font && typeof theme.font.import === 'string' && !/^https:\/\/[^\s'"()<>\\]+$/.test(theme.font.import)) theme.font = { ...theme.font, import: undefined };
|
|
144
|
+
if (theme.font && typeof theme.font.family === 'string') theme.font = { ...theme.font, family: theme.font.family.replace(/[<>{};]/g, '') };
|
|
145
|
+
return theme;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Paleta categórica por nombre: propias, matplotlib cualitativas o muestras de una colormap. */
|
|
149
|
+
function namedPalette(name, n = 8) {
|
|
150
|
+
if (palettes[name]) return palettes[name];
|
|
151
|
+
const cs = resolveColorScale(name);
|
|
152
|
+
if (!cs) return null;
|
|
153
|
+
if (cs.kind === 'qualitative') return cs.stops;
|
|
154
|
+
// Escala continua como paleta: n muestras sin los extremos (estilo seaborn).
|
|
155
|
+
return Array.from({ length: n }, (_, i) => interpolate(cs.stops, (i + 1) / (n + 1)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Rampa secuencial/divergente por nombre: propias o cualquier colormap. */
|
|
159
|
+
function namedRamp(name) {
|
|
160
|
+
if (ramps[name]) return ramps[name];
|
|
161
|
+
const cs = resolveColorScale(name);
|
|
162
|
+
return cs && cs.kind !== 'qualitative' ? sample(cs.stops, 17) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
108
165
|
/**
|
|
109
|
-
*
|
|
110
|
-
*
|
|
166
|
+
* Registra un tema con nombre (se fusiona sobre `extends`, por defecto papel):
|
|
167
|
+
* registerTheme('acme', { background: '#FFF', ink: '#111', categorical: 'tab10', grid: { dash: '2 3' } })
|
|
168
|
+
* configure({ theme: 'acme' }) // todos los gráficos
|
|
169
|
+
*/
|
|
170
|
+
export function registerTheme(name, theme) {
|
|
171
|
+
if (!name || typeof name !== 'string') throw new TypeError('registerTheme(name, theme): falta el nombre.');
|
|
172
|
+
if (name === 'papel' || name === 'noche' || name === 'light' || name === 'dark') throw new TypeError(`"${name}" es un tema de fábrica: usa otro nombre o { extends: "${name}" }.`);
|
|
173
|
+
themes[name] = { ...getTheme(theme || {}), name };
|
|
174
|
+
return themes[name];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Tema = JSON. Acepta un nombre ("papel", "noche" o uno registrado) o un
|
|
179
|
+
* objeto parcial que se fusiona en profundidad sobre `extends` (por defecto
|
|
180
|
+
* "papel"):
|
|
111
181
|
* { extends: "noche", categorical: ["#..."], type: { title: 24 }, marks: { lineWidth: 3 } }
|
|
112
|
-
*
|
|
182
|
+
* Alias: background, text, accent, grid { color, dash, width }, tooltip { bg, ink }, radius, barRadius, font.
|
|
183
|
+
* `categorical`, `sequential` y `diverging` aceptan también un nombre de paleta o colormap.
|
|
113
184
|
*/
|
|
114
185
|
export function getTheme(t) {
|
|
115
186
|
if (!t) return papel;
|
|
116
187
|
if (typeof t === 'string') return themes[t] || papel;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (typeof merged.
|
|
122
|
-
|
|
188
|
+
const over = normalizeTokens(t);
|
|
189
|
+
const base = (typeof over.extends === 'string' ? themes[over.extends] : isObj(over.extends) ? getTheme(over.extends) : null) || (over.mode === 'dark' ? noche : papel);
|
|
190
|
+
const merged = deepMerge(base, over);
|
|
191
|
+
delete merged.extends;
|
|
192
|
+
if (typeof merged.categorical === 'string') merged.categorical = namedPalette(merged.categorical) || base.categorical;
|
|
193
|
+
if (typeof merged.sequential === 'string') merged.sequential = namedRamp(merged.sequential) || base.sequential;
|
|
194
|
+
if (typeof merged.diverging === 'string') merged.diverging = namedRamp(merged.diverging) || base.diverging;
|
|
195
|
+
return sanitize(merged, base);
|
|
123
196
|
}
|
|
124
197
|
|
|
125
|
-
/** Tema efectivo de un spec: `theme` + atajos `palette` y `
|
|
198
|
+
/** Tema efectivo de un spec: `theme` + atajos `palette`, `ramp` y `colorScale`. */
|
|
126
199
|
export function themeFor(spec = {}) {
|
|
127
200
|
let t = getTheme(spec.theme);
|
|
128
|
-
if (spec.palette)
|
|
129
|
-
|
|
201
|
+
if (spec.palette) {
|
|
202
|
+
const p = Array.isArray(spec.palette) ? spec.palette.filter(isColor) : namedPalette(spec.palette);
|
|
203
|
+
if (p && p.length) t = { ...t, categorical: p };
|
|
204
|
+
}
|
|
205
|
+
if (spec.ramp) {
|
|
206
|
+
const r = Array.isArray(spec.ramp) ? spec.ramp.filter(isColor) : namedRamp(spec.ramp);
|
|
207
|
+
if (r && r.length >= 2) t = { ...t, sequential: r };
|
|
208
|
+
}
|
|
209
|
+
const cs = resolveColorScale(spec.colorScale ?? spec.cmap ?? spec.colormap);
|
|
210
|
+
if (cs && cs.kind !== 'qualitative') {
|
|
211
|
+
const stops = sample(cs.stops, 17);
|
|
212
|
+
// `cmap`: la escala se usa completa (sin recortar el extremo claro como la rampa de fábrica).
|
|
213
|
+
t = cs.kind === 'diverging' ? { ...t, diverging: stops, cmap: cs.name } : { ...t, sequential: [stops[0], ...stops], cmap: cs.name };
|
|
214
|
+
}
|
|
130
215
|
return t;
|
|
131
216
|
}
|
|
132
217
|
|
|
@@ -185,11 +270,13 @@ export function ramp(stops, t) {
|
|
|
185
270
|
*/
|
|
186
271
|
export function colorMap(keys, theme, overrides = {}) {
|
|
187
272
|
const map = new Map();
|
|
273
|
+
// Los colores fijados por el usuario no se repiten en las demás entidades.
|
|
274
|
+
const taken = new Set(Object.values(overrides || {}).map((c) => String(c).toLowerCase()));
|
|
275
|
+
const free = theme.categorical.filter((c) => !taken.has(String(c).toLowerCase()));
|
|
188
276
|
let i = 0;
|
|
189
277
|
for (const k of keys) {
|
|
190
278
|
if (overrides && overrides[k]) map.set(k, overrides[k]);
|
|
191
|
-
else map.set(k, i <
|
|
192
|
-
if (!(overrides && overrides[k])) i++;
|
|
279
|
+
else map.set(k, i < free.length ? free[i++] : theme.other);
|
|
193
280
|
}
|
|
194
281
|
return map;
|
|
195
282
|
}
|