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/geo/registry.js
CHANGED
|
@@ -1,31 +1,110 @@
|
|
|
1
1
|
// Registro de geometrías. El núcleo no trae mapas pesados: se registran al
|
|
2
2
|
// importar 'plotagents/geo/chile', 'plotagents/geo/usa' o 'plotagents/geo/world', o se pasa un
|
|
3
3
|
// GeoJSON propio en spec.geo.
|
|
4
|
+
//
|
|
5
|
+
// `geo` acepta, además de un nombre registrado:
|
|
6
|
+
// · un GeoJSON FeatureCollection, un Feature, un arreglo de Features o una geometría suelta
|
|
7
|
+
// · un TopoJSON (Topology); `geoObject` elige el objeto (por defecto, el primero)
|
|
8
|
+
// · en el navegador, una URL (render() la descarga; ver runtime.js)
|
|
4
9
|
import { PlotError } from '../data.js';
|
|
5
10
|
|
|
6
11
|
const REG = new Map();
|
|
7
12
|
|
|
8
|
-
export function registerGeo(name,
|
|
9
|
-
|
|
13
|
+
export function registerGeo(name, geo, aliases = []) {
|
|
14
|
+
const fc = toFeatureCollection(geo);
|
|
10
15
|
for (const n of [name, ...aliases]) REG.set(String(n).toLowerCase(), fc);
|
|
11
16
|
return fc;
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
/** ¿Parece una URL o una ruta a un archivo .json / .geojson / .topojson? */
|
|
20
|
+
export const isGeoUrl = (s) => typeof s === 'string' && (/^(https?:)?\/\//i.test(s) || /^(\.{0,2}\/|data:)/.test(s) || /\.(geo|topo)?json(\?.*)?$/i.test(s));
|
|
21
|
+
|
|
22
|
+
export function getGeo(geo, objectName) {
|
|
23
|
+
if (geo && typeof geo === 'object') return toFeatureCollection(geo, objectName);
|
|
24
|
+
if (isGeoUrl(geo)) {
|
|
25
|
+
throw new PlotError(
|
|
26
|
+
`"${geo}" es una URL o un archivo: plot() es sincrónico y no descarga nada.`,
|
|
27
|
+
"En Node, lee el archivo y pásalo como objeto: geo: JSON.parse(fs.readFileSync('mapa.geojson', 'utf8')). En el navegador, render(el, spec) sí acepta la URL."
|
|
28
|
+
);
|
|
19
29
|
}
|
|
20
30
|
const fc = REG.get(String(geo || '').toLowerCase());
|
|
21
31
|
if (!fc) {
|
|
22
32
|
const known = [...new Set(REG.keys())];
|
|
23
33
|
throw new PlotError(
|
|
24
34
|
`No hay una geometría registrada con el nombre "${geo}".`,
|
|
25
|
-
known.length ? `Disponibles: ${known.join(', ')}. O pasa un GeoJSON en \`geo\`.` : "Importa 'plotagents/geo/chile', 'plotagents/geo/usa' o 'plotagents/geo/world', o pasa un GeoJSON en `geo`."
|
|
35
|
+
known.length ? `Disponibles: ${known.join(', ')}. O pasa un GeoJSON o TopoJSON en \`geo\`.` : "Importa 'plotagents/geo/chile', 'plotagents/geo/usa' o 'plotagents/geo/world', o pasa un GeoJSON en `geo`."
|
|
26
36
|
);
|
|
27
37
|
}
|
|
28
38
|
return fc;
|
|
29
39
|
}
|
|
30
40
|
|
|
31
41
|
export const registeredGeo = () => [...new Set(REG.keys())];
|
|
42
|
+
|
|
43
|
+
const GEOMS = new Set(['Point', 'MultiPoint', 'LineString', 'MultiLineString', 'Polygon', 'MultiPolygon', 'GeometryCollection']);
|
|
44
|
+
const cache = new WeakMap();
|
|
45
|
+
|
|
46
|
+
/** Cualquier forma de GeoJSON / TopoJSON → FeatureCollection. */
|
|
47
|
+
export function toFeatureCollection(geo, objectName) {
|
|
48
|
+
if (!geo || typeof geo !== 'object') throw new PlotError('`geo` debe ser un nombre registrado, un GeoJSON o un TopoJSON.', 'geo: "chile" | "usa" | "world" | { type: "FeatureCollection", features: [...] }');
|
|
49
|
+
if (Array.isArray(geo)) return { type: 'FeatureCollection', features: geo.map(asFeature) };
|
|
50
|
+
if (geo.type === 'FeatureCollection') {
|
|
51
|
+
if (!Array.isArray(geo.features)) throw new PlotError('El FeatureCollection no tiene `features`.');
|
|
52
|
+
return geo.features.every((f) => f && f.type === 'Feature') ? geo : { ...geo, features: geo.features.map(asFeature) };
|
|
53
|
+
}
|
|
54
|
+
if (geo.type === 'Feature') return { type: 'FeatureCollection', features: [geo] };
|
|
55
|
+
if (GEOMS.has(geo.type)) return { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: geo }] };
|
|
56
|
+
if (geo.type === 'Topology') {
|
|
57
|
+
const key = objectName ?? '';
|
|
58
|
+
const memo = cache.get(geo) || {};
|
|
59
|
+
if (!memo[key]) { memo[key] = topoFeatures(geo, objectName); cache.set(geo, memo); }
|
|
60
|
+
return memo[key];
|
|
61
|
+
}
|
|
62
|
+
throw new PlotError(`No reconozco este \`geo\` (type: "${geo.type}").`, 'Usa un GeoJSON (FeatureCollection, Feature o geometría) o un TopoJSON (type: "Topology").');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function asFeature(f) {
|
|
66
|
+
if (f && f.type === 'Feature') return f;
|
|
67
|
+
if (f && GEOMS.has(f.type)) return { type: 'Feature', properties: {}, geometry: f };
|
|
68
|
+
throw new PlotError('Cada elemento de `geo` debe ser un Feature de GeoJSON.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── TopoJSON (decodificador mínimo, sin dependencias) ──────────────────────
|
|
72
|
+
function topoFeatures(topo, objectName) {
|
|
73
|
+
const names = Object.keys(topo.objects || {});
|
|
74
|
+
if (!names.length) throw new PlotError('El TopoJSON no tiene objetos.');
|
|
75
|
+
const name = objectName ?? names[0];
|
|
76
|
+
const obj = topo.objects[name];
|
|
77
|
+
if (!obj) throw new PlotError(`El TopoJSON no tiene el objeto "${name}".`, `Objetos disponibles: ${names.join(', ')}. Indícalo con \`geoObject\`.`);
|
|
78
|
+
const tf = topo.transform;
|
|
79
|
+
// Arcos absolutos (deshace la cuantización y la codificación delta).
|
|
80
|
+
const arcs = (topo.arcs || []).map((arc) => {
|
|
81
|
+
if (!tf) return arc.map((p) => p.slice(0, 2));
|
|
82
|
+
let x = 0, y = 0;
|
|
83
|
+
return arc.map((p) => { x += p[0]; y += p[1]; return [x * tf.scale[0] + tf.translate[0], y * tf.scale[1] + tf.translate[1]]; });
|
|
84
|
+
});
|
|
85
|
+
const point = (p) => (tf ? [p[0] * tf.scale[0] + tf.translate[0], p[1] * tf.scale[1] + tf.translate[1]] : p.slice(0, 2));
|
|
86
|
+
const line = (ids) => {
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const i of ids) {
|
|
89
|
+
const a = i < 0 ? arcs[~i].slice().reverse() : arcs[i];
|
|
90
|
+
if (!a) continue;
|
|
91
|
+
for (let k = out.length ? 1 : 0; k < a.length; k++) out.push(a[k]);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
const ring = (ids) => { const r = line(ids); if (r.length && (r[0][0] !== r[r.length - 1][0] || r[0][1] !== r[r.length - 1][1])) r.push(r[0]); return r; };
|
|
96
|
+
const geom = (o) => {
|
|
97
|
+
switch (o.type) {
|
|
98
|
+
case 'Point': return { type: 'Point', coordinates: point(o.coordinates) };
|
|
99
|
+
case 'MultiPoint': return { type: 'MultiPoint', coordinates: o.coordinates.map(point) };
|
|
100
|
+
case 'LineString': return { type: 'LineString', coordinates: line(o.arcs) };
|
|
101
|
+
case 'MultiLineString': return { type: 'MultiLineString', coordinates: o.arcs.map(line) };
|
|
102
|
+
case 'Polygon': return { type: 'Polygon', coordinates: o.arcs.map(ring) };
|
|
103
|
+
case 'MultiPolygon': return { type: 'MultiPolygon', coordinates: o.arcs.map((p) => p.map(ring)) };
|
|
104
|
+
default: return null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const feat = (o) => ({ type: 'Feature', id: o.id, properties: { ...(o.properties || {}), ...(o.id !== undefined && (o.properties || {}).id === undefined ? { id: o.id } : {}) }, geometry: geom(o) });
|
|
108
|
+
const list = obj.type === 'GeometryCollection' ? obj.geometries.map(feat) : [feat(obj)];
|
|
109
|
+
return { type: 'FeatureCollection', features: list.filter((f) => f.geometry) };
|
|
110
|
+
}
|
package/src/hierarchy.js
CHANGED
|
@@ -66,6 +66,12 @@ export function findNode(root, focus = []) {
|
|
|
66
66
|
return node;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Nodo de foco para dibujar: una hoja no se puede "abrir" (quedaría vacío), se usa su padre. */
|
|
70
|
+
export function focusNode(root, focus = []) {
|
|
71
|
+
const n = findNode(root, focus);
|
|
72
|
+
return !n.children.length && n.parent ? n.parent : n;
|
|
73
|
+
}
|
|
74
|
+
|
|
69
75
|
export const height = (n) => (n.children.length ? 1 + Math.max(...n.children.map(height)) : 0);
|
|
70
76
|
export const descendants = (n) => [n, ...n.children.flatMap(descendants)];
|
|
71
77
|
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Idioma: reconocimiento a partir del texto del spec y las pocas palabras que
|
|
2
|
+
// la biblioteca escribe por su cuenta (fuente, total, buscar…). Lo demás
|
|
3
|
+
// (paginación, flechas, ✓) se dibuja con símbolos que no dependen del idioma.
|
|
4
|
+
//
|
|
5
|
+
// detectLocale(['Ventas por región, 2025', 'Biobío']) → 'es'
|
|
6
|
+
// detectLocale(['Sales grew 12% in the South']) → 'en'
|
|
7
|
+
// locale: 'auto' en cualquier spec (y por defecto en `table`) lo usa.
|
|
8
|
+
|
|
9
|
+
export const LANGS = ['es', 'en', 'pt', 'fr', 'de'];
|
|
10
|
+
|
|
11
|
+
const WORDS = {
|
|
12
|
+
es: { source: 'Fuente: ', total: 'Total', search: 'Buscar', none: 'Sin resultados', clear: 'Borrar', page: 'Página', prev: 'Anterior', next: 'Siguiente', sort: 'Ordenar por', yes: 'Sí', no: 'No', rows: 'filas', table: 'Tabla', of: 'de' },
|
|
13
|
+
en: { source: 'Source: ', total: 'Total', search: 'Search', none: 'No results', clear: 'Clear', page: 'Page', prev: 'Previous', next: 'Next', sort: 'Sort by', yes: 'Yes', no: 'No', rows: 'rows', table: 'Table', of: 'of' },
|
|
14
|
+
pt: { source: 'Fonte: ', total: 'Total', search: 'Buscar', none: 'Sem resultados', clear: 'Limpar', page: 'Página', prev: 'Anterior', next: 'Próxima', sort: 'Ordenar por', yes: 'Sim', no: 'Não', rows: 'linhas', table: 'Tabela', of: 'de' },
|
|
15
|
+
fr: { source: 'Source : ', total: 'Total', search: 'Rechercher', none: 'Aucun résultat', clear: 'Effacer', page: 'Page', prev: 'Précédente', next: 'Suivante', sort: 'Trier par', yes: 'Oui', no: 'Non', rows: 'lignes', table: 'Tableau', of: 'sur' },
|
|
16
|
+
de: { source: 'Quelle: ', total: 'Gesamt', search: 'Suchen', none: 'Keine Treffer', clear: 'Löschen', page: 'Seite', prev: 'Zurück', next: 'Weiter', sort: 'Sortieren nach', yes: 'Ja', no: 'Nein', rows: 'Zeilen', table: 'Tabelle', of: 'von' },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Código de idioma soportado ('es', 'en', 'pt', 'fr', 'de') a partir de cualquier etiqueta ('pt-BR', 'en_US'…). */
|
|
20
|
+
export function langOf(locale) {
|
|
21
|
+
const k = String(locale || 'es').toLowerCase().slice(0, 2);
|
|
22
|
+
return LANGS.includes(k) ? k : 'es';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Palabras de interfaz en el idioma del gráfico. */
|
|
26
|
+
export const words = (locale) => WORDS[langOf(locale)];
|
|
27
|
+
|
|
28
|
+
// Palabras funcionales frecuentes de cada idioma. Una palabra compartida
|
|
29
|
+
// (de, la, que…) reparte su peso entre los idiomas que la usan.
|
|
30
|
+
const STOP = {
|
|
31
|
+
es: 'de la el los las y en por con para del que una un se al más es son sus su según entre como año años sin hasta desde ventas región regiones total mil millones porcentaje hogares personas este esta sobre cada ha han fue lidera mayor menor tasa crecimiento meta comuna comunas mes meses semana proyecto estado avance',
|
|
32
|
+
en: 'the of and in by for with to is are from on at than more its as per was were this that which share sales region regions total thousand million percent households people over each has have had leads largest smallest rate growth target month months week project status progress revenue year years',
|
|
33
|
+
pt: 'de da do das dos e em por com para que uma um ao mais é são sua seu entre como ano anos não até desde pelo pela no na nos nas vendas região total mil milhões taxa crescimento meta mês meses projeto estado cada foi tem',
|
|
34
|
+
fr: 'de la le les et en par pour avec du des que une un au aux plus est sont sa son ses entre comme année années sans jusqu depuis sur dans ventes région total mille millions taux croissance objectif mois semaine projet état chaque été',
|
|
35
|
+
de: 'der die das und in von mit für zu ist sind den dem des ein eine einer im am mehr als nach bei auf aus nicht jahr jahre umsatz region gesamt tausend millionen anteil wachstum ziel monat monate woche projekt status jede wurde hat',
|
|
36
|
+
};
|
|
37
|
+
const INDEX = new Map();
|
|
38
|
+
for (const [lang, list] of Object.entries(STOP)) for (const w of list.split(' ')) { if (!INDEX.has(w)) INDEX.set(w, []); INDEX.get(w).push(lang); }
|
|
39
|
+
|
|
40
|
+
// Letras que delatan un idioma (se suman a las palabras).
|
|
41
|
+
const CHARS = [
|
|
42
|
+
[/[ñ¿¡]/g, { es: 3 }],
|
|
43
|
+
[/[ãõ]/g, { pt: 3 }],
|
|
44
|
+
[/ç/g, { pt: 1.5, fr: 1.5 }],
|
|
45
|
+
[/[èêëàâùûœî]/g, { fr: 2 }],
|
|
46
|
+
[/[äöüß]/g, { de: 3 }],
|
|
47
|
+
[/[áíóú]/g, { es: 1, pt: 1 }],
|
|
48
|
+
[/é/g, { es: 0.5, pt: 0.5, fr: 0.8 }],
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Idioma más probable de unos textos (títulos, encabezados, categorías), o
|
|
53
|
+
* null si no hay señal suficiente. Determinista y sin diccionarios grandes.
|
|
54
|
+
*/
|
|
55
|
+
export function detectLocale(texts) {
|
|
56
|
+
const s = [].concat(texts || []).filter((t) => typeof t === 'string' && t).join(' \n ').toLowerCase().slice(0, 6000);
|
|
57
|
+
if (!s) return null;
|
|
58
|
+
const score = { es: 0, en: 0, pt: 0, fr: 0, de: 0 };
|
|
59
|
+
for (const tok of s.split(/[^\p{L}]+/u)) {
|
|
60
|
+
const langs = tok && INDEX.get(tok);
|
|
61
|
+
if (langs) for (const l of langs) score[l] += 1 / langs.length;
|
|
62
|
+
}
|
|
63
|
+
for (const [re, w] of CHARS) {
|
|
64
|
+
const n = (s.match(re) || []).length;
|
|
65
|
+
if (n) for (const [l, v] of Object.entries(w)) score[l] += Math.min(6, n) * v;
|
|
66
|
+
}
|
|
67
|
+
const ranked = Object.entries(score).sort((a, b) => b[1] - a[1]);
|
|
68
|
+
const [[best, a], [, b]] = ranked;
|
|
69
|
+
if (a < 1 || a < b * 1.25 + 0.25) return null;
|
|
70
|
+
return best;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Idioma del documento en el navegador (`<html lang>` o el del sistema), o null. */
|
|
74
|
+
export function browserLocale() {
|
|
75
|
+
// Solo en un navegador: Node 21+ también define `navigator`, y el SVG del servidor no debe depender de él.
|
|
76
|
+
if (typeof document === 'undefined') return null;
|
|
77
|
+
try {
|
|
78
|
+
const l = (document.documentElement && document.documentElement.lang) || (typeof navigator !== 'undefined' && navigator.language) || '';
|
|
79
|
+
const k = String(l).toLowerCase().slice(0, 2);
|
|
80
|
+
return LANGS.includes(k) ? k : null;
|
|
81
|
+
} catch { return null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Textos de un spec que delatan su idioma: título, bajada, fuente, nombres de
|
|
86
|
+
* columna y algunas categorías.
|
|
87
|
+
*/
|
|
88
|
+
export function specTexts(spec, data) {
|
|
89
|
+
const out = [spec.title, spec.subtitle, spec.source, spec.dashedLabel];
|
|
90
|
+
for (const c of [].concat(Array.isArray(spec.columns) ? spec.columns : [])) {
|
|
91
|
+
if (typeof c === 'string') out.push(c);
|
|
92
|
+
else if (c && typeof c === 'object') out.push(c.label, c.field);
|
|
93
|
+
}
|
|
94
|
+
if (Array.isArray(data)) {
|
|
95
|
+
const rows = data.slice(0, 40);
|
|
96
|
+
const keys = new Set();
|
|
97
|
+
for (const r of rows) if (r && typeof r === 'object') for (const [k, v] of Object.entries(r)) { keys.add(k); if (typeof v === 'string' && v.length > 2 && !/^[\d\s.,%$€£-]+$/.test(v)) out.push(v); }
|
|
98
|
+
// Nombres de campo con guiones bajos o camelCase: "ventas_region" → "ventas region".
|
|
99
|
+
for (const k of keys) out.push(String(k).replace(/[_-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2'));
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Resuelve `locale: "auto"`: idioma del texto, luego el del navegador, luego español. */
|
|
105
|
+
export function autoLocale(spec, data) {
|
|
106
|
+
return detectLocale(specTexts(spec, data)) || browserLocale() || 'es';
|
|
107
|
+
}
|
package/src/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// import { plot } from 'plotagents';
|
|
4
4
|
// const svg = plot({ data, x: 'region', y: 'ventas', title: 'Biobío lidera las ventas' });
|
|
5
5
|
//
|
|
6
|
-
import { getTheme, themeFor, themes, palettes, ramps, colorMap, fontSpec, FONT_CSS } from './theme.js';
|
|
6
|
+
import { getTheme, themeFor, themes, palettes, ramps, colorMap, fontSpec, deepMerge, registerTheme, isColor, FONT_CSS } from './theme.js';
|
|
7
7
|
import { withContext } from './context.js';
|
|
8
8
|
import { validatePalette } from './palette.js';
|
|
9
9
|
import { bar } from './charts/bar.js';
|
|
@@ -20,26 +20,34 @@ import { network } from './charts/network.js';
|
|
|
20
20
|
import { chord } from './charts/chord.js';
|
|
21
21
|
import { pyramid } from './charts/pyramid.js';
|
|
22
22
|
import { flowchart } from './charts/flowchart.js';
|
|
23
|
+
import { gantt } from './charts/gantt.js';
|
|
24
|
+
import { wordcloud, countWords } from './charts/wordcloud.js';
|
|
23
25
|
import { choropleth, symbolMap } from './charts/choropleth.js';
|
|
24
|
-
import {
|
|
26
|
+
import { pointmap, embedTiles, TILE_PROVIDERS } from './charts/pointmap.js';
|
|
27
|
+
import { registerGeo, getGeo, registeredGeo, toFeatureCollection } from './geo/registry.js';
|
|
25
28
|
// Mapas incluidos (Chile por regiones, estados de EE. UU. y países del mundo): listos sin importar nada más.
|
|
26
29
|
// El bundle del navegador los separa en dist/plotagents-geo.js.
|
|
27
30
|
import './geo/chile.js';
|
|
28
31
|
import './geo/world.js';
|
|
29
32
|
import './geo/usa.js';
|
|
30
33
|
import { heatmap } from './charts/heatmap.js';
|
|
34
|
+
import { corr, pairplot, correlations } from './charts/corr.js';
|
|
31
35
|
import { sankey } from './charts/sankey.js';
|
|
32
36
|
import { kpi } from './charts/kpi.js';
|
|
37
|
+
import { table } from './charts/table.js';
|
|
38
|
+
import { autoLocale, detectLocale } from './i18n.js';
|
|
33
39
|
import { ensureData, PlotError, fieldKind, uniq, num } from './data.js';
|
|
34
40
|
import { facet } from './facet.js';
|
|
35
41
|
import { parseDate } from './format.js';
|
|
36
42
|
import { inferType, TYPES, ALIASES, normalizeType } from './infer.js';
|
|
37
43
|
import { lint, _setProbe } from './lint.js';
|
|
38
|
-
import { schema, toolDefinition } from './schema.js';
|
|
44
|
+
import { schema, toolDefinition, dashboardSchema } from './schema.js';
|
|
39
45
|
import { attach, attachSource } from './interact.js';
|
|
40
|
-
import { render, _bind } from './runtime.js';
|
|
46
|
+
import { render, _bind, loadGeo } from './runtime.js';
|
|
47
|
+
import { dashboard, renderDashboard, planDashboard, lintDashboard, safeDashboard, mountDashboard, packRows, balancedRows, _bindDashboard } from './dashboard.js';
|
|
41
48
|
import { makeFormat, formatNumber, formatCompact } from './format.js';
|
|
42
49
|
import { esc } from './svg.js';
|
|
50
|
+
import { resolveColorScale, colorScaleNames, interpolate as interpolateColor, COLORMAPS, QUALITATIVE } from './colorscales.js';
|
|
43
51
|
|
|
44
52
|
const RENDERERS = {
|
|
45
53
|
bar: (s, t) => bar(s, t),
|
|
@@ -54,8 +62,11 @@ const RENDERERS = {
|
|
|
54
62
|
donut: (s, t) => donut(s, t),
|
|
55
63
|
pie: (s, t) => pie(s, t),
|
|
56
64
|
heatmap: (s, t) => heatmap(s, t),
|
|
65
|
+
corr: (s, t) => corr(s, t),
|
|
66
|
+
pairplot: (s, t) => pairplot(s, t),
|
|
57
67
|
sankey: (s, t) => sankey(s, t),
|
|
58
68
|
kpi: (s, t) => kpi(s, t),
|
|
69
|
+
table: (s, t) => table(s, t),
|
|
59
70
|
hist: (s, t) => hist(s, t),
|
|
60
71
|
kde: (s, t) => kde(s, t),
|
|
61
72
|
ecdf: (s, t) => ecdf(s, t),
|
|
@@ -72,8 +83,11 @@ const RENDERERS = {
|
|
|
72
83
|
chord: (s, t) => chord(s, t),
|
|
73
84
|
pyramid: (s, t) => pyramid(s, t),
|
|
74
85
|
flowchart: (s, t) => flowchart(s, t),
|
|
86
|
+
gantt: (s, t) => gantt(s, t),
|
|
87
|
+
wordcloud: (s, t) => wordcloud(s, t),
|
|
75
88
|
choropleth: (s, t) => choropleth(s, t),
|
|
76
89
|
symbolmap: (s, t) => symbolMap(s, t),
|
|
90
|
+
pointmap: (s, t) => pointmap(s, t),
|
|
77
91
|
};
|
|
78
92
|
|
|
79
93
|
|
|
@@ -82,19 +96,35 @@ const RENDERERS = {
|
|
|
82
96
|
export function resolve(spec = {}) {
|
|
83
97
|
if (!spec || typeof spec !== 'object') throw new PlotError('El spec debe ser un objeto.', 'plot({ data: [...], x: "campo", y: "campo" })');
|
|
84
98
|
const s = withDefaults(spec);
|
|
99
|
+
// refLines / annotations: un objeto suelto vale como lista de uno.
|
|
100
|
+
for (const k of ['refLines', 'annotations']) if (s[k] != null && !Array.isArray(s[k]) && typeof s[k] === 'object') s[k] = [s[k]];
|
|
85
101
|
if (s.color && !s.series && typeof s.color === 'string' && !s.color.startsWith('#')) s.series = s.color;
|
|
86
|
-
let type = s.type && s.type !== 'auto' ? String(s.type).toLowerCase() : null;
|
|
102
|
+
let type = s.type && s.type !== 'auto' ? String(s.type).trim().toLowerCase() : null;
|
|
87
103
|
if (type) {
|
|
88
104
|
const [t, extra] = normalizeType(type);
|
|
89
105
|
type = t;
|
|
90
106
|
for (const [k, v] of Object.entries(extra)) if (s[k] === undefined) s[k] = v;
|
|
91
107
|
}
|
|
92
108
|
if (!type) {
|
|
93
|
-
const data = s.items ? [] : ensureData(s);
|
|
109
|
+
const data = s.items || (!s.data && (typeof s.text === 'string' || Array.isArray(s.text))) ? [] : ensureData(s);
|
|
94
110
|
type = inferType(s, data);
|
|
95
111
|
}
|
|
96
112
|
if (!RENDERERS[type]) throw new PlotError(`Tipo de gráfico "${s.type}" no soportado.`, `Usa uno de: ${TYPES.join(', ')} o "auto".`);
|
|
97
113
|
s.type = type;
|
|
114
|
+
// Idioma: `locale: "auto"` (y las tablas sin `locale`) lo reconocen del texto del spec.
|
|
115
|
+
if (s.locale === 'auto' || (s.locale === undefined && type === 'table')) {
|
|
116
|
+
s.locale = autoLocale(s, Array.isArray(s.data) ? s.data : []);
|
|
117
|
+
}
|
|
118
|
+
// Colormap divergente (RdBu, coolwarm…): centrada en 0 si los datos lo cruzan; si no, en el medio.
|
|
119
|
+
const cs = resolveColorScale(s.colorScale ?? s.cmap ?? s.colormap);
|
|
120
|
+
if (cs && cs.kind === 'diverging' && s.midpoint === undefined && s.diverging === undefined) {
|
|
121
|
+
const vf = typeof s.colorScale === 'object' && s.colorScale.mid !== undefined ? null : s.value;
|
|
122
|
+
if (typeof s.colorScale === 'object' && s.colorScale?.mid !== undefined) s.midpoint = +s.colorScale.mid;
|
|
123
|
+
else if (vf && Array.isArray(s.data) && s.data.length && typeof s.data[0] === 'object') {
|
|
124
|
+
const vs = s.data.map((r) => num(r[vf])).filter(Number.isFinite);
|
|
125
|
+
if (vs.length) { let lo = Infinity, hi = -Infinity; for (const v of vs) { if (v < lo) lo = v; if (v > hi) hi = v; } s.midpoint = lo < 0 && hi > 0 ? 0 : (lo + hi) / 2; }
|
|
126
|
+
} else s.diverging = true;
|
|
127
|
+
}
|
|
98
128
|
return s;
|
|
99
129
|
}
|
|
100
130
|
|
|
@@ -105,6 +135,7 @@ export function resolve(spec = {}) {
|
|
|
105
135
|
export function plot(spec) {
|
|
106
136
|
let s = resolve(spec);
|
|
107
137
|
s = applyTime(s);
|
|
138
|
+
s = applyExclude(s);
|
|
108
139
|
const theme = themeFor(s);
|
|
109
140
|
const textScale = Math.max(0.5, Math.min(4, +(s.textScale ?? s.fontScale ?? s.labelScale ?? 1) || 1));
|
|
110
141
|
return withContext({ textScale }, () => {
|
|
@@ -131,7 +162,12 @@ export function configure(options) {
|
|
|
131
162
|
|
|
132
163
|
function withDefaults(spec) {
|
|
133
164
|
const s = { ...DEFAULTS, ...spec };
|
|
134
|
-
|
|
165
|
+
// Colores fijados por entidad: solo colores CSS válidos (viajan a atributos y al tooltip).
|
|
166
|
+
if (s.colors && typeof s.colors === 'object') s.colors = Object.fromEntries(Object.entries(s.colors).filter(([, c]) => isColor(c)));
|
|
167
|
+
// Tema global + tema del spec: el del spec se fusiona en profundidad sobre el global.
|
|
168
|
+
if (DEFAULTS.theme && spec.theme && typeof spec.theme === 'object' && spec.theme.extends === undefined) {
|
|
169
|
+
s.theme = typeof DEFAULTS.theme === 'string' ? { ...spec.theme, extends: DEFAULTS.theme } : deepMerge(DEFAULTS.theme, spec.theme);
|
|
170
|
+
}
|
|
135
171
|
// `font`: atajo de theme.font (familia + URL para cargarla).
|
|
136
172
|
const f = fontSpec(s.font);
|
|
137
173
|
if (f) {
|
|
@@ -173,6 +209,37 @@ function applyTime(s) {
|
|
|
173
209
|
return { ...s, data, colors: stableColors(s, all), subtitle: s.subtitle ? `${s.subtitle} · ${label}` : label, _time: { field: s.time, value: cur, values: vals }, _prev };
|
|
174
210
|
}
|
|
175
211
|
|
|
212
|
+
/** Campo que define las entradas de la leyenda (series, primer nivel, grupo…). */
|
|
213
|
+
function legendField(s) {
|
|
214
|
+
if (Array.isArray(s.path) && s.path.length) return s.path[0];
|
|
215
|
+
return s.series ?? s.hue ?? s.group ?? (['donut', 'pie'].includes(s.type) ? s.label ?? s.x : null) ?? (s.type === 'treemap' || s.type === 'sunburst' ? s.label ?? s.x : null);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* `exclude: ["Maule"]` (o shift + clic en la leyenda): quita esas entradas y
|
|
220
|
+
* recalcula ejes, participaciones y layout con lo que queda. Los colores no
|
|
221
|
+
* se mueven y las entradas excluidas siguen en la leyenda, apagadas.
|
|
222
|
+
*/
|
|
223
|
+
function applyExclude(s) {
|
|
224
|
+
const ex = s.exclude == null || s.exclude === false ? [] : [].concat(s.exclude).map(String);
|
|
225
|
+
if (!ex.length) return s;
|
|
226
|
+
// Tabla ancha: y: ["ingresos", "costos"] → se excluyen columnas.
|
|
227
|
+
if (Array.isArray(s.y) && !s.series && !s.hue) {
|
|
228
|
+
const keep = s.y.filter((k) => !ex.includes(String(k)));
|
|
229
|
+
if (!keep.length || keep.length === s.y.length) return s;
|
|
230
|
+
const cmap = colorMap(s.y, themeFor(s), s.colors);
|
|
231
|
+
return { ...s, y: keep, colors: { ...Object.fromEntries(cmap), ...(s.colors || {}) }, _excluded: s.y.filter((k) => ex.includes(String(k))) };
|
|
232
|
+
}
|
|
233
|
+
const f = legendField(s);
|
|
234
|
+
if (!f || s.items) return s;
|
|
235
|
+
const all = ensureData(s);
|
|
236
|
+
if (!all.length || !(f in all[0])) return s;
|
|
237
|
+
const keep = all.filter((r) => !ex.includes(String(r[f])));
|
|
238
|
+
if (!keep.length || keep.length === all.length) return s;
|
|
239
|
+
const present = new Set(all.map((r) => String(r[f])));
|
|
240
|
+
return { ...s, data: keep, colors: { ...stableColors(s, all), ...(s.colors || {}) }, _excluded: ex.filter((v) => present.has(v)) };
|
|
241
|
+
}
|
|
242
|
+
|
|
176
243
|
/**
|
|
177
244
|
* Colores fijos por entidad a lo largo de TODOS los periodos: al mover el
|
|
178
245
|
* slider, "Minería" sigue siendo índigo aunque cambie su tamaño o su orden.
|
|
@@ -200,9 +267,10 @@ export function safePlot(spec) {
|
|
|
200
267
|
const report = lint(spec || {});
|
|
201
268
|
try {
|
|
202
269
|
const svg = plot(spec);
|
|
203
|
-
|
|
270
|
+
// Se dibujó (con valores de respaldo), pero los errores de lint igual se devuelven: ok = sin errores.
|
|
271
|
+
return { ok: report.errors.length === 0, svg, type: report.type, description: describeSvg(svg), errors: report.errors, warnings: report.warnings, suggestions: report.suggestions };
|
|
204
272
|
} catch (e) {
|
|
205
|
-
const errors = report.errors.length ? report.errors : [{ code: 'render', message: e.message.split('\n')[0], fix: e.hint }];
|
|
273
|
+
const errors = report.errors.length ? report.errors : [{ code: 'render', message: e.message.split('\n')[0], fix: e.hint || 'Revisa los campos del spec contra el schema (`toolDefinition()`): textos donde van textos, listas donde van listas.' }];
|
|
206
274
|
return { ok: false, svg: null, type: report.type, description: '', errors, warnings: report.warnings, suggestions: report.suggestions };
|
|
207
275
|
}
|
|
208
276
|
}
|
|
@@ -224,9 +292,11 @@ function describeSvg(svg) {
|
|
|
224
292
|
*/
|
|
225
293
|
export function toHTML(specs, { title = 'Gráficos', lang = 'es', columns = 1 } = {}) {
|
|
226
294
|
const list = Array.isArray(specs) ? specs : [specs];
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
295
|
+
const themesOf = list.map((s) => themeFor(withDefaults(s)));
|
|
296
|
+
const dark = themesOf.every((t) => t.mode === 'dark');
|
|
297
|
+
// Fondo de la página = fondo del tema (también uno global de configure()).
|
|
298
|
+
const bg = themesOf.every((t) => t.surface === themesOf[0].surface) ? themesOf[0].surface : dark ? '#0B0F2B' : '#FCFBF9';
|
|
299
|
+
const needsGeo = list.some((s) => { try { return ['choropleth', 'symbolmap', 'pointmap'].includes(resolve(s).type); } catch { return false; } });
|
|
230
300
|
const bundle = bundleSource() && (needsGeo ? bundleSource() + '\n' + (bundleSource('plotagents-geo.js') || '') : bundleSource());
|
|
231
301
|
const safeJSON = (o) => JSON.stringify(o).replace(/</g, '\\u003c');
|
|
232
302
|
const charts = list.map((s, i) => `<figure class="pa" id="pa-${i}">${plot(s)}</figure>` + (bundle ? `<script type="application/json" id="pa-spec-${i}">${safeJSON(s)}</script>` : '')).join('\n');
|
|
@@ -239,7 +309,7 @@ export function toHTML(specs, { title = 'Gráficos', lang = 'es', columns = 1 }
|
|
|
239
309
|
${[...new Set([FONT_CSS, ...list.map((s) => themeFor(withDefaults(s)).font?.import).filter(Boolean)])].map((u) => `<link rel="stylesheet" href="${esc(u)}">`).join('\n')}
|
|
240
310
|
<style>
|
|
241
311
|
:root{color-scheme:${dark ? 'dark' : 'light'}}
|
|
242
|
-
body{margin:0;background:${bg};font-family:${
|
|
312
|
+
body{margin:0;background:${cssText(bg)};font-family:${cssText(themeFor(withDefaults(list[0])).font?.family || "'Mozilla Text',system-ui,sans-serif")}}
|
|
243
313
|
main{max-width:${columns > 1 ? 1500 : 820}px;margin:0 auto;padding:24px 16px;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,${columns > 1 ? 560 : 320}px),1fr));gap:24px}
|
|
244
314
|
figure.pa{margin:0;min-width:0}
|
|
245
315
|
figure.pa svg{width:100%;height:auto;display:block}
|
|
@@ -251,6 +321,9 @@ ${boot}
|
|
|
251
321
|
</body></html>`;
|
|
252
322
|
}
|
|
253
323
|
|
|
324
|
+
/** Texto dentro de <style>: las entidades HTML no se decodifican ahí (esc() rompería las comillas), así que se quitan los caracteres que cierran la regla o la etiqueta. */
|
|
325
|
+
const cssText = (v) => String(v ?? '').replace(/[<>{};\\&]/g, '');
|
|
326
|
+
|
|
254
327
|
/** Código de la biblioteca en un archivo (para incrustar en HTML). */
|
|
255
328
|
export function bundleSource(file = 'plotagents.js') {
|
|
256
329
|
try {
|
|
@@ -263,9 +336,14 @@ export function bundleSource(file = 'plotagents.js') {
|
|
|
263
336
|
}
|
|
264
337
|
|
|
265
338
|
_bind(plot, resolve);
|
|
339
|
+
_bindDashboard({ plot, resolve, lint, bundleSource, resolveDefaults: withDefaults });
|
|
266
340
|
_setProbe((s) => plot(s));
|
|
267
341
|
export { render };
|
|
268
342
|
|
|
269
|
-
export {
|
|
343
|
+
export { countWords, dashboardSchema, detectLocale, correlations };
|
|
344
|
+
export { resolveColorScale, colorScaleNames, interpolateColor, COLORMAPS, QUALITATIVE };
|
|
345
|
+
export { dashboard, renderDashboard, planDashboard, lintDashboard, safeDashboard, mountDashboard, packRows, balancedRows };
|
|
346
|
+
export { RENDERERS, ALIASES, registerGeo, getGeo, registeredGeo, toFeatureCollection, loadGeo };
|
|
347
|
+
export { registerTheme, withDefaults as resolveDefaults, embedTiles, TILE_PROVIDERS };
|
|
270
348
|
export { lint, schema, toolDefinition, themes, palettes, ramps, validatePalette, getTheme, themeFor, attach, PlotError, TYPES, squarify, makeFormat, formatNumber, formatCompact };
|
|
271
349
|
export default plot;
|
package/src/infer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Elige el tipo de gráfico cuando el agente no lo indica (type: "auto").
|
|
2
2
|
import { fieldsOf, fieldKind } from './data.js';
|
|
3
3
|
|
|
4
|
-
export const TYPES = ['bar', 'column', 'count', 'line', 'area', 'scatter', 'bubble', 'treemap', 'sunburst', 'donut', 'pie', 'heatmap', 'sankey', 'kpi', 'hist', 'kde', 'ecdf', 'regplot', 'residplot', 'jointplot', 'box', 'violin', 'strip', 'swarm', 'point', 'radar', 'network', 'chord', 'pyramid', 'flowchart', 'choropleth', 'symbolmap'];
|
|
4
|
+
export const TYPES = ['bar', 'column', 'count', 'line', 'area', 'scatter', 'bubble', 'treemap', 'sunburst', 'donut', 'pie', 'heatmap', 'sankey', 'kpi', 'hist', 'kde', 'ecdf', 'regplot', 'residplot', 'jointplot', 'box', 'violin', 'strip', 'swarm', 'point', 'radar', 'network', 'chord', 'pyramid', 'flowchart', 'gantt', 'wordcloud', 'choropleth', 'symbolmap', 'pointmap', 'table', 'corr', 'pairplot'];
|
|
5
5
|
|
|
6
6
|
// Nombres de seaborn, d3plus y otros: el agente puede usar el que conozca.
|
|
7
7
|
export const ALIASES = {
|
|
@@ -9,10 +9,18 @@ export const ALIASES = {
|
|
|
9
9
|
scatterplot: 'scatter', histplot: 'hist', histogram: 'hist', kdeplot: 'kde', density: 'kde', ecdfplot: 'ecdf',
|
|
10
10
|
lmplot: 'regplot', reg: 'regplot', regression: 'regplot', joint: 'jointplot', boxplot: 'box', violinplot: 'violin',
|
|
11
11
|
stripplot: 'strip', swarmplot: 'swarm', beeswarm: 'swarm', pointplot: 'point', spider: 'radar', doughnut: 'donut',
|
|
12
|
+
pointmap: 'pointmap', dotmap: 'pointmap', 'dot-map': 'pointmap', tilemap: 'pointmap', basemap: 'pointmap', 'mapa-puntos': 'pointmap', 'mapa-de-puntos': 'pointmap', latlon: 'pointmap',
|
|
12
13
|
graph: 'network', force: 'network', red: 'network', 'chord-diagram': 'chord', cuerdas: 'chord',
|
|
13
14
|
populationpyramid: 'pyramid', population: 'pyramid', piramide: 'pyramid', 'pirámide': 'pyramid',
|
|
14
15
|
flow: 'flowchart', diagram: 'flowchart', process: 'flowchart', diagrama: 'flowchart',
|
|
15
16
|
map: 'choropleth', mapa: 'choropleth', choroplethmap: 'choropleth', symbol: 'symbolmap', bubblemap: 'symbolmap', 'mapa-simbolos': 'symbolmap',
|
|
17
|
+
words: 'wordcloud', 'word-cloud': 'wordcloud', wordclouds: 'wordcloud', tagcloud: 'wordcloud', 'tag-cloud': 'wordcloud', nube: 'wordcloud', 'nube-de-palabras': 'wordcloud', cloud: 'wordcloud',
|
|
18
|
+
timeline: 'gantt', roadmap: 'gantt', schedule: 'gantt', cronograma: 'gantt', 'carta-gantt': 'gantt', ganttchart: 'gantt', 'gantt-chart': 'gantt', planning: 'gantt',
|
|
19
|
+
tabla: 'table', tables: 'table', datatable: 'table', 'data-table': 'table', grid: 'table', tableau: 'table', tabelle: 'table', tabela: 'table', spreadsheet: 'table', planilla: 'table',
|
|
20
|
+
correlation: 'corr', correlations: 'corr', corrplot: 'corr', corrmatrix: 'corr', 'corr-matrix': 'corr', 'correlation-matrix': 'corr', correlogram: 'corr', correlograma: 'corr',
|
|
21
|
+
correlacion: 'corr', 'correlación': 'corr', 'matriz-correlacion': 'corr', 'matriz-de-correlacion': 'corr', 'matriz-de-correlación': 'corr',
|
|
22
|
+
clustermap: ['corr', { cluster: true }], 'cluster-map': ['corr', { cluster: true }],
|
|
23
|
+
pairs: 'pairplot', pairgrid: 'pairplot', splom: 'pairplot', scattermatrix: 'pairplot', 'scatter-matrix': 'pairplot', 'scatterplot-matrix': 'pairplot', 'matriz-de-dispersion': 'pairplot', 'matriz-de-dispersión': 'pairplot',
|
|
16
24
|
tree: 'treemap', stat: 'kpi', stats: 'kpi', number: 'kpi', heat: 'heatmap', flow: 'sankey',
|
|
17
25
|
};
|
|
18
26
|
|
|
@@ -26,16 +34,25 @@ export function normalizeType(t) {
|
|
|
26
34
|
|
|
27
35
|
export function inferType(spec, data = []) {
|
|
28
36
|
if (spec.items) return 'kpi';
|
|
37
|
+
if (Array.isArray(spec.columns) || spec.pageSize !== undefined) return 'table';
|
|
29
38
|
if (spec.geo) return 'choropleth';
|
|
39
|
+
if (spec.lat || spec.lon || spec.tiles !== undefined) return 'pointmap';
|
|
40
|
+
{ const f = fieldsOf(data); if (f.some((k) => /^(lat|latitude|latitud)$/i.test(k)) && f.some((k) => /^(lon|lng|long|longitude|longitud)$/i.test(k))) return 'pointmap'; }
|
|
41
|
+
if ((typeof spec.text === 'string' && !(data[0] && spec.text in data[0])) || (Array.isArray(spec.text) && spec.text.length && typeof spec.text[0] === 'string') || spec.textField) return 'wordcloud';
|
|
42
|
+
if (spec.start !== undefined && (spec.end !== undefined || spec.duration !== undefined) && typeof spec.start === 'string') return 'gantt';
|
|
30
43
|
if (spec.nodes && spec.nodes.some?.((n) => n && n.type)) return 'flowchart';
|
|
31
44
|
if (spec.nodes) return 'network';
|
|
32
45
|
if (spec.matrix) return 'chord';
|
|
33
46
|
if (spec.from || spec.to) return 'sankey';
|
|
34
47
|
if (spec.path || spec.group) return 'treemap';
|
|
48
|
+
if (spec.triangle !== undefined || spec.cluster !== undefined || spec.method !== undefined) return 'corr';
|
|
49
|
+
if (spec.diag !== undefined || spec.corner !== undefined || spec.upper !== undefined) return 'pairplot';
|
|
35
50
|
if (spec.size) return 'bubble';
|
|
36
51
|
const fields = fieldsOf(data);
|
|
37
52
|
if ((fields.includes('source') && fields.includes('target')) || (fields.includes('from') && fields.includes('to'))) return 'sankey';
|
|
38
53
|
const kinds = Object.fromEntries(fields.map((f) => [f, fieldKind(data, f)]));
|
|
54
|
+
// Dos fechas por fila (inicio y fin) y un nombre: un cronograma.
|
|
55
|
+
if (fields.filter((f) => kinds[f] === 'date').length >= 2 && fields.some((f) => kinds[f] === 'category') && !spec.y && !spec.series) return 'gantt';
|
|
39
56
|
const x = spec.x ?? fields[0];
|
|
40
57
|
const xk = kinds[x];
|
|
41
58
|
if (xk === 'date' || xk === 'year') return 'line';
|