@opendata-ai/openchart-vanilla 8.0.0-rc.9 → 8.0.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/dist/{chunk-SPILUE5S.js → chunk-2NAQRP66.js} +2 -2
- package/dist/{chunk-IZQBT6QM.js → chunk-6BUCYCLD.js} +7283 -5881
- package/dist/chunk-6BUCYCLD.js.map +1 -0
- package/dist/chunk-DCODCLWI.js +234 -0
- package/dist/chunk-DCODCLWI.js.map +1 -0
- package/dist/{chunk-QW7ZB6NE.js → chunk-EKAUQAAX.js} +49 -45
- package/dist/chunk-EKAUQAAX.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 +309 -27
- package/dist/index.js +2020 -836
- package/dist/index.js.map +1 -1
- package/dist/{mount-CUKQLeqD.d.ts → mount-CL-nA0f-.d.ts} +85 -2
- package/dist/simulation-worker.js +83 -2
- package/dist/static.d.ts +2 -0
- package/dist/static.js +8 -69
- package/dist/static.js.map +1 -1
- package/dist/story/index.d.ts +25 -9
- package/dist/story/index.js +9 -7
- 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-IZQBT6QM.js.map +0 -1
- package/dist/chunk-QW7ZB6NE.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 -1477
- package/src/__tests__/editable-prop.test.ts +0 -174
- 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__/invert.test.ts +0 -187
- package/src/__tests__/known-bugs-render.test.ts +0 -183
- package/src/__tests__/map.test.ts +0 -636
- 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 -1062
- 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 -1792
- package/src/__tests__/update-scrolly-matrix.test.ts +0 -563
- 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 -649
- package/src/interactions/index.ts +0 -27
- package/src/interactions/invert.ts +0 -54
- package/src/interactions/keyboard-nav.ts +0 -111
- package/src/interactions/legend-interaction.ts +0 -46
- package/src/interactions/selection.ts +0 -286
- package/src/interactions/tooltip-events.ts +0 -72
- package/src/map-camera.ts +0 -111
- package/src/map-mount.ts +0 -797
- package/src/map-renderer.ts +0 -476
- package/src/map-transition.ts +0 -99
- package/src/measure-text.ts +0 -134
- package/src/mount.ts +0 -1434
- 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 -312
- 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 -389
- 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-SPILUE5S.js.map → chunk-2NAQRP66.js.map} +0 -0
package/src/map-renderer.ts
DELETED
|
@@ -1,476 +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
|
-
MapPointMark,
|
|
14
|
-
ResolvedAnimation,
|
|
15
|
-
} from '@opendata-ai/openchart-core';
|
|
16
|
-
import { stampAnimationVars } from './animation-vars';
|
|
17
|
-
import { FOCUS_DIM_OPACITY } from './map-camera';
|
|
18
|
-
import { renderChromeElement } from './renderers/chrome';
|
|
19
|
-
import { renderLegend } from './renderers/legend';
|
|
20
|
-
|
|
21
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
22
|
-
const XLINK_NS = 'http://www.w3.org/1999/xlink';
|
|
23
|
-
const BRAND_URL = 'https://tryopendata.ai';
|
|
24
|
-
|
|
25
|
-
// Above this count, skip per-feature CSS fill animations (not GPU-compositable)
|
|
26
|
-
// and fade the entire features group with a single opacity animation instead.
|
|
27
|
-
const BULK_ANIMATION_THRESHOLD = 200;
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Helpers
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
function createSVGElement(tag: string): SVGElement {
|
|
34
|
-
return document.createElementNS(SVG_NS, tag);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function setAttrs(el: SVGElement, attrs: Record<string, string | number>): void {
|
|
38
|
-
for (const [key, value] of Object.entries(attrs)) {
|
|
39
|
-
el.setAttribute(key, String(value));
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Chrome rendering (delegates to the shared helper for text wrapping)
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
function renderChrome(parent: SVGElement, layout: MapLayout): void {
|
|
48
|
-
const g = createSVGElement('g');
|
|
49
|
-
g.setAttribute('class', 'oc-chrome');
|
|
50
|
-
|
|
51
|
-
const { chrome, measureText } = layout;
|
|
52
|
-
const bottomOffset = chrome.bottomAnchorY ?? layout.area.y + layout.area.height;
|
|
53
|
-
|
|
54
|
-
if (chrome.title) {
|
|
55
|
-
renderChromeElement(g, chrome.title, 'oc-title', 'title', measureText);
|
|
56
|
-
}
|
|
57
|
-
if (chrome.subtitle) {
|
|
58
|
-
renderChromeElement(g, chrome.subtitle, 'oc-subtitle', 'subtitle', measureText);
|
|
59
|
-
}
|
|
60
|
-
if (chrome.source) {
|
|
61
|
-
renderChromeElement(
|
|
62
|
-
g,
|
|
63
|
-
{ ...chrome.source, y: bottomOffset + chrome.source.y },
|
|
64
|
-
'oc-source',
|
|
65
|
-
'source',
|
|
66
|
-
measureText,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
if (chrome.byline) {
|
|
70
|
-
renderChromeElement(
|
|
71
|
-
g,
|
|
72
|
-
{ ...chrome.byline, y: bottomOffset + chrome.byline.y },
|
|
73
|
-
'oc-byline',
|
|
74
|
-
'byline',
|
|
75
|
-
measureText,
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
if (chrome.footer) {
|
|
79
|
-
renderChromeElement(
|
|
80
|
-
g,
|
|
81
|
-
{ ...chrome.footer, y: bottomOffset + chrome.footer.y },
|
|
82
|
-
'oc-footer',
|
|
83
|
-
'footer',
|
|
84
|
-
measureText,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
parent.appendChild(g);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// Watermark rendering
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
function renderWatermark(parent: SVGElement, layout: MapLayout): void {
|
|
96
|
-
if (layout.width < 480) return;
|
|
97
|
-
|
|
98
|
-
const { width, height, theme } = layout;
|
|
99
|
-
const padding = theme.spacing.padding;
|
|
100
|
-
const rightEdge = width - padding;
|
|
101
|
-
const bottomEdge = height - padding;
|
|
102
|
-
const fill = theme.colors.axis;
|
|
103
|
-
|
|
104
|
-
const a = createSVGElement('a');
|
|
105
|
-
a.setAttribute('href', BRAND_URL);
|
|
106
|
-
a.setAttributeNS(XLINK_NS, 'xlink:href', BRAND_URL);
|
|
107
|
-
a.setAttribute('target', '_blank');
|
|
108
|
-
a.setAttribute('rel', 'noopener');
|
|
109
|
-
a.setAttribute('class', 'oc-chrome-ref');
|
|
110
|
-
|
|
111
|
-
const text = createSVGElement('text');
|
|
112
|
-
setAttrs(text, {
|
|
113
|
-
x: rightEdge,
|
|
114
|
-
y: bottomEdge,
|
|
115
|
-
'dominant-baseline': 'alphabetic',
|
|
116
|
-
'text-anchor': 'end',
|
|
117
|
-
'font-family': theme.fonts.family,
|
|
118
|
-
'font-size': 12,
|
|
119
|
-
'fill-opacity': 0.55,
|
|
120
|
-
});
|
|
121
|
-
(text as SVGElement & ElementCSSInlineStyle).style.setProperty('fill', fill);
|
|
122
|
-
|
|
123
|
-
const trySpan = createSVGElement('tspan');
|
|
124
|
-
setAttrs(trySpan, { 'font-weight': 500 });
|
|
125
|
-
trySpan.textContent = 'try';
|
|
126
|
-
|
|
127
|
-
const openDataSpan = createSVGElement('tspan');
|
|
128
|
-
setAttrs(openDataSpan, { 'font-weight': 600, 'font-size': 16 });
|
|
129
|
-
openDataSpan.textContent = 'OpenData';
|
|
130
|
-
|
|
131
|
-
const aiSpan = createSVGElement('tspan');
|
|
132
|
-
setAttrs(aiSpan, { 'font-weight': 500 });
|
|
133
|
-
aiSpan.textContent = '.ai';
|
|
134
|
-
|
|
135
|
-
text.appendChild(trySpan);
|
|
136
|
-
text.appendChild(openDataSpan);
|
|
137
|
-
text.appendChild(aiSpan);
|
|
138
|
-
a.appendChild(text);
|
|
139
|
-
parent.appendChild(a);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Border rendering
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
function renderBorders(parent: SVGElement, borders: MapBorders): void {
|
|
147
|
-
const g = createSVGElement('g');
|
|
148
|
-
g.setAttribute('class', 'oc-map-borders');
|
|
149
|
-
|
|
150
|
-
// Interior borders (between features)
|
|
151
|
-
if (borders.interiorPath) {
|
|
152
|
-
const interior = createSVGElement('path');
|
|
153
|
-
setAttrs(interior, {
|
|
154
|
-
d: borders.interiorPath,
|
|
155
|
-
fill: 'none',
|
|
156
|
-
stroke: borders.interiorStroke,
|
|
157
|
-
'stroke-width': 0.5,
|
|
158
|
-
'stroke-linejoin': 'round',
|
|
159
|
-
});
|
|
160
|
-
interior.setAttribute('class', 'oc-map-border-interior');
|
|
161
|
-
interior.setAttribute('pointer-events', 'none');
|
|
162
|
-
g.appendChild(interior);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Outline border (coastline / outer boundary)
|
|
166
|
-
if (borders.outlinePath) {
|
|
167
|
-
const outline = createSVGElement('path');
|
|
168
|
-
setAttrs(outline, {
|
|
169
|
-
d: borders.outlinePath,
|
|
170
|
-
fill: 'none',
|
|
171
|
-
stroke: borders.outlineStroke,
|
|
172
|
-
'stroke-width': 1,
|
|
173
|
-
'stroke-linejoin': 'round',
|
|
174
|
-
});
|
|
175
|
-
outline.setAttribute('class', 'oc-map-border-outline');
|
|
176
|
-
outline.setAttribute('pointer-events', 'none');
|
|
177
|
-
g.appendChild(outline);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
parent.appendChild(g);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// Feature rendering
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
function renderFeatures(
|
|
188
|
-
parent: SVGElement,
|
|
189
|
-
features: MapFeatureMark[],
|
|
190
|
-
animation?: ResolvedAnimation,
|
|
191
|
-
staggerBudget = 0,
|
|
192
|
-
focusIds?: Set<string>,
|
|
193
|
-
): void {
|
|
194
|
-
const g = createSVGElement('g');
|
|
195
|
-
g.setAttribute('class', 'oc-map-features');
|
|
196
|
-
g.setAttribute('role', 'list');
|
|
197
|
-
|
|
198
|
-
const bulk = features.length > BULK_ANIMATION_THRESHOLD;
|
|
199
|
-
|
|
200
|
-
if (bulk && animation?.enter) {
|
|
201
|
-
g.setAttribute('data-bulk-animate', '');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const maxIdx = features.length - 1;
|
|
205
|
-
|
|
206
|
-
// Build evenly spaced delays then shuffle for organic pop-in
|
|
207
|
-
// (skipped in bulk mode where the whole group fades as one)
|
|
208
|
-
const shuffledDelays: number[] = [];
|
|
209
|
-
if (!bulk && staggerBudget > 0 && maxIdx > 0) {
|
|
210
|
-
for (let i = 0; i <= maxIdx; i++) {
|
|
211
|
-
shuffledDelays.push((i / maxIdx) * staggerBudget);
|
|
212
|
-
}
|
|
213
|
-
// Seeded Fisher-Yates shuffle (deterministic per-map via first feature id)
|
|
214
|
-
let seed = 0x9e3779b9 ^ (features.length * 2654435761);
|
|
215
|
-
for (let i = maxIdx; i > 0; i--) {
|
|
216
|
-
seed = (seed + 0x6d2b79f5) | 0;
|
|
217
|
-
let t = seed ^ (seed >>> 15);
|
|
218
|
-
t = Math.imul(t, t | 1);
|
|
219
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
220
|
-
const r = ((t ^ (t >>> 14)) >>> 0) % (i + 1);
|
|
221
|
-
[shuffledDelays[i], shuffledDelays[r]] = [shuffledDelays[r], shuffledDelays[i]];
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
for (let i = 0; i < features.length; i++) {
|
|
226
|
-
const feature = features[i];
|
|
227
|
-
const path = createSVGElement('path');
|
|
228
|
-
path.setAttribute('class', 'oc-map-feature');
|
|
229
|
-
path.setAttribute('role', 'listitem');
|
|
230
|
-
setAttrs(path, {
|
|
231
|
-
d: feature.path,
|
|
232
|
-
fill: feature.fill,
|
|
233
|
-
stroke: feature.stroke,
|
|
234
|
-
'stroke-width': feature.strokeWidth,
|
|
235
|
-
});
|
|
236
|
-
path.setAttribute('data-feature-id', String(feature.id));
|
|
237
|
-
path.setAttribute('data-key', String(feature.id));
|
|
238
|
-
if (feature.name) {
|
|
239
|
-
path.setAttribute('data-feature-name', feature.name);
|
|
240
|
-
}
|
|
241
|
-
if (feature.aria?.label) {
|
|
242
|
-
path.setAttribute('aria-label', feature.aria.label);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Under a focus, features outside the set rest at the dim opacity. Set it
|
|
246
|
-
// as the resting inline opacity (correct in every render path) and as the
|
|
247
|
-
// entrance animation's target so they fade straight to dim instead of
|
|
248
|
-
// hitting full opacity and snapping down when the dim lands post-animation.
|
|
249
|
-
const dimmed = focusIds ? !focusIds.has(String(feature.id)) : false;
|
|
250
|
-
const s = (path as SVGElement & ElementCSSInlineStyle).style;
|
|
251
|
-
if (dimmed) {
|
|
252
|
-
s.setProperty('opacity', String(FOCUS_DIM_OPACITY));
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Per-feature animation props only in non-bulk mode
|
|
256
|
-
if (!bulk && animation?.enter) {
|
|
257
|
-
const idx = feature.animationIndex ?? i;
|
|
258
|
-
path.setAttribute('data-animation-index', String(idx));
|
|
259
|
-
s.setProperty('--oc-mark-index', String(idx));
|
|
260
|
-
s.setProperty('--oc-feature-fill', feature.fill);
|
|
261
|
-
if (dimmed) {
|
|
262
|
-
s.setProperty('--oc-feature-target-opacity', String(FOCUS_DIM_OPACITY));
|
|
263
|
-
}
|
|
264
|
-
if (staggerBudget > 0 && maxIdx > 0) {
|
|
265
|
-
s.setProperty('--oc-map-delay', `${shuffledDelays[i]}ms`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
g.appendChild(path);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
parent.appendChild(g);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
// Point mark rendering
|
|
277
|
-
// ---------------------------------------------------------------------------
|
|
278
|
-
|
|
279
|
-
function renderPointMarks(
|
|
280
|
-
parent: SVGElement,
|
|
281
|
-
points: MapPointMark[],
|
|
282
|
-
animation?: ResolvedAnimation,
|
|
283
|
-
): void {
|
|
284
|
-
if (points.length === 0) return;
|
|
285
|
-
|
|
286
|
-
const g = createSVGElement('g');
|
|
287
|
-
g.setAttribute('class', 'oc-map-points');
|
|
288
|
-
|
|
289
|
-
for (const pt of points) {
|
|
290
|
-
const circle = createSVGElement('circle');
|
|
291
|
-
circle.setAttribute('class', 'oc-map-point');
|
|
292
|
-
setAttrs(circle, {
|
|
293
|
-
cx: pt.cx,
|
|
294
|
-
cy: pt.cy,
|
|
295
|
-
r: pt.r,
|
|
296
|
-
fill: pt.fill,
|
|
297
|
-
'fill-opacity': pt.fillOpacity,
|
|
298
|
-
stroke: pt.stroke,
|
|
299
|
-
'stroke-width': pt.strokeWidth,
|
|
300
|
-
});
|
|
301
|
-
circle.setAttribute('data-point-key', pt.key);
|
|
302
|
-
circle.setAttribute('data-base-r', String(pt.r));
|
|
303
|
-
circle.setAttribute('data-base-stroke-width', String(pt.strokeWidth));
|
|
304
|
-
if (pt.aria?.label) {
|
|
305
|
-
circle.setAttribute('aria-label', pt.aria.label);
|
|
306
|
-
}
|
|
307
|
-
if (animation?.enter) {
|
|
308
|
-
const s = (circle as unknown as ElementCSSInlineStyle).style;
|
|
309
|
-
s.setProperty('--oc-mark-index', String(pt.animationIndex));
|
|
310
|
-
s.setProperty('transform-origin', `${pt.cx}px ${pt.cy}px`);
|
|
311
|
-
}
|
|
312
|
-
g.appendChild(circle);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
parent.appendChild(g);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// ---------------------------------------------------------------------------
|
|
319
|
-
// Public API
|
|
320
|
-
// ---------------------------------------------------------------------------
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Render a MapLayout to an SVG element.
|
|
324
|
-
*/
|
|
325
|
-
export function renderMapSVG(layout: MapLayout, opts?: { animate?: boolean }): SVGSVGElement {
|
|
326
|
-
const { width, height, features, borders, a11y, watermark, animation } = layout;
|
|
327
|
-
const animate = opts?.animate && !!animation?.enter;
|
|
328
|
-
|
|
329
|
-
const svg = createSVGElement('svg') as SVGSVGElement;
|
|
330
|
-
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
331
|
-
svg.setAttribute('role', 'img');
|
|
332
|
-
if (a11y.altText) {
|
|
333
|
-
svg.setAttribute('aria-label', a11y.altText);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const classes = animate ? 'oc-map oc-animate' : 'oc-map';
|
|
337
|
-
svg.setAttribute('class', classes);
|
|
338
|
-
|
|
339
|
-
const bulk = features.length > BULK_ANIMATION_THRESHOLD;
|
|
340
|
-
let mapStaggerBudget = 0;
|
|
341
|
-
if (animate && animation?.enter) {
|
|
342
|
-
const dur = animation.enter.duration;
|
|
343
|
-
const perFeature = Math.min(dur, 500);
|
|
344
|
-
if (!bulk) {
|
|
345
|
-
mapStaggerBudget = Math.round(dur * 1.2);
|
|
346
|
-
}
|
|
347
|
-
const n = Math.max(features.length, 1);
|
|
348
|
-
const stagger = !bulk && n > 1 ? mapStaggerBudget / (n - 1) : 0;
|
|
349
|
-
stampAnimationVars(svg, {
|
|
350
|
-
duration: perFeature,
|
|
351
|
-
stagger,
|
|
352
|
-
annotationDelay: animation.annotationDelay,
|
|
353
|
-
ease: animation.enter.ease,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Empty defs element (will be filled by gradient legend)
|
|
358
|
-
const defs = createSVGElement('defs');
|
|
359
|
-
svg.appendChild(defs);
|
|
360
|
-
|
|
361
|
-
// Create map group offset to the drawing area
|
|
362
|
-
const mapGroup = createSVGElement('g');
|
|
363
|
-
mapGroup.setAttribute('class', 'oc-map-group');
|
|
364
|
-
mapGroup.setAttribute('transform', `translate(${layout.area.x},${layout.area.y})`);
|
|
365
|
-
|
|
366
|
-
// Camera group wraps features + borders only (chrome, legend, watermark stay outside)
|
|
367
|
-
const cameraGroup = createSVGElement('g');
|
|
368
|
-
cameraGroup.setAttribute('class', 'oc-map-camera');
|
|
369
|
-
cameraGroup.setAttribute('data-oc-map-camera', '');
|
|
370
|
-
|
|
371
|
-
// Features outside the focus set rest dimmed; the renderer stamps that so it's
|
|
372
|
-
// correct in every render path and the entrance fades straight to it. This
|
|
373
|
-
// handles *declarative* focus (layout.focus from the spec) only. Imperative
|
|
374
|
-
// focus set via zoomTo()/panTo() never re-renders, so it dims through
|
|
375
|
-
// applyMapCamera + applyFocusDim on the live SVG instead.
|
|
376
|
-
const focusIdSet =
|
|
377
|
-
layout.focus && layout.focus.ids.length > 0 ? new Set(layout.focus.ids.map(String)) : undefined;
|
|
378
|
-
|
|
379
|
-
// Render features first (so borders overlay them)
|
|
380
|
-
renderFeatures(
|
|
381
|
-
cameraGroup,
|
|
382
|
-
features,
|
|
383
|
-
animate ? animation : undefined,
|
|
384
|
-
mapStaggerBudget,
|
|
385
|
-
focusIdSet,
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
// Render borders on top of features
|
|
389
|
-
renderBorders(cameraGroup, borders);
|
|
390
|
-
|
|
391
|
-
// Render point marks inside camera group (above borders).
|
|
392
|
-
// Stamp a per-point stagger so the total spread stays bounded regardless of
|
|
393
|
-
// point count (the CSS hardcoded 60ms per point which explodes past ~15 pts).
|
|
394
|
-
if (animate && animation?.enter && layout.pointMarks.length > 1) {
|
|
395
|
-
const pointStaggerBudget = animation.enter.duration * 0.5;
|
|
396
|
-
const perPoint = pointStaggerBudget / (layout.pointMarks.length - 1);
|
|
397
|
-
svg.style.setProperty('--oc-point-stagger', `${perPoint}ms`);
|
|
398
|
-
}
|
|
399
|
-
renderPointMarks(cameraGroup, layout.pointMarks, animate ? animation : undefined);
|
|
400
|
-
|
|
401
|
-
mapGroup.appendChild(cameraGroup);
|
|
402
|
-
svg.appendChild(mapGroup);
|
|
403
|
-
|
|
404
|
-
// Render chrome AFTER the map group so the title/subtitle paint on top of a
|
|
405
|
-
// focus-zoomed map that overflows up into the chrome band. Earlier SVG
|
|
406
|
-
// children paint underneath later ones, so chrome must come last of the two.
|
|
407
|
-
renderChrome(svg, layout);
|
|
408
|
-
|
|
409
|
-
// Render legend (continuous for quantitative, categorical for nominal)
|
|
410
|
-
if (layout.continuousLegend) {
|
|
411
|
-
renderLegend(svg, layout.continuousLegend);
|
|
412
|
-
} else if (layout.categoricalLegend) {
|
|
413
|
-
renderLegend(svg, layout.categoricalLegend);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Render point legends with a semi-transparent background so they read
|
|
417
|
-
// clearly over map geography (e.g. AK/HI insets in Albers USA).
|
|
418
|
-
const ptLegend = layout.pointCategoricalLegend ?? layout.pointContinuousLegend;
|
|
419
|
-
if (ptLegend) {
|
|
420
|
-
const isDark = layout.theme.isDark;
|
|
421
|
-
const padX = 10;
|
|
422
|
-
const padY = 8;
|
|
423
|
-
|
|
424
|
-
// Compute content width from entries so the background wraps tightly
|
|
425
|
-
const swatchSize =
|
|
426
|
-
'swatchSize' in ptLegend ? (ptLegend as { swatchSize: number }).swatchSize : 10;
|
|
427
|
-
const swatchGap = 'swatchGap' in ptLegend ? (ptLegend as { swatchGap: number }).swatchGap : 6;
|
|
428
|
-
const entryGap = 'entryGap' in ptLegend ? (ptLegend as { entryGap: number }).entryGap : 16;
|
|
429
|
-
let contentWidth = ptLegend.bounds.width;
|
|
430
|
-
if ('entries' in ptLegend && ptLegend.entries) {
|
|
431
|
-
const cat = ptLegend as typeof layout.pointCategoricalLegend & {
|
|
432
|
-
entries: Array<{ label: string }>;
|
|
433
|
-
};
|
|
434
|
-
if (cat && cat.entries.length > 0) {
|
|
435
|
-
const fontSize = ptLegend.labelStyle.fontSize;
|
|
436
|
-
let w = 0;
|
|
437
|
-
for (const entry of cat.entries) {
|
|
438
|
-
w += swatchSize + swatchGap + entry.label.length * fontSize * 0.6 + entryGap;
|
|
439
|
-
}
|
|
440
|
-
w -= entryGap;
|
|
441
|
-
contentWidth = Math.min(w, ptLegend.bounds.width);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Center the background rect vertically around the swatch content.
|
|
446
|
-
// Overlay continuous legends ('top-left') float over map geography, so
|
|
447
|
-
// their backdrop must cover the full bounds (tick labels hang below the
|
|
448
|
-
// bar) instead of just the swatch row. Categorical rows keep the
|
|
449
|
-
// swatch-centered backdrop in both positions.
|
|
450
|
-
const isOverlayContinuous =
|
|
451
|
-
ptLegend.position === 'top-left' && !('entries' in ptLegend && ptLegend.entries);
|
|
452
|
-
const contentCenterY = ptLegend.bounds.y + swatchSize / 2;
|
|
453
|
-
const bgHeight = isOverlayContinuous
|
|
454
|
-
? ptLegend.bounds.height + padY * 2
|
|
455
|
-
: swatchSize + padY * 2;
|
|
456
|
-
const bgY = isOverlayContinuous ? ptLegend.bounds.y - padY : contentCenterY - bgHeight / 2;
|
|
457
|
-
const bg = createSVGElement('rect');
|
|
458
|
-
setAttrs(bg, {
|
|
459
|
-
x: ptLegend.bounds.x - padX,
|
|
460
|
-
y: bgY,
|
|
461
|
-
width: contentWidth + padX * 2,
|
|
462
|
-
height: bgHeight,
|
|
463
|
-
rx: 4,
|
|
464
|
-
fill: isDark ? 'rgba(24,24,27,0.9)' : 'rgba(255,255,255,0.9)',
|
|
465
|
-
});
|
|
466
|
-
svg.appendChild(bg);
|
|
467
|
-
renderLegend(svg, ptLegend);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Render watermark
|
|
471
|
-
if (watermark) {
|
|
472
|
-
renderWatermark(svg, layout);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return svg;
|
|
476
|
-
}
|
package/src/map-transition.ts
DELETED
|
@@ -1,99 +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
|
-
const points = svg.querySelectorAll('.oc-map-point[data-point-key]');
|
|
23
|
-
for (const p of points) {
|
|
24
|
-
const key = `pt:${p.getAttribute('data-point-key')}`;
|
|
25
|
-
const fill = p.getAttribute('fill');
|
|
26
|
-
if (fill) fills.set(key, fill);
|
|
27
|
-
}
|
|
28
|
-
return fills;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function runMapFillTransition(
|
|
32
|
-
svg: SVGElement,
|
|
33
|
-
prevFills: Map<string, string>,
|
|
34
|
-
opts: { duration: number },
|
|
35
|
-
): { cancel: () => void } {
|
|
36
|
-
const tweens: Array<{ el: Element; from: string; to: string; interp: (t: number) => string }> =
|
|
37
|
-
[];
|
|
38
|
-
|
|
39
|
-
const features = svg.querySelectorAll('.oc-map-feature[data-key]');
|
|
40
|
-
for (const p of features) {
|
|
41
|
-
const key = p.getAttribute('data-key');
|
|
42
|
-
const to = p.getAttribute('fill');
|
|
43
|
-
const from = key ? prevFills.get(key) : null;
|
|
44
|
-
if (from && to && from !== to) {
|
|
45
|
-
tweens.push({ el: p, from, to, interp: interpolateRgb(from, to) });
|
|
46
|
-
p.setAttribute('fill', from);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const points = svg.querySelectorAll('.oc-map-point[data-point-key]');
|
|
51
|
-
for (const p of points) {
|
|
52
|
-
const key = `pt:${p.getAttribute('data-point-key')}`;
|
|
53
|
-
const to = p.getAttribute('fill');
|
|
54
|
-
const from = prevFills.get(key);
|
|
55
|
-
if (from && to && from !== to) {
|
|
56
|
-
tweens.push({ el: p, from, to, interp: interpolateRgb(from, to) });
|
|
57
|
-
p.setAttribute('fill', from);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (tweens.length === 0 || opts.duration <= 0) {
|
|
62
|
-
for (const tw of tweens) tw.el.setAttribute('fill', tw.to);
|
|
63
|
-
return { cancel: () => {} };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const duration = opts.duration;
|
|
67
|
-
let startTime: number | null = null;
|
|
68
|
-
let rafId = 0;
|
|
69
|
-
let cancelled = false;
|
|
70
|
-
|
|
71
|
-
function tick(now: number) {
|
|
72
|
-
if (cancelled) return;
|
|
73
|
-
if (startTime === null) startTime = now;
|
|
74
|
-
const elapsed = now - startTime;
|
|
75
|
-
const t = Math.min(1, elapsed / duration);
|
|
76
|
-
const eased = easingFns.easeInOutCubic(t);
|
|
77
|
-
|
|
78
|
-
for (const tw of tweens) {
|
|
79
|
-
tw.el.setAttribute('fill', tw.interp(eased));
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (t < 1) {
|
|
83
|
-
rafId = requestAnimationFrame(tick);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
rafId = requestAnimationFrame(tick);
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
cancel() {
|
|
91
|
-
cancelled = true;
|
|
92
|
-
cancelAnimationFrame(rafId);
|
|
93
|
-
// Snap to final state
|
|
94
|
-
for (const tw of tweens) {
|
|
95
|
-
tw.el.setAttribute('fill', tw.to);
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|