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/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
|
}
|