@opendata-ai/openchart-vanilla 8.0.0-rc.2 → 8.0.0-rc.21
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/dist/{chunk-W34KN7FD.js → chunk-2NAQRP66.js} +2 -2
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-5EOV5JQZ.js → chunk-EKAUQAAX.js} +50 -46
- package/dist/chunk-EKAUQAAX.js.map +1 -0
- package/dist/{chunk-ENXSUXC5.js → chunk-KNT5AZ2O.js} +7309 -5490
- package/dist/chunk-KNT5AZ2O.js.map +1 -0
- package/dist/chunk-RM5XS6NR.js +113 -0
- package/dist/chunk-RM5XS6NR.js.map +1 -0
- package/dist/chunk-T32UHMZA.js +282 -0
- package/dist/chunk-T32UHMZA.js.map +1 -0
- package/dist/export-gif.d.ts +113 -0
- package/dist/export-gif.js +21 -0
- package/dist/export-gif.js.map +1 -0
- package/dist/index.d.ts +283 -24
- package/dist/index.js +2012 -763
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-Dvv8NYIv.d.ts} +117 -3
- package/dist/simulation-worker.js +83 -2
- package/dist/static.js +7 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +3 -2
- package/dist/story/index.js +5 -3
- package/dist/story/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +14 -6
- package/dist/chunk-5EOV5JQZ.js.map +0 -1
- package/dist/chunk-ENXSUXC5.js.map +0 -1
- package/src/__test-fixtures__/dom.ts +0 -70
- package/src/__test-fixtures__/specs.ts +0 -214
- package/src/__tests__/animation.test.ts +0 -358
- package/src/__tests__/annotations-render.test.ts +0 -201
- package/src/__tests__/compound-labels.test.ts +0 -122
- package/src/__tests__/continuous-legend.test.ts +0 -91
- package/src/__tests__/crosshair.test.ts +0 -130
- package/src/__tests__/edit-events.test.ts +0 -902
- package/src/__tests__/events.test.ts +0 -387
- package/src/__tests__/export.test.ts +0 -241
- package/src/__tests__/font-race.test.ts +0 -199
- package/src/__tests__/gradient-ids.test.ts +0 -159
- package/src/__tests__/gradient-utils.test.ts +0 -125
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -293
- package/src/__tests__/mount.test.ts +0 -357
- package/src/__tests__/pattern-fills.test.ts +0 -105
- package/src/__tests__/resize-timing.test.ts +0 -218
- package/src/__tests__/sankey.test.ts +0 -133
- package/src/__tests__/selection-events.test.ts +0 -869
- package/src/__tests__/series-search.test.ts +0 -311
- package/src/__tests__/static.test.ts +0 -141
- package/src/__tests__/svg-renderer.test.ts +0 -895
- package/src/__tests__/table-keyboard.test.ts +0 -361
- package/src/__tests__/table-mount.test.ts +0 -639
- package/src/__tests__/tilemap.test.ts +0 -158
- package/src/__tests__/tooltip.test.ts +0 -337
- package/src/__tests__/transition.test.ts +0 -1777
- package/src/__tests__/you-draw-it.test.ts +0 -273
- package/src/animation-vars.ts +0 -42
- package/src/animation.ts +0 -76
- package/src/barlist-mount.ts +0 -383
- package/src/barlist-renderer.ts +0 -267
- package/src/dom-helpers.ts +0 -17
- package/src/export.ts +0 -482
- package/src/gradient-utils.ts +0 -138
- package/src/graph/__tests__/canvas-renderer.test.ts +0 -713
- package/src/graph/__tests__/graph-mount.test.ts +0 -213
- package/src/graph/__tests__/interaction.test.ts +0 -205
- package/src/graph/__tests__/keyboard.test.ts +0 -653
- package/src/graph/__tests__/search.test.ts +0 -88
- package/src/graph/__tests__/simulation.test.ts +0 -233
- package/src/graph/__tests__/spatial-index.test.ts +0 -142
- package/src/graph/__tests__/zoom.test.ts +0 -203
- package/src/graph/canvas-renderer.ts +0 -747
- package/src/graph/interaction.ts +0 -367
- package/src/graph/keyboard.ts +0 -208
- package/src/graph/search.ts +0 -50
- package/src/graph/simulation-worker-url.ts +0 -12
- package/src/graph/simulation-worker.ts +0 -278
- package/src/graph/simulation.ts +0 -422
- package/src/graph/spatial-index.ts +0 -121
- package/src/graph/types.ts +0 -47
- package/src/graph/worker-protocol.ts +0 -70
- package/src/graph/zoom.ts +0 -134
- package/src/graph-mount.ts +0 -888
- package/src/index.ts +0 -76
- package/src/interactions/chart-events.ts +0 -139
- package/src/interactions/crosshair.ts +0 -233
- package/src/interactions/drag-handler.ts +0 -175
- package/src/interactions/editing-drags.ts +0 -515
- package/src/interactions/index.ts +0 -25
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -280
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -88
- package/src/map-mount.ts +0 -701
- package/src/map-renderer.ts +0 -338
- package/src/map-transition.ts +0 -83
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1382
- package/src/pattern-utils.ts +0 -110
- package/src/renderers/annotations.ts +0 -388
- package/src/renderers/axes.ts +0 -351
- package/src/renderers/brand.ts +0 -87
- package/src/renderers/chrome.ts +0 -196
- package/src/renderers/endpoint-labels.ts +0 -150
- package/src/renderers/legend.ts +0 -311
- package/src/renderers/marks.ts +0 -561
- package/src/renderers/metrics.ts +0 -63
- package/src/renderers/svg-dom.ts +0 -40
- package/src/renderers/table-cells.ts +0 -446
- package/src/resize-observer.ts +0 -46
- package/src/sankey-mount.ts +0 -622
- package/src/sankey-renderer.ts +0 -614
- package/src/series-search.ts +0 -392
- package/src/static.ts +0 -251
- package/src/story/__tests__/camera-math.test.ts +0 -145
- package/src/story/__tests__/create-chart-story.test.ts +0 -317
- package/src/story/__tests__/progress-math.test.ts +0 -95
- package/src/story/__tests__/scroll-driver.test.ts +0 -130
- package/src/story/__tests__/tween.test.ts +0 -159
- package/src/story/camera-math.ts +0 -125
- package/src/story/create-chart-story.ts +0 -353
- package/src/story/crossfade.ts +0 -70
- package/src/story/index.ts +0 -39
- package/src/story/progress-math.ts +0 -85
- package/src/story/resolve-camera-target.ts +0 -119
- package/src/story/scroll-driver.ts +0 -206
- package/src/story/story-camera.ts +0 -49
- package/src/story/tween.ts +0 -112
- package/src/story/types.ts +0 -76
- package/src/svg-ids.ts +0 -23
- package/src/svg-renderer.ts +0 -383
- package/src/table-keyboard.ts +0 -266
- package/src/table-mount.ts +0 -567
- package/src/table-renderer.ts +0 -414
- package/src/text-edit-overlay.ts +0 -255
- package/src/theme-tokens.ts +0 -51
- package/src/tilemap-mount.ts +0 -511
- package/src/tilemap-renderer.ts +0 -396
- package/src/tooltip.ts +0 -177
- package/src/transition.ts +0 -2816
- package/src/you-draw-it.ts +0 -480
- /package/dist/{chunk-W34KN7FD.js.map → chunk-2NAQRP66.js.map} +0 -0
package/src/tilemap-renderer.ts
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TileMap SVG renderer: converts a TileMapLayout into SVG DOM elements.
|
|
3
|
-
*
|
|
4
|
-
* Creates an <svg> with tile rectangles, state code labels, value labels,
|
|
5
|
-
* gradient legend, and chrome. All styling via inline SVG attributes from
|
|
6
|
-
* layout data. Animation is pure CSS, driven by data attributes.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
ResolvedAnimation,
|
|
11
|
-
TileMapLayout,
|
|
12
|
-
TileMapTileMark,
|
|
13
|
-
} from '@opendata-ai/openchart-core';
|
|
14
|
-
import { stampAnimationVars } from './animation-vars';
|
|
15
|
-
import { renderChromeElement } from './renderers/chrome';
|
|
16
|
-
import { renderLegend } from './renderers/legend';
|
|
17
|
-
import { nextSvgId } from './svg-ids';
|
|
18
|
-
|
|
19
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
20
|
-
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
21
|
-
const BRAND_URL = 'https://tryopendata.ai';
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Helpers
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
function createSVGElement(tag: string): SVGElement {
|
|
28
|
-
return document.createElementNS(SVG_NS, tag);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {
|
|
32
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
33
|
-
el.setAttribute(key, String(value));
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
// Chrome rendering (delegates to the shared helper for text wrapping)
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
function renderChrome(parent: SVGElement, layout: TileMapLayout): void {
|
|
42
|
-
const g = createSVGElement('g');
|
|
43
|
-
g.setAttribute('class', 'oc-chrome');
|
|
44
|
-
|
|
45
|
-
const { chrome, measureText } = layout;
|
|
46
|
-
const bottomOffset = layout.area.y + layout.area.height;
|
|
47
|
-
|
|
48
|
-
if (chrome.title) {
|
|
49
|
-
renderChromeElement(g, chrome.title, 'oc-title', 'title', measureText);
|
|
50
|
-
}
|
|
51
|
-
if (chrome.subtitle) {
|
|
52
|
-
renderChromeElement(g, chrome.subtitle, 'oc-subtitle', 'subtitle', measureText);
|
|
53
|
-
}
|
|
54
|
-
if (chrome.source) {
|
|
55
|
-
renderChromeElement(
|
|
56
|
-
g,
|
|
57
|
-
{ ...chrome.source, y: bottomOffset + chrome.source.y },
|
|
58
|
-
'oc-source',
|
|
59
|
-
'source',
|
|
60
|
-
measureText,
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
if (chrome.byline) {
|
|
64
|
-
renderChromeElement(
|
|
65
|
-
g,
|
|
66
|
-
{ ...chrome.byline, y: bottomOffset + chrome.byline.y },
|
|
67
|
-
'oc-byline',
|
|
68
|
-
'byline',
|
|
69
|
-
measureText,
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
if (chrome.footer) {
|
|
73
|
-
renderChromeElement(
|
|
74
|
-
g,
|
|
75
|
-
{ ...chrome.footer, y: bottomOffset + chrome.footer.y },
|
|
76
|
-
'oc-footer',
|
|
77
|
-
'footer',
|
|
78
|
-
measureText,
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
parent.appendChild(g);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Watermark rendering
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
function renderWatermark(parent: SVGElement, layout: TileMapLayout): void {
|
|
90
|
-
if (layout.width < 480) return; // Don't render if too narrow
|
|
91
|
-
|
|
92
|
-
const { width, height } = layout;
|
|
93
|
-
const { theme } = layout;
|
|
94
|
-
const padding = theme.spacing.padding;
|
|
95
|
-
const rightEdge = width - padding;
|
|
96
|
-
const bottomEdge = height - padding;
|
|
97
|
-
const fill = theme.colors.axis;
|
|
98
|
-
|
|
99
|
-
const a = createSVGElement('a');
|
|
100
|
-
a.setAttribute('href', BRAND_URL);
|
|
101
|
-
a.setAttributeNS(XLINK_NS, 'xlink:href', BRAND_URL);
|
|
102
|
-
a.setAttribute('target', '_blank');
|
|
103
|
-
a.setAttribute('rel', 'noopener');
|
|
104
|
-
a.setAttribute('class', 'oc-chrome-ref');
|
|
105
|
-
|
|
106
|
-
const text = createSVGElement('text');
|
|
107
|
-
setAttrs(text, {
|
|
108
|
-
x: rightEdge,
|
|
109
|
-
y: bottomEdge,
|
|
110
|
-
'dominant-baseline': 'alphabetic',
|
|
111
|
-
'text-anchor': 'end',
|
|
112
|
-
'font-family': theme.fonts.family,
|
|
113
|
-
'font-size': 12,
|
|
114
|
-
'fill-opacity': 0.55,
|
|
115
|
-
});
|
|
116
|
-
(text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', fill);
|
|
117
|
-
|
|
118
|
-
const trySpan = createSVGElement('tspan');
|
|
119
|
-
setAttrs(trySpan, { 'font-weight': 500 });
|
|
120
|
-
trySpan.textContent = 'try';
|
|
121
|
-
|
|
122
|
-
const openDataSpan = createSVGElement('tspan');
|
|
123
|
-
setAttrs(openDataSpan, { 'font-weight': 600, 'font-size': 16 });
|
|
124
|
-
openDataSpan.textContent = 'OpenData';
|
|
125
|
-
|
|
126
|
-
const aiSpan = createSVGElement('tspan');
|
|
127
|
-
setAttrs(aiSpan, { 'font-weight': 500 });
|
|
128
|
-
aiSpan.textContent = '.ai';
|
|
129
|
-
|
|
130
|
-
text.appendChild(trySpan);
|
|
131
|
-
text.appendChild(openDataSpan);
|
|
132
|
-
text.appendChild(aiSpan);
|
|
133
|
-
a.appendChild(text);
|
|
134
|
-
parent.appendChild(a);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
// Tiles rendering
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
141
|
-
function renderTiles(
|
|
142
|
-
parent: SVGElement,
|
|
143
|
-
tiles: TileMapTileMark[],
|
|
144
|
-
animation?: ResolvedAnimation,
|
|
145
|
-
): void {
|
|
146
|
-
const g = createSVGElement('g');
|
|
147
|
-
g.setAttribute('class', 'oc-tilemap-tiles');
|
|
148
|
-
g.setAttribute('role', 'list');
|
|
149
|
-
|
|
150
|
-
// Compute per-tile delays with jitter for organic feel.
|
|
151
|
-
// Base delay spreads tiles across ~800ms window, jitter adds +-40% variation
|
|
152
|
-
// so some tiles pop in clusters while others have longer gaps.
|
|
153
|
-
const tileDelays: number[] = [];
|
|
154
|
-
if (animation?.enter) {
|
|
155
|
-
const baseStagger = 800 / Math.max(tiles.length, 1);
|
|
156
|
-
let seed = 17;
|
|
157
|
-
for (let i = 0; i < tiles.length; i++) {
|
|
158
|
-
const idx = tiles[i].animationIndex ?? i;
|
|
159
|
-
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
160
|
-
const jitter = ((seed % 1000) / 1000 - 0.5) * 0.8;
|
|
161
|
-
tileDelays.push(Math.max(0, Math.round(idx * baseStagger * (1 + jitter))));
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
for (let i = 0; i < tiles.length; i++) {
|
|
166
|
-
const tile = tiles[i];
|
|
167
|
-
const tileGroup = createSVGElement('g');
|
|
168
|
-
tileGroup.setAttribute('class', 'oc-tilemap-tile');
|
|
169
|
-
tileGroup.setAttribute('data-state', tile.stateCode);
|
|
170
|
-
tileGroup.setAttribute('role', 'listitem');
|
|
171
|
-
if (tile.aria?.label) {
|
|
172
|
-
tileGroup.setAttribute('aria-label', tile.aria.label);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (animation?.enter) {
|
|
176
|
-
const idx = tile.animationIndex ?? i;
|
|
177
|
-
tileGroup.setAttribute('data-animation-index', String(idx));
|
|
178
|
-
(tileGroup as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
179
|
-
'--oc-mark-index',
|
|
180
|
-
String(idx),
|
|
181
|
-
);
|
|
182
|
-
(tileGroup as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
183
|
-
'--oc-tile-delay',
|
|
184
|
-
`${tileDelays[i]}ms`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Tile background rect
|
|
189
|
-
const rect = createSVGElement('rect');
|
|
190
|
-
setAttrs(rect, {
|
|
191
|
-
x: tile.x,
|
|
192
|
-
y: tile.y,
|
|
193
|
-
width: tile.size,
|
|
194
|
-
height: tile.size,
|
|
195
|
-
rx: tile.cornerRadius,
|
|
196
|
-
fill: tile.fill,
|
|
197
|
-
'fill-opacity': tile.fillOpacity ?? 1,
|
|
198
|
-
stroke: tile.stroke,
|
|
199
|
-
'stroke-width': tile.strokeWidth,
|
|
200
|
-
});
|
|
201
|
-
tileGroup.appendChild(rect);
|
|
202
|
-
|
|
203
|
-
// State code label
|
|
204
|
-
const codeLabel = createSVGElement('text');
|
|
205
|
-
setAttrs(codeLabel, {
|
|
206
|
-
x: tile.label.x,
|
|
207
|
-
y: tile.label.y,
|
|
208
|
-
'text-anchor': 'middle',
|
|
209
|
-
'dominant-baseline': 'central',
|
|
210
|
-
'font-family': tile.label.style.fontFamily,
|
|
211
|
-
'font-size': tile.label.style.fontSize,
|
|
212
|
-
'font-weight': tile.label.style.fontWeight,
|
|
213
|
-
});
|
|
214
|
-
(codeLabel as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
215
|
-
'fill',
|
|
216
|
-
tile.label.style.fill,
|
|
217
|
-
);
|
|
218
|
-
codeLabel.textContent = tile.label.text;
|
|
219
|
-
tileGroup.appendChild(codeLabel);
|
|
220
|
-
|
|
221
|
-
// Value label (if visible)
|
|
222
|
-
if (tile.valueLabel.visible && tile.valueLabel.text) {
|
|
223
|
-
const valueLabel = createSVGElement('text');
|
|
224
|
-
setAttrs(valueLabel, {
|
|
225
|
-
x: tile.valueLabel.x,
|
|
226
|
-
y: tile.valueLabel.y,
|
|
227
|
-
'text-anchor': 'middle',
|
|
228
|
-
'dominant-baseline': 'central',
|
|
229
|
-
'font-family': tile.valueLabel.style.fontFamily,
|
|
230
|
-
'font-size': tile.valueLabel.style.fontSize,
|
|
231
|
-
'font-weight': tile.valueLabel.style.fontWeight,
|
|
232
|
-
});
|
|
233
|
-
(valueLabel as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
234
|
-
'fill',
|
|
235
|
-
tile.valueLabel.style.fill,
|
|
236
|
-
);
|
|
237
|
-
valueLabel.textContent = tile.valueLabel.text;
|
|
238
|
-
tileGroup.appendChild(valueLabel);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
g.appendChild(tileGroup);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
parent.appendChild(g);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// Gradient legend rendering
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
function renderGradientLegend(parent: SVGElement, layout: TileMapLayout): void {
|
|
252
|
-
if (!layout.gradientLegend) return;
|
|
253
|
-
|
|
254
|
-
const { gradientLegend } = layout;
|
|
255
|
-
const g = createSVGElement('g');
|
|
256
|
-
g.setAttribute('class', 'oc-tilemap-legend');
|
|
257
|
-
|
|
258
|
-
// Build linear gradient in defs
|
|
259
|
-
const defs = parent.querySelector('defs') || createSVGElement('defs');
|
|
260
|
-
const exists = parent.querySelector('defs');
|
|
261
|
-
if (!exists) {
|
|
262
|
-
parent.insertBefore(defs, parent.firstChild);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const gradientId = nextSvgId('oc-tilemap-legend-gradient');
|
|
266
|
-
const grad = createSVGElement('linearGradient');
|
|
267
|
-
grad.id = gradientId;
|
|
268
|
-
grad.setAttribute('x1', '0%');
|
|
269
|
-
grad.setAttribute('y1', '0%');
|
|
270
|
-
grad.setAttribute('x2', '100%');
|
|
271
|
-
grad.setAttribute('y2', '0%');
|
|
272
|
-
|
|
273
|
-
for (const stop of gradientLegend.colorStops) {
|
|
274
|
-
const s = createSVGElement('stop');
|
|
275
|
-
const attrs: Record<string, string | number> = {
|
|
276
|
-
offset: `${stop.offset * 100}%`,
|
|
277
|
-
'stop-color': stop.color,
|
|
278
|
-
};
|
|
279
|
-
if (stop.opacity !== undefined) {
|
|
280
|
-
attrs['stop-opacity'] = stop.opacity;
|
|
281
|
-
}
|
|
282
|
-
setAttrs(s, attrs);
|
|
283
|
-
grad.appendChild(s);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
(defs as SVGElement).appendChild(grad);
|
|
287
|
-
|
|
288
|
-
// Gradient bar (pill-shaped)
|
|
289
|
-
const barHeight = gradientLegend.bounds.height;
|
|
290
|
-
const bar = createSVGElement('rect');
|
|
291
|
-
setAttrs(bar, {
|
|
292
|
-
x: gradientLegend.bounds.x,
|
|
293
|
-
y: gradientLegend.bounds.y,
|
|
294
|
-
width: gradientLegend.bounds.width,
|
|
295
|
-
height: barHeight,
|
|
296
|
-
rx: barHeight / 2,
|
|
297
|
-
fill: `url(#${gradientId})`,
|
|
298
|
-
});
|
|
299
|
-
g.appendChild(bar);
|
|
300
|
-
|
|
301
|
-
// Min label
|
|
302
|
-
const minText = createSVGElement('text');
|
|
303
|
-
setAttrs(minText, {
|
|
304
|
-
x: gradientLegend.bounds.x,
|
|
305
|
-
y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
|
|
306
|
-
'text-anchor': 'start',
|
|
307
|
-
'font-family': gradientLegend.labelStyle.fontFamily,
|
|
308
|
-
'font-size': gradientLegend.labelStyle.fontSize,
|
|
309
|
-
'font-weight': gradientLegend.labelStyle.fontWeight,
|
|
310
|
-
});
|
|
311
|
-
(minText as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
312
|
-
'fill',
|
|
313
|
-
gradientLegend.labelStyle.fill,
|
|
314
|
-
);
|
|
315
|
-
minText.textContent = gradientLegend.minLabel;
|
|
316
|
-
g.appendChild(minText);
|
|
317
|
-
|
|
318
|
-
// Max label
|
|
319
|
-
const maxText = createSVGElement('text');
|
|
320
|
-
setAttrs(maxText, {
|
|
321
|
-
x: gradientLegend.bounds.x + gradientLegend.bounds.width,
|
|
322
|
-
y: gradientLegend.bounds.y + gradientLegend.bounds.height + 14,
|
|
323
|
-
'text-anchor': 'end',
|
|
324
|
-
'font-family': gradientLegend.labelStyle.fontFamily,
|
|
325
|
-
'font-size': gradientLegend.labelStyle.fontSize,
|
|
326
|
-
'font-weight': gradientLegend.labelStyle.fontWeight,
|
|
327
|
-
});
|
|
328
|
-
(maxText as SVGElement & ElementCSSInlineStyle).style.setProperty(
|
|
329
|
-
'fill',
|
|
330
|
-
gradientLegend.labelStyle.fill,
|
|
331
|
-
);
|
|
332
|
-
maxText.textContent = gradientLegend.maxLabel;
|
|
333
|
-
g.appendChild(maxText);
|
|
334
|
-
|
|
335
|
-
parent.appendChild(g);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// ---------------------------------------------------------------------------
|
|
339
|
-
// Public API
|
|
340
|
-
// ---------------------------------------------------------------------------
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Render a TileMapLayout to an SVG element.
|
|
344
|
-
*/
|
|
345
|
-
export function renderTileMapSVG(
|
|
346
|
-
layout: TileMapLayout,
|
|
347
|
-
opts?: { animate?: boolean },
|
|
348
|
-
): SVGSVGElement {
|
|
349
|
-
const { width, height, tiles, a11y, watermark, animation } = layout;
|
|
350
|
-
const animate = opts?.animate && !!animation?.enter;
|
|
351
|
-
|
|
352
|
-
const svg = createSVGElement('svg') as SVGSVGElement;
|
|
353
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
354
|
-
svg.setAttribute('role', 'img');
|
|
355
|
-
if (a11y.altText) {
|
|
356
|
-
svg.setAttribute('aria-label', a11y.altText);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const classes = animate ? 'oc-tilemap oc-animate' : 'oc-tilemap';
|
|
360
|
-
svg.setAttribute('class', classes);
|
|
361
|
-
|
|
362
|
-
if (animate && animation?.enter) {
|
|
363
|
-
// Target ~1s total: stagger window ~800ms + per-tile pop ~200ms
|
|
364
|
-
const stagger = Math.max(5, Math.round(800 / Math.max(tiles.length, 1)));
|
|
365
|
-
stampAnimationVars(svg, {
|
|
366
|
-
duration: animation.enter.duration,
|
|
367
|
-
stagger,
|
|
368
|
-
annotationDelay: animation.annotationDelay,
|
|
369
|
-
ease: animation.enter.ease,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Empty defs element (will be filled by gradient legend)
|
|
374
|
-
const defs = createSVGElement('defs');
|
|
375
|
-
svg.appendChild(defs);
|
|
376
|
-
|
|
377
|
-
// Render chrome
|
|
378
|
-
renderChrome(svg, layout);
|
|
379
|
-
|
|
380
|
-
// Render tiles
|
|
381
|
-
renderTiles(svg, tiles, animate ? animation : undefined);
|
|
382
|
-
|
|
383
|
-
// Render legend (gradient for quantitative, categorical for nominal)
|
|
384
|
-
if (layout.categoricalLegend) {
|
|
385
|
-
renderLegend(svg, layout.categoricalLegend);
|
|
386
|
-
} else {
|
|
387
|
-
renderGradientLegend(svg, layout);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Render watermark
|
|
391
|
-
if (watermark) {
|
|
392
|
-
renderWatermark(svg, layout);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return svg;
|
|
396
|
-
}
|
package/src/tooltip.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tooltip manager: creates and positions a floating tooltip element.
|
|
3
|
-
*
|
|
4
|
-
* Shows tooltip content near the mouse/touch position with viewport
|
|
5
|
-
* edge avoidance via @floating-ui/dom. Touch support via tap-to-show,
|
|
6
|
-
* tap-outside-to-hide.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { Placement } from '@floating-ui/dom';
|
|
10
|
-
import { computePosition, flip, offset, shift } from '@floating-ui/dom';
|
|
11
|
-
import type { TooltipContent } from '@opendata-ai/openchart-core';
|
|
12
|
-
|
|
13
|
-
export interface TooltipShowOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Floating-ui placement. Defaults to 'bottom-start' for mark hover
|
|
16
|
-
* (the legacy behavior). Line/area snap-tooltips pass 'right-start'
|
|
17
|
-
* so the card sits beside the snapped point and flips to 'left-start'
|
|
18
|
-
* via the flip middleware when crowding the right edge.
|
|
19
|
-
*/
|
|
20
|
-
placement?: Placement;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface TooltipManager {
|
|
24
|
-
/** Show the tooltip with content at a given position. */
|
|
25
|
-
show(content: TooltipContent, x: number, y: number, opts?: TooltipShowOptions): void;
|
|
26
|
-
/** Hide the tooltip. */
|
|
27
|
-
hide(): void;
|
|
28
|
-
/** Remove the tooltip element and clean up event listeners. */
|
|
29
|
-
destroy(): void;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const TOOLTIP_OFFSET = 12;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Create a tooltip manager attached to a container element.
|
|
36
|
-
*
|
|
37
|
-
* The manager creates a floating div positioned relative to the container.
|
|
38
|
-
* Content is rendered as a title line with optional color indicator,
|
|
39
|
-
* followed by a compact list of field-value pairs.
|
|
40
|
-
*
|
|
41
|
-
* @param container - The parent element for the tooltip.
|
|
42
|
-
* @returns TooltipManager with show/hide/destroy methods.
|
|
43
|
-
*/
|
|
44
|
-
export function createTooltipManager(container: HTMLElement): TooltipManager {
|
|
45
|
-
const tooltip = document.createElement('div');
|
|
46
|
-
tooltip.className = 'oc-tooltip';
|
|
47
|
-
tooltip.setAttribute('role', 'tooltip');
|
|
48
|
-
|
|
49
|
-
container.style.position = container.style.position || 'relative';
|
|
50
|
-
container.appendChild(tooltip);
|
|
51
|
-
|
|
52
|
-
// Track last content to skip innerHTML when only position changes
|
|
53
|
-
let lastContentKey = '';
|
|
54
|
-
// Generation counter to discard stale async position callbacks
|
|
55
|
-
let currentPositionId = 0;
|
|
56
|
-
|
|
57
|
-
// Hide on tap-outside for touch devices
|
|
58
|
-
const handleDocumentTouch = (e: Event): void => {
|
|
59
|
-
if (!container.contains(e.target as Node)) {
|
|
60
|
-
hide();
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
document.addEventListener('touchstart', handleDocumentTouch);
|
|
64
|
-
|
|
65
|
-
// WCAG 1.4.13: hover-triggered content must be dismissible without moving
|
|
66
|
-
// the pointer. Document-level so it works while hovering, when the chart
|
|
67
|
-
// container does not have keyboard focus.
|
|
68
|
-
const handleDocumentKeydown = (e: KeyboardEvent): void => {
|
|
69
|
-
if (e.key === 'Escape' && tooltip.style.display === 'block') {
|
|
70
|
-
hide();
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
document.addEventListener('keydown', handleDocumentKeydown);
|
|
74
|
-
|
|
75
|
-
function show(content: TooltipContent, x: number, y: number, opts?: TooltipShowOptions): void {
|
|
76
|
-
// Fast content identity check: title + field count + first/last field values
|
|
77
|
-
const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}`;
|
|
78
|
-
|
|
79
|
-
if (contentKey !== lastContentKey) {
|
|
80
|
-
lastContentKey = contentKey;
|
|
81
|
-
|
|
82
|
-
let html = '';
|
|
83
|
-
|
|
84
|
-
// Multi-series tooltips put a swatch on each row instead of in the
|
|
85
|
-
// header, so detect that case once.
|
|
86
|
-
const colorRowCount = content.fields.filter((f) => f.color).length;
|
|
87
|
-
const perRowSwatches = colorRowCount > 1;
|
|
88
|
-
|
|
89
|
-
// Title row: header dot only when there's a single color (single-series)
|
|
90
|
-
if (content.title) {
|
|
91
|
-
const titleColor = perRowSwatches ? undefined : content.fields.find((f) => f.color)?.color;
|
|
92
|
-
html += '<div class="oc-tooltip-header">';
|
|
93
|
-
if (titleColor) {
|
|
94
|
-
html += `<span class="oc-tooltip-dot" style="background:${esc(titleColor)}"></span>`;
|
|
95
|
-
}
|
|
96
|
-
html += `<span class="oc-tooltip-title">${esc(content.title)}</span>`;
|
|
97
|
-
html += '</div>';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Field rows
|
|
101
|
-
if (content.fields.length > 0) {
|
|
102
|
-
html += '<div class="oc-tooltip-body">';
|
|
103
|
-
for (const field of content.fields) {
|
|
104
|
-
html += '<div class="oc-tooltip-row">';
|
|
105
|
-
html += '<span class="oc-tooltip-label">';
|
|
106
|
-
if (perRowSwatches && field.color) {
|
|
107
|
-
html += `<span class="oc-tooltip-row-swatch" style="background:${esc(field.color)}"></span>`;
|
|
108
|
-
}
|
|
109
|
-
html += `${esc(field.label)}</span>`;
|
|
110
|
-
html += `<span class="oc-tooltip-value">${esc(field.value)}</span>`;
|
|
111
|
-
html += '</div>';
|
|
112
|
-
}
|
|
113
|
-
html += '</div>';
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
tooltip.innerHTML = html;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
tooltip.style.display = 'block';
|
|
120
|
-
|
|
121
|
-
// Position with viewport-aware edge avoidance via @floating-ui/dom.
|
|
122
|
-
// Uses a virtual element since the reference point is a coordinate,
|
|
123
|
-
// not a DOM element.
|
|
124
|
-
const positionId = ++currentPositionId;
|
|
125
|
-
const virtualRef = {
|
|
126
|
-
getBoundingClientRect() {
|
|
127
|
-
const rect = container.getBoundingClientRect();
|
|
128
|
-
return {
|
|
129
|
-
x: rect.left + x,
|
|
130
|
-
y: rect.top + y,
|
|
131
|
-
width: 0,
|
|
132
|
-
height: 0,
|
|
133
|
-
top: rect.top + y,
|
|
134
|
-
left: rect.left + x,
|
|
135
|
-
right: rect.left + x,
|
|
136
|
-
bottom: rect.top + y,
|
|
137
|
-
};
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
computePosition(virtualRef, tooltip, {
|
|
142
|
-
placement: opts?.placement ?? 'bottom-start',
|
|
143
|
-
middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: 8 })],
|
|
144
|
-
}).then(({ x: fx, y: fy }) => {
|
|
145
|
-
// Discard stale callbacks from earlier show() calls
|
|
146
|
-
if (positionId !== currentPositionId) return;
|
|
147
|
-
// computePosition returns coordinates relative to the tooltip's offset
|
|
148
|
-
// parent (the container with position: relative), so apply directly.
|
|
149
|
-
tooltip.style.left = `${fx}px`;
|
|
150
|
-
tooltip.style.top = `${fy}px`;
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function hide(): void {
|
|
155
|
-
tooltip.style.display = 'none';
|
|
156
|
-
lastContentKey = '';
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function destroy(): void {
|
|
160
|
-
document.removeEventListener('touchstart', handleDocumentTouch);
|
|
161
|
-
document.removeEventListener('keydown', handleDocumentKeydown);
|
|
162
|
-
if (tooltip.parentNode) {
|
|
163
|
-
tooltip.parentNode.removeChild(tooltip);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { show, hide, destroy };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function esc(str: string): string {
|
|
171
|
-
return str
|
|
172
|
-
.replace(/&/g, '&')
|
|
173
|
-
.replace(/</g, '<')
|
|
174
|
-
.replace(/>/g, '>')
|
|
175
|
-
.replace(/"/g, '"')
|
|
176
|
-
.replace(/'/g, ''');
|
|
177
|
-
}
|