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.
Files changed (51) hide show
  1. package/AGENTS.md +24 -4
  2. package/README.md +26 -14
  3. package/bin/plotagents.js +38 -14
  4. package/dist/plotagents-geo.js +1 -1
  5. package/dist/plotagents.js +4294 -637
  6. package/docs/api.md +23 -5
  7. package/docs/charts.md +143 -9
  8. package/docs/colorscales.md +46 -0
  9. package/docs/dashboards.md +79 -0
  10. package/docs/getting-started.md +6 -2
  11. package/docs/themes.md +39 -9
  12. package/index.d.ts +188 -17
  13. package/package.json +9 -3
  14. package/schema.json +1025 -46
  15. package/src/axes.js +11 -3
  16. package/src/charts/bar.js +28 -17
  17. package/src/charts/categorical.js +31 -7
  18. package/src/charts/chord.js +3 -3
  19. package/src/charts/choropleth.js +284 -36
  20. package/src/charts/dist.js +37 -20
  21. package/src/charts/donut.js +1 -1
  22. package/src/charts/gantt.js +460 -0
  23. package/src/charts/heatmap.js +2 -2
  24. package/src/charts/kpi.js +9 -6
  25. package/src/charts/line.js +173 -41
  26. package/src/charts/network.js +552 -189
  27. package/src/charts/pointmap.js +599 -0
  28. package/src/charts/radar.js +7 -1
  29. package/src/charts/regplot.js +7 -1
  30. package/src/charts/sankey.js +41 -5
  31. package/src/charts/scatter.js +100 -22
  32. package/src/charts/sunburst.js +5 -4
  33. package/src/charts/treemap.js +10 -7
  34. package/src/charts/wordcloud.js +163 -0
  35. package/src/colorscales.js +182 -0
  36. package/src/dashboard.js +438 -0
  37. package/src/dashed.js +1 -1
  38. package/src/data.js +2 -2
  39. package/src/format.js +46 -14
  40. package/src/frame.js +95 -26
  41. package/src/geo/projection.js +74 -9
  42. package/src/geo/registry.js +87 -8
  43. package/src/index.js +76 -11
  44. package/src/infer.js +10 -1
  45. package/src/interact.js +157 -36
  46. package/src/lint.js +289 -9
  47. package/src/runtime.js +209 -97
  48. package/src/scale.js +80 -10
  49. package/src/schema.js +187 -31
  50. package/src/stats.js +71 -14
  51. 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
- * Tema = JSON. Acepta un nombre ("papel", "noche") o un objeto parcial que se
110
- * fusiona en profundidad sobre `extends` (por defecto "papel"):
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
- * `categorical`, `sequential` y `diverging` aceptan también un nombre de paleta.
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 base = themes[t.extends] || (t.mode === 'dark' ? noche : papel);
118
- const merged = deepMerge(base, t);
119
- if (typeof merged.categorical === 'string') merged.categorical = palettes[merged.categorical] || base.categorical;
120
- if (typeof merged.sequential === 'string') merged.sequential = ramps[merged.sequential] || base.sequential;
121
- if (typeof merged.diverging === 'string') merged.diverging = ramps[merged.diverging] || base.diverging;
122
- return merged;
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 `ramp`. */
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) t = { ...t, categorical: Array.isArray(spec.palette) ? spec.palette : palettes[spec.palette] || t.categorical };
129
- if (spec.ramp) t = { ...t, sequential: Array.isArray(spec.ramp) ? spec.ramp : ramps[spec.ramp] || t.sequential };
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 < theme.categorical.length ? theme.categorical[i] : theme.other);
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
  }