plotagents 0.0.0-stage → 0.5.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 (60) hide show
  1. package/AGENTS.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +205 -2
  4. package/bin/plotagents.js +49 -0
  5. package/dist/plotagents-geo.js +79 -0
  6. package/dist/plotagents.js +7954 -0
  7. package/docs/api.md +154 -0
  8. package/docs/charts.md +218 -0
  9. package/docs/getting-started.md +138 -0
  10. package/docs/rendering.md +47 -0
  11. package/docs/themes.md +86 -0
  12. package/index.d.ts +213 -0
  13. package/package.json +74 -4
  14. package/schema.json +895 -0
  15. package/src/axes.js +144 -0
  16. package/src/charts/bar.js +339 -0
  17. package/src/charts/categorical.js +471 -0
  18. package/src/charts/chord.js +218 -0
  19. package/src/charts/choropleth.js +559 -0
  20. package/src/charts/dist.js +513 -0
  21. package/src/charts/donut.js +232 -0
  22. package/src/charts/flowchart.js +407 -0
  23. package/src/charts/heatmap.js +76 -0
  24. package/src/charts/kpi.js +97 -0
  25. package/src/charts/line.js +419 -0
  26. package/src/charts/network.js +344 -0
  27. package/src/charts/pyramid.js +177 -0
  28. package/src/charts/radar.js +152 -0
  29. package/src/charts/regplot.js +364 -0
  30. package/src/charts/sankey.js +133 -0
  31. package/src/charts/scatter.js +136 -0
  32. package/src/charts/sunburst.js +135 -0
  33. package/src/charts/treemap.js +194 -0
  34. package/src/context.js +10 -0
  35. package/src/dashed.js +90 -0
  36. package/src/data.js +137 -0
  37. package/src/facet.js +84 -0
  38. package/src/format.js +171 -0
  39. package/src/frame.js +157 -0
  40. package/src/geo/chile.js +7 -0
  41. package/src/geo/data/chile-regiones.js +12 -0
  42. package/src/geo/data/usa-estados.js +12 -0
  43. package/src/geo/data/world.js +12 -0
  44. package/src/geo/projection.js +152 -0
  45. package/src/geo/registry.js +31 -0
  46. package/src/geo/usa.js +8 -0
  47. package/src/geo/world.js +7 -0
  48. package/src/hierarchy.js +139 -0
  49. package/src/index.js +271 -0
  50. package/src/infer.js +49 -0
  51. package/src/interact.js +362 -0
  52. package/src/lint.js +137 -0
  53. package/src/palette.js +59 -0
  54. package/src/runtime.js +304 -0
  55. package/src/scale.js +171 -0
  56. package/src/schema.js +181 -0
  57. package/src/stats.js +246 -0
  58. package/src/svg.js +85 -0
  59. package/src/text.js +61 -0
  60. package/src/theme.js +224 -0
package/src/theme.js ADDED
@@ -0,0 +1,224 @@
1
+ // Temas de plotagents.
2
+ //
3
+ // Las paletas categóricas están validadas (no a ojo) con el validador de
4
+ // paletas OKLab: banda de luminosidad, piso de croma, separación bajo
5
+ // daltonismo (protan/deutan) entre vecinos y piso de visión normal.
6
+ // papel · adyacentes ΔE CVD 13,7 · visión normal 22,6 · primeros 3 todos-los-pares 16,5
7
+ // noche · adyacentes ΔE CVD 9,1 · visión normal 19,6 · primeros 3 todos-los-pares 9,1
8
+ // El ORDEN de los colores es parte de la seguridad: no lo cambies sin volver
9
+ // a validar.
10
+
11
+ export const FONT =
12
+ "'Mozilla Text', 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif";
13
+ export const MONO = "'Fragment Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace";
14
+ export const FONT_CSS =
15
+ 'https://fonts.googleapis.com/css2?family=Mozilla+Text:wght@400;500;600;700&display=swap';
16
+
17
+ // Tokens no cromáticos, compartidos por ambos temas y configurables por JSON.
18
+ const STRUCT = {
19
+ font: { family: FONT, mono: MONO, import: FONT_CSS },
20
+ type: { title: 22, subtitle: 14.5, label: 12, axis: 11.5, legend: 12.5, source: 11, titleWeight: 600 },
21
+ marks: { barRadius: 4, barMax: 36, barMaxH: 22, lineWidth: 2.25, pointRadius: 4, gap: 2, ring: 2, areaOpacity: 0.12, bandOpacity: 0.16, dotOpacity: 0.85 },
22
+ axes: { inside: true, grid: true },
23
+ layout: { padding: 24, paddingSmall: 12 },
24
+ };
25
+
26
+ const papel = {
27
+ name: 'papel',
28
+ mode: 'light',
29
+ surface: '#FCFBF9',
30
+ ink: '#141A3F',
31
+ lede: '#4A5070',
32
+ muted: '#7C819C',
33
+ grid: '#E9EBF3',
34
+ axis: '#C9CDE0',
35
+ tint: '#ECEEF8',
36
+ tint2: '#D9DDF3',
37
+ dim: '#CDD1E3', // marcas fuera de foco (highlight)
38
+ other: '#A5AAC0', // "Otros"
39
+ link: 0.32,
40
+ categorical: [
41
+ '#3A47B8', // índigo
42
+ '#D42A5E', // magenta
43
+ '#E8A33D', // ámbar
44
+ '#1E9C89', // turquesa
45
+ '#7B5CD6', // violeta
46
+ '#B5562E', // óxido
47
+ '#5BA3E0', // cielo
48
+ '#8E9A2F', // oliva
49
+ ],
50
+ // Secuencial: un solo tono, claro → oscuro.
51
+ sequential: ['#EEF0FA', '#D9DDF3', '#B3BAE8', '#8590D8', '#5A66C7', '#3A47B8', '#26307F', '#141A3F'],
52
+ // Divergente: índigo ↔ gris neutro ↔ magenta.
53
+ diverging: ['#26307F', '#5A66C7', '#A9B0E2', '#ECEBE6', '#EFA9BE', '#D9557E', '#9E1D45'],
54
+ status: { good: '#178A68', warning: '#B87A00', critical: '#C9531A' },
55
+ ...STRUCT,
56
+ };
57
+
58
+ const noche = {
59
+ name: 'noche',
60
+ mode: 'dark',
61
+ surface: '#0B0F2B',
62
+ ink: '#F2F3FA',
63
+ lede: '#B4BAE0',
64
+ muted: '#8A90B4',
65
+ grid: '#1B2147',
66
+ axis: '#2E3666',
67
+ tint: '#151A3D',
68
+ tint2: '#1F2654',
69
+ dim: '#2A3160',
70
+ other: '#4A5280',
71
+ link: 0.4,
72
+ categorical: ['#6170E0', '#E63369', '#C4851C', '#22A892', '#9780E8', '#D06A3E', '#499BDA', '#909D2D'],
73
+ sequential: ['#141A3F', '#1F2862', '#2C3888', '#3A47B8', '#6170E0', '#8E99EA', '#BEC5F3', '#E6E9FB'],
74
+ diverging: ['#BEC5F3', '#7F8BE6', '#3F4CA8', '#2A2E48', '#A3294F', '#E8577F', '#F7B3C7'],
75
+ status: { good: '#2BB38A', warning: '#E0A030', critical: '#E8743F' },
76
+ ...STRUCT,
77
+ };
78
+
79
+ export const themes = { papel, noche, light: papel, dark: noche };
80
+
81
+ // Paletas con nombre (categóricas). La de fábrica está validada; las demás
82
+ // son conocidas y se pueden revisar con validatePalette().
83
+ export const palettes = {
84
+ cdia: papel.categorical,
85
+ 'cdia-noche': noche.categorical,
86
+ 'okabe-ito': ['#0072B2', '#E69F00', '#009E73', '#CC79A7', '#56B4E9', '#D55E00', '#F0E442', '#000000'],
87
+ tableau10: ['#4E79A7', '#F28E2B', '#E15759', '#76B7B2', '#59A14F', '#EDC948', '#B07AA1', '#FF9DA7', '#9C755F', '#BAB0AC'],
88
+ udec: ['#223C6A', '#E69B0A', '#D21428', '#5C7DB8'],
89
+ };
90
+ // Rampas secuenciales (un tono, claro → oscuro) y divergentes.
91
+ export const ramps = {
92
+ indigo: papel.sequential,
93
+ magenta: ['#FBEAF0', '#F6CADA', '#EE9DBA', '#E46F98', '#D42A5E', '#A81F4A', '#7A1636', '#4D0E22'],
94
+ teal: ['#E6F5F2', '#C2E8E1', '#8CD3C6', '#52BBA8', '#1E9C89', '#167A6B', '#0F5A4F', '#093B34'],
95
+ amber: ['#FDF3E1', '#F9E0B5', '#F4C77E', '#EEAD4E', '#E8A33D', '#BF7F1F', '#8C5C14', '#5C3C0C'],
96
+ 'indigo-magenta': papel.diverging,
97
+ 'teal-rust': ['#0F5A4F', '#1E9C89', '#8CD3C6', '#ECEBE6', '#E8B49A', '#C46A3F', '#7F3A1B'],
98
+ };
99
+
100
+ const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
101
+ export function deepMerge(base, over) {
102
+ if (!isObj(over)) return base;
103
+ const out = { ...base };
104
+ for (const [k, v] of Object.entries(over)) out[k] = isObj(v) && isObj(base[k]) ? deepMerge(base[k], v) : v;
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Tema = JSON. Acepta un nombre ("papel", "noche") o un objeto parcial que se
110
+ * fusiona en profundidad sobre `extends` (por defecto "papel"):
111
+ * { extends: "noche", categorical: ["#..."], type: { title: 24 }, marks: { lineWidth: 3 } }
112
+ * `categorical`, `sequential` y `diverging` aceptan también un nombre de paleta.
113
+ */
114
+ export function getTheme(t) {
115
+ if (!t) return papel;
116
+ 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;
123
+ }
124
+
125
+ /** Tema efectivo de un spec: `theme` + atajos `palette` y `ramp`. */
126
+ export function themeFor(spec = {}) {
127
+ 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 };
130
+ return t;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Utilidades de color
135
+
136
+ export function hexToRgb(hex) {
137
+ let h = String(hex).replace('#', '');
138
+ if (h.length === 3) h = h.split('').map((c) => c + c).join('');
139
+ const n = parseInt(h.slice(0, 6), 16);
140
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
141
+ }
142
+
143
+ export function rgbToHex([r, g, b]) {
144
+ const f = (v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
145
+ return ('#' + f(r) + f(g) + f(b)).toUpperCase();
146
+ }
147
+
148
+ const lin = (c) => {
149
+ c /= 255;
150
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
151
+ };
152
+ const delin = (c) => 255 * (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
153
+
154
+ export function luminance(hex) {
155
+ const [r, g, b] = hexToRgb(hex).map(lin);
156
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
157
+ }
158
+
159
+ export function contrast(a, b) {
160
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
161
+ return (hi + 0.05) / (lo + 0.05);
162
+ }
163
+
164
+ /** Texto blanco o tinta según qué contraste mejor sobre el relleno. */
165
+ export function textOn(fill, theme) {
166
+ const dark = theme.mode === 'dark' ? '#0B0F2B' : theme.ink;
167
+ return contrast('#FFFFFF', fill) >= contrast(dark, fill) ? '#FFFFFF' : dark;
168
+ }
169
+
170
+ /** Interpola en RGB lineal a lo largo de una rampa de colores. t ∈ [0,1]. */
171
+ export function ramp(stops, t) {
172
+ t = Math.max(0, Math.min(1, Number.isFinite(t) ? t : 0));
173
+ const p = t * (stops.length - 1);
174
+ const i = Math.min(stops.length - 2, Math.floor(p));
175
+ const f = p - i;
176
+ const a = hexToRgb(stops[i]).map(lin);
177
+ const b = hexToRgb(stops[i + 1]).map(lin);
178
+ return rgbToHex(a.map((v, k) => delin(v + (b[k] - v) * f)));
179
+ }
180
+
181
+ /**
182
+ * Asigna colores a entidades en orden fijo. El color sigue a la entidad,
183
+ * nunca a su posición: pasar `colors: {"Biobío": "#..."}` fija el color.
184
+ * Más allá de 8 entidades no se inventan tonos: el resto va en gris "Otros".
185
+ */
186
+ export function colorMap(keys, theme, overrides = {}) {
187
+ const map = new Map();
188
+ let i = 0;
189
+ for (const k of keys) {
190
+ 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++;
193
+ }
194
+ return map;
195
+ }
196
+
197
+ /**
198
+ * Versión legible de un color de serie para usarlo en TEXTO: lo oscurece
199
+ * (o aclara, en tema oscuro) hasta ≥ 4,5:1 contra el fondo. Así una etiqueta
200
+ * puede llevar el color de su línea sin perder legibilidad.
201
+ */
202
+ export function readable(color, theme, min = 4.5) {
203
+ const target = theme.mode === 'dark' ? '#FFFFFF' : theme.ink;
204
+ let c = color;
205
+ for (let t = 0; t <= 1.0001 && contrast(c, theme.surface) < min; t += 0.05) {
206
+ const A = hexToRgb(color), B = hexToRgb(target);
207
+ c = rgbToHex(A.map((v, i) => v + (B[i] - v) * t));
208
+ }
209
+ return c;
210
+ }
211
+
212
+ /**
213
+ * Tipografía a partir de un nombre: "Poppins" → pila con respaldo y la URL de
214
+ * Google Fonts para cargarla. Una pila completa ("'Inter', sans-serif") se usa
215
+ * tal cual. También acepta { family, import }.
216
+ */
217
+ export function fontSpec(font) {
218
+ if (!font) return null;
219
+ if (typeof font === 'object') return { ...font };
220
+ const f = String(font).trim();
221
+ if (/[,'"]/.test(f)) return { family: f };
222
+ const url = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(f).replace(/%20/g, '+')}:wght@400;500;600;700&display=swap`;
223
+ return { family: `'${f}', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif`, import: url };
224
+ }