@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/map-renderer.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map SVG renderer: converts a MapLayout into SVG DOM elements.
|
|
3
|
-
*
|
|
4
|
-
* Creates an <svg> with geo feature paths, border overlays, gradient or
|
|
5
|
-
* categorical 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
|
-
MapBorders,
|
|
11
|
-
MapFeatureMark,
|
|
12
|
-
MapLayout,
|
|
13
|
-
ResolvedAnimation,
|
|
14
|
-
} from '@opendata-ai/openchart-core';
|
|
15
|
-
import { stampAnimationVars } from './animation-vars';
|
|
16
|
-
import { renderChromeElement } from './renderers/chrome';
|
|
17
|
-
import { renderLegend } from './renderers/legend';
|
|
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
|
-
// Above this count, skip per-feature CSS fill animations (not GPU-compositable)
|
|
24
|
-
// and fade the entire features group with a single opacity animation instead.
|
|
25
|
-
const BULK_ANIMATION_THRESHOLD = 200;
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Helpers
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
function createSVGElement(tag: string): SVGElement {
|
|
32
|
-
return document.createElementNS(SVG_NS, tag);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {
|
|
36
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
37
|
-
el.setAttribute(key, String(value));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
// Chrome rendering (delegates to the shared helper for text wrapping)
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
function renderChrome(parent: SVGElement, layout: MapLayout): void {
|
|
46
|
-
const g = createSVGElement('g');
|
|
47
|
-
g.setAttribute('class', 'oc-chrome');
|
|
48
|
-
|
|
49
|
-
const { chrome, measureText } = layout;
|
|
50
|
-
const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
|
|
51
|
-
|
|
52
|
-
if (chrome.title) {
|
|
53
|
-
renderChromeElement(g, chrome.title, 'oc-title', 'title', measureText);
|
|
54
|
-
}
|
|
55
|
-
if (chrome.subtitle) {
|
|
56
|
-
renderChromeElement(g, chrome.subtitle, 'oc-subtitle', 'subtitle', measureText);
|
|
57
|
-
}
|
|
58
|
-
if (chrome.source) {
|
|
59
|
-
renderChromeElement(
|
|
60
|
-
g,
|
|
61
|
-
{ ...chrome.source, y: bottomOffset + chrome.source.y },
|
|
62
|
-
'oc-source',
|
|
63
|
-
'source',
|
|
64
|
-
measureText,
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
if (chrome.byline) {
|
|
68
|
-
renderChromeElement(
|
|
69
|
-
g,
|
|
70
|
-
{ ...chrome.byline, y: bottomOffset + chrome.byline.y },
|
|
71
|
-
'oc-byline',
|
|
72
|
-
'byline',
|
|
73
|
-
measureText,
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (chrome.footer) {
|
|
77
|
-
renderChromeElement(
|
|
78
|
-
g,
|
|
79
|
-
{ ...chrome.footer, y: bottomOffset + chrome.footer.y },
|
|
80
|
-
'oc-footer',
|
|
81
|
-
'footer',
|
|
82
|
-
measureText,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
parent.appendChild(g);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Watermark rendering
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
|
|
93
|
-
function renderWatermark(parent: SVGElement, layout: MapLayout): void {
|
|
94
|
-
if (layout.width < 480) return;
|
|
95
|
-
|
|
96
|
-
const { width, height, theme } = layout;
|
|
97
|
-
const padding = theme.spacing.padding;
|
|
98
|
-
const rightEdge = width - padding;
|
|
99
|
-
const bottomEdge = height - padding;
|
|
100
|
-
const fill = theme.colors.axis;
|
|
101
|
-
|
|
102
|
-
const a = createSVGElement('a');
|
|
103
|
-
a.setAttribute('href', BRAND_URL);
|
|
104
|
-
a.setAttributeNS(XLINK_NS, 'xlink:href', BRAND_URL);
|
|
105
|
-
a.setAttribute('target', '_blank');
|
|
106
|
-
a.setAttribute('rel', 'noopener');
|
|
107
|
-
a.setAttribute('class', 'oc-chrome-ref');
|
|
108
|
-
|
|
109
|
-
const text = createSVGElement('text');
|
|
110
|
-
setAttrs(text, {
|
|
111
|
-
x: rightEdge,
|
|
112
|
-
y: bottomEdge,
|
|
113
|
-
'dominant-baseline': 'alphabetic',
|
|
114
|
-
'text-anchor': 'end',
|
|
115
|
-
'font-family': theme.fonts.family,
|
|
116
|
-
'font-size': 12,
|
|
117
|
-
'fill-opacity': 0.55,
|
|
118
|
-
});
|
|
119
|
-
(text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', fill);
|
|
120
|
-
|
|
121
|
-
const trySpan = createSVGElement('tspan');
|
|
122
|
-
setAttrs(trySpan, { 'font-weight': 500 });
|
|
123
|
-
trySpan.textContent = 'try';
|
|
124
|
-
|
|
125
|
-
const openDataSpan = createSVGElement('tspan');
|
|
126
|
-
setAttrs(openDataSpan, { 'font-weight': 600, 'font-size': 16 });
|
|
127
|
-
openDataSpan.textContent = 'OpenData';
|
|
128
|
-
|
|
129
|
-
const aiSpan = createSVGElement('tspan');
|
|
130
|
-
setAttrs(aiSpan, { 'font-weight': 500 });
|
|
131
|
-
aiSpan.textContent = '.ai';
|
|
132
|
-
|
|
133
|
-
text.appendChild(trySpan);
|
|
134
|
-
text.appendChild(openDataSpan);
|
|
135
|
-
text.appendChild(aiSpan);
|
|
136
|
-
a.appendChild(text);
|
|
137
|
-
parent.appendChild(a);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
// Border rendering
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
|
|
144
|
-
function renderBorders(parent: SVGElement, borders: MapBorders): void {
|
|
145
|
-
const g = createSVGElement('g');
|
|
146
|
-
g.setAttribute('class', 'oc-map-borders');
|
|
147
|
-
|
|
148
|
-
// Interior borders (between features)
|
|
149
|
-
if (borders.interiorPath) {
|
|
150
|
-
const interior = createSVGElement('path');
|
|
151
|
-
setAttrs(interior, {
|
|
152
|
-
d: borders.interiorPath,
|
|
153
|
-
fill: 'none',
|
|
154
|
-
stroke: borders.interiorStroke,
|
|
155
|
-
'stroke-width': 0.5,
|
|
156
|
-
'stroke-linejoin': 'round',
|
|
157
|
-
});
|
|
158
|
-
interior.setAttribute('class', 'oc-map-border-interior');
|
|
159
|
-
interior.setAttribute('pointer-events', 'none');
|
|
160
|
-
g.appendChild(interior);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Outline border (coastline / outer boundary)
|
|
164
|
-
if (borders.outlinePath) {
|
|
165
|
-
const outline = createSVGElement('path');
|
|
166
|
-
setAttrs(outline, {
|
|
167
|
-
d: borders.outlinePath,
|
|
168
|
-
fill: 'none',
|
|
169
|
-
stroke: borders.outlineStroke,
|
|
170
|
-
'stroke-width': 1,
|
|
171
|
-
'stroke-linejoin': 'round',
|
|
172
|
-
});
|
|
173
|
-
outline.setAttribute('class', 'oc-map-border-outline');
|
|
174
|
-
outline.setAttribute('pointer-events', 'none');
|
|
175
|
-
g.appendChild(outline);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
parent.appendChild(g);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Feature rendering
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
function renderFeatures(
|
|
186
|
-
parent: SVGElement,
|
|
187
|
-
features: MapFeatureMark[],
|
|
188
|
-
animation?: ResolvedAnimation,
|
|
189
|
-
staggerBudget = 0,
|
|
190
|
-
): void {
|
|
191
|
-
const g = createSVGElement('g');
|
|
192
|
-
g.setAttribute('class', 'oc-map-features');
|
|
193
|
-
g.setAttribute('role', 'list');
|
|
194
|
-
|
|
195
|
-
const bulk = features.length > BULK_ANIMATION_THRESHOLD;
|
|
196
|
-
|
|
197
|
-
if (bulk && animation?.enter) {
|
|
198
|
-
g.setAttribute('data-bulk-animate', '');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const maxIdx = features.length - 1;
|
|
202
|
-
|
|
203
|
-
// Build evenly spaced delays then shuffle for organic pop-in
|
|
204
|
-
// (skipped in bulk mode where the whole group fades as one)
|
|
205
|
-
const shuffledDelays: number[] = [];
|
|
206
|
-
if (!bulk && staggerBudget > 0 && maxIdx > 0) {
|
|
207
|
-
for (let i = 0; i <= maxIdx; i++) {
|
|
208
|
-
shuffledDelays.push((i / maxIdx) * staggerBudget);
|
|
209
|
-
}
|
|
210
|
-
// Seeded Fisher-Yates shuffle (deterministic per-map via first feature id)
|
|
211
|
-
let seed = 0x9e3779b9 ^ (features.length * 2654435761);
|
|
212
|
-
for (let i = maxIdx; i > 0; i--) {
|
|
213
|
-
seed = (seed + 0x6d2b79f5) | 0;
|
|
214
|
-
let t = seed ^ (seed >>> 15);
|
|
215
|
-
t = Math.imul(t, t | 1);
|
|
216
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
217
|
-
const r = ((t ^ (t >>> 14)) >>> 0) % (i + 1);
|
|
218
|
-
[shuffledDelays[i], shuffledDelays[r]] = [shuffledDelays[r], shuffledDelays[i]];
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
for (let i = 0; i < features.length; i++) {
|
|
223
|
-
const feature = features[i];
|
|
224
|
-
const path = createSVGElement('path');
|
|
225
|
-
path.setAttribute('class', 'oc-map-feature');
|
|
226
|
-
path.setAttribute('role', 'listitem');
|
|
227
|
-
setAttrs(path, {
|
|
228
|
-
d: feature.path,
|
|
229
|
-
fill: feature.fill,
|
|
230
|
-
stroke: feature.stroke,
|
|
231
|
-
'stroke-width': feature.strokeWidth,
|
|
232
|
-
});
|
|
233
|
-
path.setAttribute('data-feature-id', String(feature.id));
|
|
234
|
-
path.setAttribute('data-key', String(feature.id));
|
|
235
|
-
if (feature.name) {
|
|
236
|
-
path.setAttribute('data-feature-name', feature.name);
|
|
237
|
-
}
|
|
238
|
-
if (feature.aria?.label) {
|
|
239
|
-
path.setAttribute('aria-label', feature.aria.label);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Per-feature animation props only in non-bulk mode
|
|
243
|
-
if (!bulk && animation?.enter) {
|
|
244
|
-
const idx = feature.animationIndex ?? i;
|
|
245
|
-
path.setAttribute('data-animation-index', String(idx));
|
|
246
|
-
const s = (path as SVGElement & ElementCSSInlineStyle).style;
|
|
247
|
-
s.setProperty('--oc-mark-index', String(idx));
|
|
248
|
-
s.setProperty('--oc-feature-fill', feature.fill);
|
|
249
|
-
if (staggerBudget > 0 && maxIdx > 0) {
|
|
250
|
-
s.setProperty('--oc-map-delay', `${shuffledDelays[i]}ms`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
g.appendChild(path);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
parent.appendChild(g);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Public API
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Render a MapLayout to an SVG element.
|
|
266
|
-
*/
|
|
267
|
-
export function renderMapSVG(layout: MapLayout, opts?: { animate?: boolean }): SVGSVGElement {
|
|
268
|
-
const { width, height, features, borders, a11y, watermark, animation } = layout;
|
|
269
|
-
const animate = opts?.animate && !!animation?.enter;
|
|
270
|
-
|
|
271
|
-
const svg = createSVGElement('svg') as SVGSVGElement;
|
|
272
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
273
|
-
svg.setAttribute('role', 'img');
|
|
274
|
-
if (a11y.altText) {
|
|
275
|
-
svg.setAttribute('aria-label', a11y.altText);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const classes = animate ? 'oc-map oc-animate' : 'oc-map';
|
|
279
|
-
svg.setAttribute('class', classes);
|
|
280
|
-
|
|
281
|
-
const bulk = features.length > BULK_ANIMATION_THRESHOLD;
|
|
282
|
-
let mapStaggerBudget = 0;
|
|
283
|
-
if (animate && animation?.enter) {
|
|
284
|
-
const dur = animation.enter.duration;
|
|
285
|
-
const perFeature = Math.min(dur, 500);
|
|
286
|
-
if (!bulk) {
|
|
287
|
-
mapStaggerBudget = Math.round(dur * 1.2);
|
|
288
|
-
}
|
|
289
|
-
const n = Math.max(features.length, 1);
|
|
290
|
-
const stagger = !bulk && n > 1 ? mapStaggerBudget / (n - 1) : 0;
|
|
291
|
-
stampAnimationVars(svg, {
|
|
292
|
-
duration: perFeature,
|
|
293
|
-
stagger,
|
|
294
|
-
annotationDelay: animation.annotationDelay,
|
|
295
|
-
ease: animation.enter.ease,
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Empty defs element (will be filled by gradient legend)
|
|
300
|
-
const defs = createSVGElement('defs');
|
|
301
|
-
svg.appendChild(defs);
|
|
302
|
-
|
|
303
|
-
// Render chrome
|
|
304
|
-
renderChrome(svg, layout);
|
|
305
|
-
|
|
306
|
-
// Create map group offset to the drawing area
|
|
307
|
-
const mapGroup = createSVGElement('g');
|
|
308
|
-
mapGroup.setAttribute('class', 'oc-map-group');
|
|
309
|
-
mapGroup.setAttribute('transform', `translate(${layout.area.x},${layout.area.y})`);
|
|
310
|
-
|
|
311
|
-
// Camera group wraps features + borders only (chrome, legend, watermark stay outside)
|
|
312
|
-
const cameraGroup = createSVGElement('g');
|
|
313
|
-
cameraGroup.setAttribute('class', 'oc-map-camera');
|
|
314
|
-
cameraGroup.setAttribute('data-oc-map-camera', '');
|
|
315
|
-
|
|
316
|
-
// Render features first (so borders overlay them)
|
|
317
|
-
renderFeatures(cameraGroup, features, animate ? animation : undefined, mapStaggerBudget);
|
|
318
|
-
|
|
319
|
-
// Render borders on top of features
|
|
320
|
-
renderBorders(cameraGroup, borders);
|
|
321
|
-
|
|
322
|
-
mapGroup.appendChild(cameraGroup);
|
|
323
|
-
svg.appendChild(mapGroup);
|
|
324
|
-
|
|
325
|
-
// Render legend (continuous for quantitative, categorical for nominal)
|
|
326
|
-
if (layout.continuousLegend) {
|
|
327
|
-
renderLegend(svg, layout.continuousLegend);
|
|
328
|
-
} else if (layout.categoricalLegend) {
|
|
329
|
-
renderLegend(svg, layout.categoricalLegend);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Render watermark
|
|
333
|
-
if (watermark) {
|
|
334
|
-
renderWatermark(svg, layout);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return svg;
|
|
338
|
-
}
|
package/src/map-transition.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Map fill transition: tweens feature fills between layout snapshots.
|
|
3
|
-
*
|
|
4
|
-
* Captures current fill colors before a re-render, then interpolates
|
|
5
|
-
* from previous to new fills using a rAF loop. Same approach as the
|
|
6
|
-
* main chart transition driver, but scoped to map-specific concerns
|
|
7
|
-
* (no geometry tweening, just color).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { interpolateRgb } from 'd3-interpolate';
|
|
11
|
-
import { easingFns } from './story/tween';
|
|
12
|
-
|
|
13
|
-
export function captureFeatureFills(svg: SVGElement | null): Map<string, string> {
|
|
14
|
-
const fills = new Map<string, string>();
|
|
15
|
-
if (!svg) return fills;
|
|
16
|
-
const paths = svg.querySelectorAll('.oc-map-feature[data-key]');
|
|
17
|
-
for (const p of paths) {
|
|
18
|
-
const key = p.getAttribute('data-key');
|
|
19
|
-
const fill = p.getAttribute('fill');
|
|
20
|
-
if (key && fill) fills.set(key, fill);
|
|
21
|
-
}
|
|
22
|
-
return fills;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function runMapFillTransition(
|
|
26
|
-
svg: SVGElement,
|
|
27
|
-
prevFills: Map<string, string>,
|
|
28
|
-
opts: { duration: number },
|
|
29
|
-
): { cancel: () => void } {
|
|
30
|
-
const paths = svg.querySelectorAll('.oc-map-feature[data-key]');
|
|
31
|
-
const tweens: Array<{ el: Element; from: string; to: string; interp: (t: number) => string }> =
|
|
32
|
-
[];
|
|
33
|
-
|
|
34
|
-
for (const p of paths) {
|
|
35
|
-
const key = p.getAttribute('data-key');
|
|
36
|
-
const to = p.getAttribute('fill');
|
|
37
|
-
const from = key ? prevFills.get(key) : null;
|
|
38
|
-
if (from && to && from !== to) {
|
|
39
|
-
tweens.push({ el: p, from, to, interp: interpolateRgb(from, to) });
|
|
40
|
-
// Synchronous from-state: set previous fill before first rAF
|
|
41
|
-
p.setAttribute('fill', from);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (tweens.length === 0 || opts.duration <= 0) {
|
|
46
|
-
for (const tw of tweens) tw.el.setAttribute('fill', tw.to);
|
|
47
|
-
return { cancel: () => {} };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const duration = opts.duration;
|
|
51
|
-
let startTime: number | null = null;
|
|
52
|
-
let rafId = 0;
|
|
53
|
-
let cancelled = false;
|
|
54
|
-
|
|
55
|
-
function tick(now: number) {
|
|
56
|
-
if (cancelled) return;
|
|
57
|
-
if (startTime === null) startTime = now;
|
|
58
|
-
const elapsed = now - startTime;
|
|
59
|
-
const t = Math.min(1, elapsed / duration);
|
|
60
|
-
const eased = easingFns.easeInOutCubic(t);
|
|
61
|
-
|
|
62
|
-
for (const tw of tweens) {
|
|
63
|
-
tw.el.setAttribute('fill', tw.interp(eased));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (t < 1) {
|
|
67
|
-
rafId = requestAnimationFrame(tick);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
rafId = requestAnimationFrame(tick);
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
cancel() {
|
|
75
|
-
cancelled = true;
|
|
76
|
-
cancelAnimationFrame(rafId);
|
|
77
|
-
// Snap to final state
|
|
78
|
-
for (const tw of tweens) {
|
|
79
|
-
tw.el.setAttribute('fill', tw.to);
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
}
|
package/src/measure-text.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Canvas-backed text measurement factory.
|
|
3
|
-
*
|
|
4
|
-
* Shared by mount.ts (charts) and sankey-mount.ts (sankey diagrams) so both
|
|
5
|
-
* pipelines get accurate browser-measured text widths instead of the heuristic
|
|
6
|
-
* fallback. Falls back to the heuristic when canvas isn't available (e.g. SSR).
|
|
7
|
-
*
|
|
8
|
-
* The font family is passed in (resolved from the container's --oc-font-family
|
|
9
|
-
* or the spec theme) rather than hardcoded, so measurement matches what the SVG
|
|
10
|
-
* text actually renders with. This matters on real devices where the primary
|
|
11
|
-
* webfont (e.g. Inter) loads late: measuring against the wrong font produces
|
|
12
|
-
* wrong widths and mangles layout.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import type { MeasureTextFn } from '@opendata-ai/openchart-core';
|
|
16
|
-
import { estimateTextWidth } from '@opendata-ai/openchart-core';
|
|
17
|
-
|
|
18
|
-
const DEFAULT_FONT_FAMILY = 'Inter, sans-serif';
|
|
19
|
-
|
|
20
|
-
export function createMeasureText(fontFamily: string = DEFAULT_FONT_FAMILY): MeasureTextFn {
|
|
21
|
-
let canvas: HTMLCanvasElement | null = null;
|
|
22
|
-
let ctx: CanvasRenderingContext2D | null = null;
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
text: string,
|
|
26
|
-
fontSize: number,
|
|
27
|
-
fontWeight?: number,
|
|
28
|
-
): { width: number; height: number } => {
|
|
29
|
-
if (!canvas) {
|
|
30
|
-
canvas = document.createElement('canvas');
|
|
31
|
-
ctx = canvas.getContext('2d');
|
|
32
|
-
}
|
|
33
|
-
if (!ctx) {
|
|
34
|
-
// Fallback: heuristic estimation
|
|
35
|
-
return {
|
|
36
|
-
width: estimateTextWidth(text, fontSize, fontWeight ?? 400),
|
|
37
|
-
height: fontSize * 1.2,
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const weight = fontWeight ?? 400;
|
|
42
|
-
ctx.font = `${weight} ${fontSize}px ${fontFamily}`;
|
|
43
|
-
const metrics = ctx.measureText(text);
|
|
44
|
-
return {
|
|
45
|
-
width: metrics.width,
|
|
46
|
-
height: fontSize * 1.2,
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Resolve the font family a container's SVG text will actually render with.
|
|
53
|
-
*
|
|
54
|
-
* Reads the --oc-font-family custom property (set by tokens.css once .oc-root
|
|
55
|
-
* is applied, or overridden per-spec by the theme resolver), falling back to
|
|
56
|
-
* the container's computed fontFamily, then to the default Inter stack. Returns
|
|
57
|
-
* the previous hardcoded stack when there is no DOM (SSR) or no computed style.
|
|
58
|
-
*/
|
|
59
|
-
export function resolveFontFamily(container: HTMLElement): string {
|
|
60
|
-
if (typeof window === 'undefined' || typeof getComputedStyle !== 'function') {
|
|
61
|
-
return DEFAULT_FONT_FAMILY;
|
|
62
|
-
}
|
|
63
|
-
try {
|
|
64
|
-
const style = getComputedStyle(container);
|
|
65
|
-
const custom = style.getPropertyValue('--oc-font-family').trim();
|
|
66
|
-
if (custom) return custom;
|
|
67
|
-
const computed = style.fontFamily?.trim();
|
|
68
|
-
if (computed) return computed;
|
|
69
|
-
} catch {
|
|
70
|
-
// getComputedStyle can throw on detached nodes in some engines; fall through.
|
|
71
|
-
}
|
|
72
|
-
return DEFAULT_FONT_FAMILY;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The first family in a CSS font stack, with surrounding quotes stripped, e.g.
|
|
77
|
-
* `'"Inter Variable", Inter, sans-serif'` -> `Inter Variable`. Used to build a
|
|
78
|
-
* representative check string for document.fonts.check().
|
|
79
|
-
*/
|
|
80
|
-
export function primaryFontName(fontFamily: string): string {
|
|
81
|
-
const first = fontFamily.split(',')[0]?.trim() ?? '';
|
|
82
|
-
return first.replace(/^["']|["']$/g, '').trim();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* True when the primary font of the resolved stack is not yet loaded and a
|
|
87
|
-
* later document.fonts.ready may change text metrics. False when fonts are
|
|
88
|
-
* ready, unavailable (SSR / no FontFaceSet), or the primary is a generic family.
|
|
89
|
-
*/
|
|
90
|
-
export function fontsPending(fontFamily: string): boolean {
|
|
91
|
-
if (typeof document === 'undefined') return false;
|
|
92
|
-
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
93
|
-
if (!fonts || typeof fonts.check !== 'function') return false;
|
|
94
|
-
const name = primaryFontName(fontFamily);
|
|
95
|
-
if (!name) return false;
|
|
96
|
-
const generics = new Set([
|
|
97
|
-
'sans-serif',
|
|
98
|
-
'serif',
|
|
99
|
-
'monospace',
|
|
100
|
-
'system-ui',
|
|
101
|
-
'cursive',
|
|
102
|
-
'fantasy',
|
|
103
|
-
'ui-sans-serif',
|
|
104
|
-
'ui-serif',
|
|
105
|
-
'ui-monospace',
|
|
106
|
-
]);
|
|
107
|
-
if (generics.has(name.toLowerCase())) return false;
|
|
108
|
-
try {
|
|
109
|
-
return !fonts.check(`12px "${name}"`);
|
|
110
|
-
} catch {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Run `onReady` exactly once after webfonts finish loading, if they were
|
|
117
|
-
* pending at call time. `isAlive` is checked before invoking so a destroyed
|
|
118
|
-
* chart is never re-rendered. Returns the initial pending state so callers can
|
|
119
|
-
* set their `ocFontsState` dataset flag. No-op (returns false) under SSR or
|
|
120
|
-
* when the primary font is already loaded.
|
|
121
|
-
*/
|
|
122
|
-
export function scheduleFontReload(
|
|
123
|
-
fontFamily: string,
|
|
124
|
-
isAlive: () => boolean,
|
|
125
|
-
onReady: () => void,
|
|
126
|
-
): boolean {
|
|
127
|
-
if (!fontsPending(fontFamily)) return false;
|
|
128
|
-
const fonts = (document as Document & { fonts?: FontFaceSet }).fonts;
|
|
129
|
-
if (!fonts?.ready) return false;
|
|
130
|
-
fonts.ready.then(() => {
|
|
131
|
-
if (isAlive()) onReady();
|
|
132
|
-
});
|
|
133
|
-
return true;
|
|
134
|
-
}
|