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/interact.js
CHANGED
|
@@ -12,12 +12,18 @@ export function attach(root) {
|
|
|
12
12
|
const dark = svg.getAttribute('data-pa-mode') === 'dark';
|
|
13
13
|
const tipEl = document.createElement('div');
|
|
14
14
|
tipEl.setAttribute('role', 'tooltip');
|
|
15
|
+
tipEl.id = 'pa-tip-' + Math.random().toString(36).slice(2, 8);
|
|
16
|
+
// Tooltip como tarjeta: fondo del tema (o `theme.tooltip`), cabecera del color de la marca.
|
|
17
|
+
const custom = (svg.getAttribute('data-pa-tip') || '').split(',');
|
|
18
|
+
const tipBg = custom[0] || (dark ? '#151A3D' : '#FFFFFF');
|
|
19
|
+
const tipInk = custom[1] || (dark ? '#F2F3FA' : '#141A3F');
|
|
15
20
|
tipEl.style.cssText = [
|
|
16
21
|
'position:absolute', 'pointer-events:none', 'z-index:10', 'opacity:0', 'transition:opacity .12s',
|
|
17
22
|
'padding:8px 10px', 'border-radius:8px', 'font:12.5px/1.45 ' + (svg.getAttribute('font-family') || 'system-ui'),
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
'
|
|
23
|
+
'width:max-content', 'max-width:280px', 'overflow:hidden', 'overflow-wrap:anywhere', 'font-variant-numeric:tabular-nums',
|
|
24
|
+
`background:${tipBg};color:${tipInk}`,
|
|
25
|
+
dark ? 'border:1px solid rgba(242,243,250,.14)' : 'border:1px solid rgba(20,26,63,.10)',
|
|
26
|
+
'box-shadow:0 8px 28px rgba(11,15,43,' + (dark ? '.45' : '.16') + ')',
|
|
21
27
|
].join(';');
|
|
22
28
|
root.appendChild(tipEl);
|
|
23
29
|
const NS = 'http://www.w3.org/2000/svg';
|
|
@@ -27,25 +33,77 @@ export function attach(root) {
|
|
|
27
33
|
let on = null;
|
|
28
34
|
let pinned = null;
|
|
29
35
|
|
|
30
|
-
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ '&': '&', '<': '<', '>': '>' })[c]);
|
|
31
|
-
|
|
36
|
+
const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
37
|
+
// Colores que llegan desde atributos de datos: solo hex o rgb() (nada que rompa el HTML).
|
|
38
|
+
const col = (c) => (/^(#[0-9a-f]{3,8}|rgba?\([\d\s.,%]+\))$/i.test(String(c || '').trim()) ? String(c).trim() : '#888888');
|
|
39
|
+
// Posición: junto a la marca (no al cursor), nunca sobre el título ni la
|
|
40
|
+
// bajada, y dentro del gráfico. Prueba arriba, derecha, izquierda y abajo
|
|
41
|
+
// (en barras horizontales, derecha primero) y conserva el lado anterior
|
|
42
|
+
// mientras siga sirviendo, para que no salte.
|
|
43
|
+
let lastSide = null;
|
|
44
|
+
const headerBottom = (r) => {
|
|
45
|
+
const t = +svg.getAttribute('data-pa-top');
|
|
46
|
+
const m = svg.getScreenCTM && svg.getScreenCTM();
|
|
47
|
+
return t && m ? t * m.d + m.f - r.top : 0;
|
|
48
|
+
};
|
|
49
|
+
const show = (html, ev, anchor, order) => {
|
|
32
50
|
tipEl.innerHTML = html;
|
|
33
51
|
tipEl.style.opacity = '1';
|
|
34
52
|
const r = root.getBoundingClientRect();
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (
|
|
53
|
+
const w = tipEl.offsetWidth, h = tipEl.offsetHeight, G = 10, M = 4;
|
|
54
|
+
const cx = ev.clientX - r.left, cy = ev.clientY - r.top;
|
|
55
|
+
let a = null;
|
|
56
|
+
if (anchor && anchor.l !== undefined) a = anchor;
|
|
57
|
+
else if (anchor && anchor.getBoundingClientRect) {
|
|
58
|
+
const b = anchor.getBoundingClientRect(), sb = svg.getBoundingClientRect();
|
|
59
|
+
// Marcas grandes (celdas de treemap, regiones, sectores): mejor el puntero.
|
|
60
|
+
if (b.width > 0 && b.width * b.height < 0.2 * sb.width * sb.height) a = { l: b.left - r.left, r: b.right - r.left, t: b.top - r.top, b: b.bottom - r.top };
|
|
61
|
+
}
|
|
62
|
+
if (!a) a = { l: cx - 8, r: cx + 8, t: cy - 8, b: cy + 8 };
|
|
63
|
+
const minY = Math.min(Math.max(M, headerBottom(r) + M), Math.max(M, r.height - h - M));
|
|
64
|
+
const maxY = Math.max(minY, r.height - h - M), maxX = Math.max(M, r.width - w - M);
|
|
65
|
+
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
66
|
+
const hx = clamp(cx, a.l, a.r) - w / 2, vy = clamp(cy, a.t, a.b) - h / 2;
|
|
67
|
+
const C = {
|
|
68
|
+
top: [clamp(hx, M, maxX), a.t - G - h, 'y'],
|
|
69
|
+
bottom: [clamp(hx, M, maxX), a.b + G, 'y'],
|
|
70
|
+
right: [a.r + G, clamp(vy, minY, maxY), 'x'],
|
|
71
|
+
left: [a.l - G - w, clamp(vy, minY, maxY), 'x'],
|
|
72
|
+
};
|
|
73
|
+
const ok = ([x, y]) => x >= M - 0.5 && x <= maxX + 0.5 && y >= minY - 0.5 && y <= maxY + 0.5;
|
|
74
|
+
const sides = order || (a.r - a.l > a.b - a.t && a.r - a.l > 40 ? ['right', 'top', 'left', 'bottom'] : ['top', 'right', 'left', 'bottom']);
|
|
75
|
+
const pick = [lastSide, ...sides].find((sd) => sd && C[sd] && ok(C[sd]));
|
|
76
|
+
let x, y;
|
|
77
|
+
if (pick) { [x, y] = C[pick]; lastSide = pick; }
|
|
78
|
+
else { x = clamp(cx + 14, M, maxX); y = clamp(cy + 14, minY, maxY); lastSide = null; }
|
|
39
79
|
tipEl.style.left = x + 'px';
|
|
40
80
|
tipEl.style.top = y + 'px';
|
|
41
81
|
};
|
|
42
82
|
const type = svg.getAttribute('data-pa-type') || '';
|
|
43
|
-
const isMap = type === 'choropleth' || type === 'symbolmap';
|
|
83
|
+
const isMap = type === 'choropleth' || type === 'symbolmap' || type === 'pointmap';
|
|
44
84
|
const zoomG = svg.querySelector('.pa-zoom');
|
|
45
85
|
const mapOverlay = document.createElementNS(NS, 'g');
|
|
46
86
|
mapOverlay.setAttribute('pointer-events', 'none');
|
|
47
87
|
if (zoomG) zoomG.appendChild(mapOverlay);
|
|
88
|
+
// Red: al pasar sobre un nodo se encienden sus vecinos y sus aristas (como
|
|
89
|
+
// emphasis.focus: 'adjacency' de ECharts); el resto se apaga.
|
|
90
|
+
let extraOn = [];
|
|
91
|
+
const adjacency = (node) => {
|
|
92
|
+
extraOn.forEach((n) => n.classList.remove('pa-on'));
|
|
93
|
+
extraOn = [];
|
|
94
|
+
const id = node && node.getAttribute && node.getAttribute('data-node');
|
|
95
|
+
const net = id !== null && id !== undefined && node.closest && node.closest('.pa-net');
|
|
96
|
+
if (!net) return;
|
|
97
|
+
const near = new Set([id]);
|
|
98
|
+
for (const e of net.querySelectorAll('.pa-links [data-s]')) {
|
|
99
|
+
const a = e.getAttribute('data-s'), b = e.getAttribute('data-t');
|
|
100
|
+
if (a === id || b === id) { near.add(a); near.add(b); e.classList.add('pa-on'); extraOn.push(e); }
|
|
101
|
+
}
|
|
102
|
+
for (const c of net.querySelectorAll('.pa-nodes [data-node]')) if (near.has(c.getAttribute('data-node')) && c !== node) { c.classList.add('pa-on'); extraOn.push(c); }
|
|
103
|
+
};
|
|
48
104
|
const hide = () => {
|
|
105
|
+
extraOn.forEach((n) => n.classList.remove('pa-on'));
|
|
106
|
+
extraOn = [];
|
|
49
107
|
tipEl.style.opacity = '0';
|
|
50
108
|
if (on) on.classList.remove('pa-on');
|
|
51
109
|
on = null;
|
|
@@ -53,19 +111,27 @@ export function attach(root) {
|
|
|
53
111
|
mapOverlay.innerHTML = '';
|
|
54
112
|
if (!pinned) svg.classList.remove('pa-focus');
|
|
55
113
|
};
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
const
|
|
59
|
-
const rgb = (h) => { h = String(h).replace('#', ''); if (h.length === 3) h = h.split('').map((c) => c + c).join(''); const n = parseInt(h, 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; };
|
|
114
|
+
// Cabecera del tooltip con el color de la marca de fondo y texto blanco u
|
|
115
|
+
// oscuro según el contraste (≥ 4,5:1; si ninguno llega, se ajusta el fondo).
|
|
116
|
+
const rgb = (h) => { h = String(h).replace('#', ''); if (h.length === 3) h = h.split('').map((c) => c + c).join(''); const n = parseInt(h.slice(0, 6), 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; };
|
|
60
117
|
const lum = (c) => { const [r, g, b] = rgb(c).map((v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
|
|
61
118
|
const ratio = (a, b) => { const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
|
|
62
119
|
const hex = (a) => '#' + a.map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
120
|
+
const toHex = (c) => {
|
|
121
|
+
c = String(c || '').trim();
|
|
122
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(c)) return c;
|
|
123
|
+
const m = /^rgba?\((\d+)[,\s]+(\d+)[,\s]+(\d+)/i.exec(c);
|
|
124
|
+
return m ? hex([+m[1], +m[2], +m[3]]) : null;
|
|
125
|
+
};
|
|
126
|
+
const headerInk = (c) => {
|
|
127
|
+
let bg = toHex(c);
|
|
128
|
+
if (!bg) return null;
|
|
129
|
+
const DARK = '#141A3F', LIGHT = '#FFFFFF';
|
|
130
|
+
const fg = ratio(LIGHT, bg) >= ratio(DARK, bg) ? LIGHT : DARK;
|
|
131
|
+
const to = fg === LIGHT ? [0, 0, 0] : [255, 255, 255];
|
|
132
|
+
const A = rgb(bg);
|
|
133
|
+
for (let t = 0.05; ratio(fg, bg) < 4.5 && t <= 1.001; t += 0.05) bg = hex(A.map((v, i) => v + (to[i] - v) * t));
|
|
134
|
+
return { bg, fg };
|
|
69
135
|
};
|
|
70
136
|
const markColor = (node) => {
|
|
71
137
|
if (!node) return null;
|
|
@@ -83,10 +149,17 @@ export function attach(root) {
|
|
|
83
149
|
return null;
|
|
84
150
|
};
|
|
85
151
|
const linesHtml = (txt, color) => {
|
|
86
|
-
const
|
|
87
|
-
return String(txt).split('\n').map((l, i) =>
|
|
88
|
-
|
|
89
|
-
|
|
152
|
+
const hd = headerInk(color);
|
|
153
|
+
return String(txt).split('\n').map((l, i) => {
|
|
154
|
+
if (i === 0) return hd
|
|
155
|
+
? `<div style="margin:-8px -10px 6px;padding:6px 10px;background:${hd.bg};color:${hd.fg};font-weight:600">${esc(l)}</div>`
|
|
156
|
+
: `<div style="font-weight:600;margin-bottom:2px">${esc(l)}</div>`;
|
|
157
|
+
// "Etiqueta: valor" → etiqueta a la izquierda y valor en negrita a la derecha.
|
|
158
|
+
const m = /^(.{1,40}?):\s+(.+)$/.exec(l);
|
|
159
|
+
return m
|
|
160
|
+
? `<div style="display:flex;gap:12px;justify-content:space-between"><span style="opacity:.78">${esc(m[1])}</span><b style="font-weight:600">${esc(m[2])}</b></div>`
|
|
161
|
+
: `<div style="opacity:.86">${esc(l)}</div>`;
|
|
162
|
+
}).join('');
|
|
90
163
|
};
|
|
91
164
|
|
|
92
165
|
const toSvg = (ev) => {
|
|
@@ -105,10 +178,12 @@ export function attach(root) {
|
|
|
105
178
|
for (const d of data) if (Math.abs(d.x - p.x) < Math.abs(best.x - p.x)) best = d;
|
|
106
179
|
const y0 = +t.getAttribute('data-y0'), y1 = +t.getAttribute('data-y1');
|
|
107
180
|
overlay.innerHTML =
|
|
108
|
-
`<line x1="${best.x}" x2="${best.x}" y1="${y0}" y2="${y1}" stroke="${dark ? '#8A90B4' : '#7C819C'}" stroke-width="1"/>` +
|
|
109
|
-
best.v.map((v) => `<circle cx="${best.x}" cy="${v[2]}" r="4.5" fill="${v[1]}" stroke="${dark ? '#0B0F2B' : '#FCFBF9'}" stroke-width="2"/>`).join('');
|
|
110
|
-
const rows = [...best.v].sort((a, b) => a[2] - b[2]).map((v) => `<div style="display:flex;gap:8px;align-items:center"><span style="width:8px;height:8px;border-radius:50%;background:${v[1]}"></span><span style="opacity:.86;flex:1">${esc(v[0])}</span><b style="font-weight:600;margin-left:12px">${esc(v[3])}</b></div>`).join('');
|
|
111
|
-
|
|
181
|
+
`<line x1="${+best.x}" x2="${+best.x}" y1="${y0}" y2="${y1}" stroke="${dark ? '#8A90B4' : '#7C819C'}" stroke-width="1"/>` +
|
|
182
|
+
best.v.map((v) => `<circle cx="${+best.x}" cy="${+v[2]}" r="4.5" fill="${col(v[1])}" stroke="${dark ? '#0B0F2B' : '#FCFBF9'}" stroke-width="2"/>`).join('');
|
|
183
|
+
const rows = [...best.v].sort((a, b) => a[2] - b[2]).map((v) => `<div style="display:flex;gap:8px;align-items:center"><span style="width:8px;height:8px;border-radius:50%;background:${col(v[1])}"></span><span style="opacity:.86;flex:1">${esc(v[0])}</span><b style="font-weight:600;margin-left:12px">${esc(v[3])}</b></div>`).join('');
|
|
184
|
+
const m = svg.getScreenCTM(), rr = root.getBoundingClientRect();
|
|
185
|
+
const lx = m ? best.x * m.a + m.e - rr.left : ev.clientX - rr.left, ly = ev.clientY - rr.top;
|
|
186
|
+
show(`<div style="font-weight:600;margin-bottom:4px">${esc(best.t)}</div>${rows}`, ev, { l: lx - 6, r: lx + 6, t: ly - 4, b: ly + 4 }, ['right', 'left', 'top', 'bottom']);
|
|
112
187
|
return;
|
|
113
188
|
}
|
|
114
189
|
if (t && t.classList && t.classList.contains('pa-near')) {
|
|
@@ -118,8 +193,8 @@ export function attach(root) {
|
|
|
118
193
|
let best = null, bd = 196;
|
|
119
194
|
for (const q of pts) { const d = (q[0] - p.x) ** 2 + (q[1] - p.y) ** 2; if (d < bd) { bd = d; best = q; } }
|
|
120
195
|
if (!best) { hide(); return; }
|
|
121
|
-
overlay.innerHTML = `<circle cx="${best[0]}" cy="${best[1]}" r="6" fill="${best[3]}" stroke="${dark ? '#0B0F2B' : '#FCFBF9'}" stroke-width="2"/>`;
|
|
122
|
-
if (best[2]) show(linesHtml(best[2], best[3]), ev); else tipEl.style.opacity = '0';
|
|
196
|
+
overlay.innerHTML = `<circle cx="${+best[0]}" cy="${+best[1]}" r="6" fill="${col(best[3])}" stroke="${dark ? '#0B0F2B' : '#FCFBF9'}" stroke-width="2"/>`;
|
|
197
|
+
if (best[2]) { const m = svg.getScreenCTM(), rr = root.getBoundingClientRect(); const px = best[0] * m.a + m.e - rr.left, py = best[1] * m.d + m.f - rr.top; show(linesHtml(best[2], best[3]), ev, { l: px - 6, r: px + 6, t: py - 6, b: py + 6 }); } else tipEl.style.opacity = '0';
|
|
123
198
|
return;
|
|
124
199
|
}
|
|
125
200
|
const tg = t && t.closest ? t.closest('[data-tip]') : null;
|
|
@@ -131,13 +206,55 @@ export function attach(root) {
|
|
|
131
206
|
if (isMap) {
|
|
132
207
|
// Mapas: sin atenuar el resto; un contorno marcado sobre la región (o su círculo).
|
|
133
208
|
mapOverlay.innerHTML = '';
|
|
134
|
-
if (on && on.tagName === 'path') mapOverlay.innerHTML = `<path d="${on.getAttribute('d')}" fill="none" stroke="${dark ? '#F2F3FA' : '#141A3F'}" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>`;
|
|
209
|
+
if (on && on.tagName === 'path') mapOverlay.innerHTML = `<path d="${esc(on.getAttribute('d'))}" fill="none" stroke="${dark ? '#F2F3FA' : '#141A3F'}" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke"/>`;
|
|
135
210
|
else if (on && on.tagName === 'circle') mapOverlay.innerHTML = `<circle cx="${on.getAttribute('cx')}" cy="${on.getAttribute('cy')}" r="${on.getAttribute('r')}" fill="none" stroke="${dark ? '#F2F3FA' : '#141A3F'}" stroke-width="2" vector-effect="non-scaling-stroke"/>`;
|
|
136
|
-
} else if (on) { on.classList.add('pa-on'); svg.classList.add('pa-focus'); } else svg.classList.remove('pa-focus');
|
|
211
|
+
} else if (on) { on.classList.add('pa-on'); svg.classList.add('pa-focus'); adjacency(on); } else svg.classList.remove('pa-focus');
|
|
137
212
|
}
|
|
138
|
-
show(linesHtml(tg.getAttribute('data-tip'), markColor(mark || tg)), ev);
|
|
213
|
+
show(linesHtml(tg.getAttribute('data-tip'), markColor(mark || tg)), ev, mark || tg);
|
|
139
214
|
});
|
|
140
|
-
|
|
215
|
+
// Táctil: un toque muestra el tooltip y lo deja abierto hasta otro toque fuera.
|
|
216
|
+
let touchTip = false;
|
|
217
|
+
svg.addEventListener('pointerdown', (ev) => {
|
|
218
|
+
if (ev.pointerType !== 'touch') return;
|
|
219
|
+
const tg = ev.target && ev.target.closest ? ev.target.closest('[data-tip]') : null;
|
|
220
|
+
if (!tg) { touchTip = false; hide(); return; }
|
|
221
|
+
touchTip = true;
|
|
222
|
+
const mark = tg.classList.contains('pa-m') ? tg : null;
|
|
223
|
+
show(linesHtml(tg.getAttribute('data-tip'), markColor(mark || tg)), ev, mark || tg);
|
|
224
|
+
});
|
|
225
|
+
svg.addEventListener('pointerleave', (ev) => { if (ev.pointerType === 'touch' && touchTip) return; hide(); });
|
|
226
|
+
|
|
227
|
+
// Teclado: Tab entra al gráfico; flechas recorren las marcas con datos,
|
|
228
|
+
// Home/End van al extremo y Esc cierra. El tooltip se ancla a la marca.
|
|
229
|
+
const lang = svg.getAttribute('data-pa-lang') === 'en';
|
|
230
|
+
svg.setAttribute('role', 'graphics-document');
|
|
231
|
+
svg.setAttribute('aria-roledescription', lang ? 'chart' : 'gráfico');
|
|
232
|
+
if (!svg.hasAttribute('tabindex')) svg.setAttribute('tabindex', '0');
|
|
233
|
+
const live = document.createElement('div');
|
|
234
|
+
live.setAttribute('aria-live', 'polite');
|
|
235
|
+
live.style.cssText = 'position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap';
|
|
236
|
+
root.appendChild(live);
|
|
237
|
+
let kIdx = -1;
|
|
238
|
+
const kMarks = () => [...svg.querySelectorAll('.pa-body [data-tip]')].filter((n) => !n.parentElement.closest('[data-tip]') && n.getAttribute('data-tip'));
|
|
239
|
+
svg.addEventListener('keydown', (ev) => {
|
|
240
|
+
if (ev.target !== svg) return;
|
|
241
|
+
if (ev.key === 'Escape') { kIdx = -1; hide(); return; }
|
|
242
|
+
const d = ev.key === 'ArrowRight' || ev.key === 'ArrowDown' ? 1 : ev.key === 'ArrowLeft' || ev.key === 'ArrowUp' ? -1 : ev.key === 'Home' ? -1e9 : ev.key === 'End' ? 1e9 : 0;
|
|
243
|
+
if (!d) return;
|
|
244
|
+
const ms = kMarks();
|
|
245
|
+
if (!ms.length) return;
|
|
246
|
+
ev.preventDefault();
|
|
247
|
+
kIdx = Math.max(0, Math.min(ms.length - 1, kIdx < 0 ? (d > 0 ? 0 : ms.length - 1) : kIdx + d));
|
|
248
|
+
const tg = ms[kIdx];
|
|
249
|
+
const b = tg.getBoundingClientRect();
|
|
250
|
+
const mark = tg.classList.contains('pa-m') ? tg : tg.querySelector('.pa-m');
|
|
251
|
+
if (on) on.classList.remove('pa-on');
|
|
252
|
+
on = mark;
|
|
253
|
+
if (!isMap && on) { on.classList.add('pa-on'); svg.classList.add('pa-focus'); }
|
|
254
|
+
show(linesHtml(tg.getAttribute('data-tip'), markColor(mark || tg)), { clientX: b.left + b.width / 2, clientY: b.top + b.height / 2 }, tg);
|
|
255
|
+
live.textContent = tg.getAttribute('data-tip').replace(/\n/g, '. ');
|
|
256
|
+
});
|
|
257
|
+
svg.addEventListener('blur', () => { kIdx = -1; hide(); });
|
|
141
258
|
|
|
142
259
|
// Leyenda conectada: pasar sobre una clave destaca sus marcas (mismo color);
|
|
143
260
|
// un clic la deja fija, otro clic la suelta.
|
|
@@ -173,9 +290,13 @@ export function attach(root) {
|
|
|
173
290
|
k.setAttribute('role', 'button');
|
|
174
291
|
k.addEventListener('pointerenter', () => { if (!pinned) focusColor(k.getAttribute('data-color')); });
|
|
175
292
|
k.addEventListener('pointerleave', () => { if (!pinned) focusColor(null); });
|
|
176
|
-
const toggle = () => {
|
|
293
|
+
const toggle = (e) => {
|
|
294
|
+
// render() puede tomar el clic (zoom en treemap, shift + clic = excluir).
|
|
295
|
+
if (root.__paLegend && root.__paLegend(k, e || {})) return;
|
|
296
|
+
pinned = pinned === k ? null : k; focusColor(pinned ? k.getAttribute('data-color') : null);
|
|
297
|
+
};
|
|
177
298
|
k.addEventListener('click', toggle);
|
|
178
|
-
k.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });
|
|
299
|
+
k.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); } });
|
|
179
300
|
}
|
|
180
301
|
|
|
181
302
|
if (zoomG) mapZoom(root, svg, zoomG, { dark, hide });
|