@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/theme-tokens.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generate CSS custom properties from a resolved JS theme.
|
|
3
|
-
*
|
|
4
|
-
* Stamped on the .oc-root container at mount time so CSS consumers
|
|
5
|
-
* read from the same source of truth as the JS engine. The static
|
|
6
|
-
* values in tokens.css / dark.css serve as fallback defaults for
|
|
7
|
-
* contexts where JS hasn't mounted (SSR, static HTML).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { ResolvedTheme } from '@opendata-ai/openchart-core';
|
|
11
|
-
import { adaptForLightLineStroke, cssTokenDefault } from '@opendata-ai/openchart-core';
|
|
12
|
-
|
|
13
|
-
export function stampThemeProperties(el: HTMLElement, theme: ResolvedTheme): void {
|
|
14
|
-
const accent = theme.colors.categorical[0] ?? cssTokenDefault('--oc-accent', 'light');
|
|
15
|
-
const bg =
|
|
16
|
-
theme.colors.background === 'transparent'
|
|
17
|
-
? theme.isDark
|
|
18
|
-
? cssTokenDefault('--oc-bg', 'dark')
|
|
19
|
-
: cssTokenDefault('--oc-bg', 'light')
|
|
20
|
-
: theme.colors.background;
|
|
21
|
-
|
|
22
|
-
const props: [string, string][] = [
|
|
23
|
-
['--oc-font-family', theme.fonts.family],
|
|
24
|
-
['--oc-font-mono', theme.fonts.mono],
|
|
25
|
-
['--oc-title-size', `${theme.chrome.title.fontSize}px`],
|
|
26
|
-
['--oc-title-weight', `${theme.chrome.title.fontWeight}`],
|
|
27
|
-
['--oc-subtitle-size', `${theme.chrome.subtitle.fontSize}px`],
|
|
28
|
-
['--oc-subtitle-weight', `${theme.chrome.subtitle.fontWeight}`],
|
|
29
|
-
['--oc-source-size', `${theme.chrome.source.fontSize}px`],
|
|
30
|
-
['--oc-source-weight', `${theme.chrome.source.fontWeight}`],
|
|
31
|
-
['--oc-body-size', `${theme.fonts.sizes.body}px`],
|
|
32
|
-
['--oc-eyebrow-size', `${theme.chrome.eyebrow.fontSize}px`],
|
|
33
|
-
['--oc-eyebrow-weight', `${theme.chrome.eyebrow.fontWeight}`],
|
|
34
|
-
['--oc-bg', bg],
|
|
35
|
-
['--oc-text', theme.colors.text],
|
|
36
|
-
['--oc-text-muted', theme.colors.axis],
|
|
37
|
-
['--oc-gridline', theme.colors.gridline],
|
|
38
|
-
['--oc-axis', theme.colors.axis],
|
|
39
|
-
['--oc-border-radius', `${theme.borderRadius}px`],
|
|
40
|
-
['--oc-accent', accent],
|
|
41
|
-
['--oc-accent-strong', adaptForLightLineStroke(accent)],
|
|
42
|
-
['--oc-positive', theme.colors.positive],
|
|
43
|
-
['--oc-negative', theme.colors.negative],
|
|
44
|
-
['--oc-space-2', `${theme.spacing.chromeGap * 2}px`],
|
|
45
|
-
['--oc-space-4', `${theme.spacing.padding}px`],
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
for (const [name, value] of props) {
|
|
49
|
-
el.style.setProperty(name, value);
|
|
50
|
-
}
|
|
51
|
-
}
|
package/src/tilemap-mount.ts
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TileMap mount API: the main entry point for vanilla JS tilemap usage.
|
|
3
|
-
*
|
|
4
|
-
* createTileMap() takes a container, TileMapSpec, and options, compiles the
|
|
5
|
-
* tilemap, renders it as SVG, sets up responsive resizing, tooltip interaction,
|
|
6
|
-
* and returns a TileMapInstance with update/resize/export/destroy.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type {
|
|
10
|
-
CompileOptions,
|
|
11
|
-
DarkMode,
|
|
12
|
-
ThemeConfig,
|
|
13
|
-
TileMapLayout,
|
|
14
|
-
TileMapSpec,
|
|
15
|
-
} from '@opendata-ai/openchart-core';
|
|
16
|
-
import { BREAKPOINT_COMPACT_MAX, BREAKPOINT_MEDIUM_MAX } from '@opendata-ai/openchart-core';
|
|
17
|
-
import { compileTileMap } from '@opendata-ai/openchart-engine';
|
|
18
|
-
import { cancelAnimations, setupAnimationCleanup } from './animation';
|
|
19
|
-
import {
|
|
20
|
-
exportJPG,
|
|
21
|
-
exportPNG,
|
|
22
|
-
exportSVG,
|
|
23
|
-
exportSVGWithFonts,
|
|
24
|
-
type JPGExportOptions,
|
|
25
|
-
type SVGExportOptions,
|
|
26
|
-
} from './export';
|
|
27
|
-
import { createMeasureText, resolveFontFamily, scheduleFontReload } from './measure-text';
|
|
28
|
-
import { observeResize } from './resize-observer';
|
|
29
|
-
import { renderTileMapSVG } from './tilemap-renderer';
|
|
30
|
-
import { createTooltipManager, type TooltipManager } from './tooltip';
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Responsive tile sizing
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
// The state grid is 8 rows tall (see engine/tilemap/layout.ts GRID_ROWS).
|
|
37
|
-
// TILEMAP_TILE_GAP must match the gap passed to computeTilePositions in
|
|
38
|
-
// compile-tilemap.ts (currently 5) so the mount's height budget matches the
|
|
39
|
-
// grid the compiler builds.
|
|
40
|
-
const TILEMAP_GRID_ROWS = 8;
|
|
41
|
-
const TILEMAP_TILE_GAP = 5;
|
|
42
|
-
// Generous vertical reserve for chrome (title/subtitle/source) + padding +
|
|
43
|
-
// legend. Intentionally over-reserved: on narrow widths we WANT tile size to be
|
|
44
|
-
// bound by width, so any excess height budget just lets the map grow taller
|
|
45
|
-
// (which is the desired mobile behavior) rather than starving the tiles.
|
|
46
|
-
const TILEMAP_CHROME_RESERVE = 180;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Target tile size (px) for a given container width. Drives the height budget
|
|
50
|
-
* on non-desktop widths so the 8-row grid isn't starved to tiny tiles after
|
|
51
|
-
* chrome is subtracted from a square drawing area. Width still caps the actual
|
|
52
|
-
* tile size, so these are ceilings the layout grows toward, not guarantees.
|
|
53
|
-
*/
|
|
54
|
-
function pickTargetTileSize(width: number): number {
|
|
55
|
-
if (width < BREAKPOINT_COMPACT_MAX) return 34; // compact / mobile
|
|
56
|
-
return 40; // medium (tablet, sidebars)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Types
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
export interface TileMapMountOptions {
|
|
64
|
-
/** Theme overrides. */
|
|
65
|
-
theme?: ThemeConfig;
|
|
66
|
-
/** Dark mode setting: "auto" (system pref), "force", or "off". */
|
|
67
|
-
darkMode?: DarkMode;
|
|
68
|
-
/** Enable responsive resizing. Defaults to true. */
|
|
69
|
-
responsive?: boolean;
|
|
70
|
-
/** Show the tryOpenData.ai watermark. Defaults to true. */
|
|
71
|
-
watermark?: boolean;
|
|
72
|
-
/** Show tooltips on hover. Defaults to true. */
|
|
73
|
-
tooltip?: boolean;
|
|
74
|
-
/** Callback when a tile is clicked. */
|
|
75
|
-
onTileClick?: (tile: {
|
|
76
|
-
stateCode: string;
|
|
77
|
-
stateName: string;
|
|
78
|
-
value: number | null;
|
|
79
|
-
data: Record<string, unknown>;
|
|
80
|
-
}) => void;
|
|
81
|
-
/** Callback when a tile is hovered (null on mouse leave). */
|
|
82
|
-
onTileHover?: (
|
|
83
|
-
tile: {
|
|
84
|
-
stateCode: string;
|
|
85
|
-
stateName: string;
|
|
86
|
-
value: number | null;
|
|
87
|
-
data: Record<string, unknown>;
|
|
88
|
-
} | null,
|
|
89
|
-
) => void;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface TileMapInstance {
|
|
93
|
-
/** Re-compile and re-render with a new spec. */
|
|
94
|
-
update(spec: TileMapSpec): void;
|
|
95
|
-
/** Re-compile at current container dimensions. */
|
|
96
|
-
resize(): void;
|
|
97
|
-
/** Export the tilemap. */
|
|
98
|
-
export(
|
|
99
|
-
format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
|
|
100
|
-
options?: JPGExportOptions | SVGExportOptions,
|
|
101
|
-
): string | Promise<Blob> | Promise<string>;
|
|
102
|
-
/** Remove all DOM elements and disconnect observers. */
|
|
103
|
-
destroy(): void;
|
|
104
|
-
/** The current compiled layout. */
|
|
105
|
-
readonly layout: TileMapLayout;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
// Dark mode resolution
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
|
-
|
|
112
|
-
function resolveDarkMode(mode?: DarkMode): boolean {
|
|
113
|
-
if (mode === 'force') return true;
|
|
114
|
-
if (mode === 'off' || mode === undefined) return false;
|
|
115
|
-
if (typeof window !== 'undefined' && window.matchMedia) {
|
|
116
|
-
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// Main API
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Create a tilemap instance from a spec and mount it into a container.
|
|
127
|
-
*/
|
|
128
|
-
export function createTileMap(
|
|
129
|
-
container: HTMLElement,
|
|
130
|
-
spec: TileMapSpec,
|
|
131
|
-
options?: TileMapMountOptions,
|
|
132
|
-
): TileMapInstance {
|
|
133
|
-
let currentSpec = spec;
|
|
134
|
-
let currentLayout: TileMapLayout;
|
|
135
|
-
let destroyed = false;
|
|
136
|
-
|
|
137
|
-
// DOM
|
|
138
|
-
let svgElement: SVGSVGElement | null = null;
|
|
139
|
-
|
|
140
|
-
// Subsystems
|
|
141
|
-
let tooltipManager: TooltipManager | null = null;
|
|
142
|
-
let cleanupTooltipEvents: (() => void) | null = null;
|
|
143
|
-
let disconnectResize: (() => void) | null = null;
|
|
144
|
-
|
|
145
|
-
// Animation state
|
|
146
|
-
let animationCleanup: (() => void) | null = null;
|
|
147
|
-
let pendingResize = false;
|
|
148
|
-
|
|
149
|
-
// Latches whether the host constrains our height. Measured once, on the
|
|
150
|
-
// first render that produces a real rect: height 0 with a real width means
|
|
151
|
-
// the host is auto-height and we pick our own budget. Any other height is
|
|
152
|
-
// an explicit box we must fit inside. Re-reading this after the first paint
|
|
153
|
-
// would just measure our own SVG, so the value has to latch.
|
|
154
|
-
let isAutoHeight: boolean | null = null;
|
|
155
|
-
|
|
156
|
-
// Set when webfonts have loaded and a recompile is owed. The next render()
|
|
157
|
-
// that recompiles flips data-oc-fonts-state to 'ready' and clears this, so
|
|
158
|
-
// the attribute stays honest when resize() defers to pendingResize during
|
|
159
|
-
// the entrance animation.
|
|
160
|
-
let fontsReloadPending = false;
|
|
161
|
-
|
|
162
|
-
// Apply the root class up front so getComputedStyle sees --oc-font-family
|
|
163
|
-
// before the text measurer is built.
|
|
164
|
-
container.classList.add('oc-tilemap-root');
|
|
165
|
-
|
|
166
|
-
// Resolve the effective font the way compile() will: compile merges
|
|
167
|
-
// { ...spec.theme, ...options.theme }, so options.theme wins over the
|
|
168
|
-
// spec-level theme; fall back to the container's computed font. Measuring a
|
|
169
|
-
// different font than gets rendered desyncs layout metrics and the reload watcher.
|
|
170
|
-
function resolveEffectiveFont(): string {
|
|
171
|
-
return (
|
|
172
|
-
options?.theme?.fonts?.family ??
|
|
173
|
-
currentSpec.theme?.fonts?.family ??
|
|
174
|
-
resolveFontFamily(container)
|
|
175
|
-
);
|
|
176
|
-
}
|
|
177
|
-
let fontFamily = resolveEffectiveFont();
|
|
178
|
-
let measureText = createMeasureText(fontFamily);
|
|
179
|
-
let renderGen = 0;
|
|
180
|
-
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Helpers
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
|
|
185
|
-
function getContainerDimensions(): { width: number; height: number } {
|
|
186
|
-
const rect = container.getBoundingClientRect();
|
|
187
|
-
const width = Math.max(rect.width || 600, 100);
|
|
188
|
-
|
|
189
|
-
if (isAutoHeight === null && (rect.width > 0 || rect.height > 0)) {
|
|
190
|
-
isAutoHeight = rect.height === 0 && rect.width > 0;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// An explicit host height is a hard budget: the compiler derives a tight
|
|
194
|
-
// content height from whatever we pass, so handing it the container's own
|
|
195
|
-
// height keeps the SVG inside its box instead of overflowing onto whatever
|
|
196
|
-
// sits below it.
|
|
197
|
-
if (isAutoHeight === false && rect.height > 0) {
|
|
198
|
-
return { width, height: Math.max(rect.height, 100) };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Auto-height host: pick our own budget. The compiler derives its own tight
|
|
202
|
-
// content height and treats the passed height as a cap. On desktop, a square
|
|
203
|
-
// budget (height = width) keeps tiles from ballooning (the 8-row grid becomes
|
|
204
|
-
// the binding constraint at ~width/8). Below desktop, a square budget starves
|
|
205
|
-
// the grid after chrome, so give it a height budget sized to a comfortable
|
|
206
|
-
// target tile instead — the map then grows taller as needed rather than
|
|
207
|
-
// shrinking the tiles.
|
|
208
|
-
if (width >= BREAKPOINT_MEDIUM_MAX) {
|
|
209
|
-
return { width, height: width };
|
|
210
|
-
}
|
|
211
|
-
const targetTile = pickTargetTileSize(width);
|
|
212
|
-
const height =
|
|
213
|
-
targetTile * TILEMAP_GRID_ROWS +
|
|
214
|
-
TILEMAP_TILE_GAP * (TILEMAP_GRID_ROWS - 1) +
|
|
215
|
-
TILEMAP_CHROME_RESERVE;
|
|
216
|
-
return { width, height };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function compile(): TileMapLayout {
|
|
220
|
-
const { width, height } = getContainerDimensions();
|
|
221
|
-
const darkMode = resolveDarkMode(options?.darkMode);
|
|
222
|
-
|
|
223
|
-
const compileOpts: CompileOptions = {
|
|
224
|
-
width,
|
|
225
|
-
height,
|
|
226
|
-
theme: options?.theme,
|
|
227
|
-
darkMode,
|
|
228
|
-
watermark: options?.watermark,
|
|
229
|
-
measureText,
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
return compileTileMap(currentSpec, compileOpts);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
// Tooltip and interaction wiring
|
|
237
|
-
// ---------------------------------------------------------------------------
|
|
238
|
-
|
|
239
|
-
function wireTooltipAndInteraction(svg: SVGSVGElement, layout: TileMapLayout): () => void {
|
|
240
|
-
const cleanups: Array<() => void> = [];
|
|
241
|
-
|
|
242
|
-
// Wire tooltip on tile elements
|
|
243
|
-
const tileElements = svg.querySelectorAll('.oc-tilemap-tile');
|
|
244
|
-
for (const el of tileElements) {
|
|
245
|
-
const stateCode = el.getAttribute('data-state');
|
|
246
|
-
if (!stateCode) continue;
|
|
247
|
-
|
|
248
|
-
const content = layout.tooltipDescriptors.get(stateCode);
|
|
249
|
-
const tile = layout.tiles.find((t) => t.stateCode === stateCode);
|
|
250
|
-
|
|
251
|
-
const handleMouseEnter = (e: Event) => {
|
|
252
|
-
const mouseEvent = e as MouseEvent;
|
|
253
|
-
if (content && tooltipManager && options?.tooltip !== false) {
|
|
254
|
-
const svgRect = svg.getBoundingClientRect();
|
|
255
|
-
const x = mouseEvent.clientX - svgRect.left;
|
|
256
|
-
const y = mouseEvent.clientY - svgRect.top;
|
|
257
|
-
tooltipManager.show(content, x, y);
|
|
258
|
-
}
|
|
259
|
-
if (tile) {
|
|
260
|
-
options?.onTileHover?.({
|
|
261
|
-
stateCode: tile.stateCode,
|
|
262
|
-
stateName: tile.data.stateName as string,
|
|
263
|
-
value: tile.value,
|
|
264
|
-
data: tile.data,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const handleMouseMove = (e: Event) => {
|
|
270
|
-
if (content && tooltipManager && options?.tooltip !== false) {
|
|
271
|
-
const mouseEvent = e as MouseEvent;
|
|
272
|
-
const svgRect = svg.getBoundingClientRect();
|
|
273
|
-
const x = mouseEvent.clientX - svgRect.left;
|
|
274
|
-
const y = mouseEvent.clientY - svgRect.top;
|
|
275
|
-
tooltipManager.show(content, x, y);
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const handleMouseLeave = () => {
|
|
280
|
-
tooltipManager?.hide();
|
|
281
|
-
options?.onTileHover?.(null);
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const handleClick = () => {
|
|
285
|
-
if (tile) {
|
|
286
|
-
options?.onTileClick?.({
|
|
287
|
-
stateCode: tile.stateCode,
|
|
288
|
-
stateName: tile.data.stateName as string,
|
|
289
|
-
value: tile.value,
|
|
290
|
-
data: tile.data,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
el.addEventListener('mouseenter', handleMouseEnter);
|
|
296
|
-
el.addEventListener('mousemove', handleMouseMove);
|
|
297
|
-
el.addEventListener('mouseleave', handleMouseLeave);
|
|
298
|
-
el.addEventListener('click', handleClick);
|
|
299
|
-
|
|
300
|
-
cleanups.push(() => {
|
|
301
|
-
el.removeEventListener('mouseenter', handleMouseEnter);
|
|
302
|
-
el.removeEventListener('mousemove', handleMouseMove);
|
|
303
|
-
el.removeEventListener('mouseleave', handleMouseLeave);
|
|
304
|
-
el.removeEventListener('click', handleClick);
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return () => {
|
|
309
|
-
for (const cleanup of cleanups) {
|
|
310
|
-
cleanup();
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// ---------------------------------------------------------------------------
|
|
316
|
-
// Render + update
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
|
|
319
|
-
function render(animate = false): void {
|
|
320
|
-
// Cancel running animations
|
|
321
|
-
if (animationCleanup) {
|
|
322
|
-
animationCleanup();
|
|
323
|
-
animationCleanup = null;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Remove old SVG
|
|
327
|
-
if (svgElement) {
|
|
328
|
-
if (cleanupTooltipEvents) {
|
|
329
|
-
cleanupTooltipEvents();
|
|
330
|
-
cleanupTooltipEvents = null;
|
|
331
|
-
}
|
|
332
|
-
svgElement.remove();
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// Render new SVG
|
|
336
|
-
const newSvg = renderTileMapSVG(currentLayout, { animate });
|
|
337
|
-
container.appendChild(newSvg);
|
|
338
|
-
svgElement = newSvg;
|
|
339
|
-
|
|
340
|
-
// Wire interactions
|
|
341
|
-
cleanupTooltipEvents = wireTooltipAndInteraction(newSvg, currentLayout);
|
|
342
|
-
|
|
343
|
-
// Setup tooltips if enabled
|
|
344
|
-
if (options?.tooltip !== false) {
|
|
345
|
-
if (!tooltipManager) {
|
|
346
|
-
tooltipManager = createTooltipManager(container);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Setup animation cleanup only when actually animating
|
|
351
|
-
if (animate && currentLayout.animation?.enter) {
|
|
352
|
-
animationCleanup = setupAnimationCleanup(newSvg, () => {
|
|
353
|
-
// On animation complete, check if resize was pending
|
|
354
|
-
if (pendingResize && !destroyed) {
|
|
355
|
-
pendingResize = false;
|
|
356
|
-
resize();
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
renderGen += 1;
|
|
362
|
-
container.dataset.ocRenderGen = String(renderGen);
|
|
363
|
-
|
|
364
|
-
// This render recompiled with the loaded webfonts; publish 'ready' now
|
|
365
|
-
// rather than right after the fonts-ready resize() (which may have deferred
|
|
366
|
-
// to pendingResize during the entrance animation).
|
|
367
|
-
if (fontsReloadPending) {
|
|
368
|
-
fontsReloadPending = false;
|
|
369
|
-
container.dataset.ocFontsState = 'ready';
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function update(newSpec: TileMapSpec): void {
|
|
374
|
-
currentSpec = newSpec;
|
|
375
|
-
// A new spec can change theme.fonts.family; rebuild the measurer so layout
|
|
376
|
-
// measures the font compile will actually render with.
|
|
377
|
-
const nextFont = resolveEffectiveFont();
|
|
378
|
-
if (nextFont !== fontFamily) {
|
|
379
|
-
fontFamily = nextFont;
|
|
380
|
-
measureText = createMeasureText(fontFamily);
|
|
381
|
-
}
|
|
382
|
-
currentLayout = compile();
|
|
383
|
-
render();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function resize(): void {
|
|
387
|
-
if (destroyed) return;
|
|
388
|
-
|
|
389
|
-
// If animation is running, queue the resize for after it completes
|
|
390
|
-
if (animationCleanup) {
|
|
391
|
-
pendingResize = true;
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
currentLayout = compile();
|
|
396
|
-
render();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
// ---------------------------------------------------------------------------
|
|
400
|
-
// Export
|
|
401
|
-
// ---------------------------------------------------------------------------
|
|
402
|
-
|
|
403
|
-
function exportChart(
|
|
404
|
-
format: 'svg' | 'svg-with-fonts' | 'png' | 'jpg',
|
|
405
|
-
options_?: JPGExportOptions | SVGExportOptions,
|
|
406
|
-
): string | Promise<Blob> | Promise<string> {
|
|
407
|
-
if (!svgElement) return '';
|
|
408
|
-
|
|
409
|
-
switch (format) {
|
|
410
|
-
case 'svg':
|
|
411
|
-
return exportSVG(svgElement);
|
|
412
|
-
case 'svg-with-fonts':
|
|
413
|
-
return exportSVGWithFonts(svgElement);
|
|
414
|
-
case 'png':
|
|
415
|
-
return exportPNG(svgElement, options_ as JPGExportOptions);
|
|
416
|
-
case 'jpg':
|
|
417
|
-
return exportJPG(svgElement, options_ as JPGExportOptions);
|
|
418
|
-
default:
|
|
419
|
-
return '';
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// ---------------------------------------------------------------------------
|
|
424
|
-
// Destroy
|
|
425
|
-
// ---------------------------------------------------------------------------
|
|
426
|
-
|
|
427
|
-
function destroy(): void {
|
|
428
|
-
if (destroyed) return;
|
|
429
|
-
destroyed = true;
|
|
430
|
-
|
|
431
|
-
// Cancel running animations
|
|
432
|
-
if (animationCleanup) {
|
|
433
|
-
cancelAnimations(svgElement);
|
|
434
|
-
animationCleanup();
|
|
435
|
-
animationCleanup = null;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// Clean up events
|
|
439
|
-
if (cleanupTooltipEvents) {
|
|
440
|
-
cleanupTooltipEvents();
|
|
441
|
-
cleanupTooltipEvents = null;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Remove SVG
|
|
445
|
-
if (svgElement) {
|
|
446
|
-
svgElement.remove();
|
|
447
|
-
svgElement = null;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// Unmount tooltip manager
|
|
451
|
-
if (tooltipManager) {
|
|
452
|
-
tooltipManager.destroy();
|
|
453
|
-
tooltipManager = null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Disconnect resize observer
|
|
457
|
-
if (disconnectResize) {
|
|
458
|
-
disconnectResize();
|
|
459
|
-
disconnectResize = null;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
// Remove root classes
|
|
463
|
-
container.classList.remove('oc-tilemap-root', 'oc-dark');
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ---------------------------------------------------------------------------
|
|
467
|
-
// Initialize
|
|
468
|
-
// ---------------------------------------------------------------------------
|
|
469
|
-
|
|
470
|
-
// Root class was applied before the measurer was built (see above); dark
|
|
471
|
-
// mode class still applies here.
|
|
472
|
-
if (resolveDarkMode(options?.darkMode)) {
|
|
473
|
-
container.classList.add('oc-dark');
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Initial compile and render (animate on first mount)
|
|
477
|
-
currentLayout = compile();
|
|
478
|
-
render(true);
|
|
479
|
-
|
|
480
|
-
// Recompile once after webfonts load so labels aren't stuck measured
|
|
481
|
-
// against fallback metrics on real devices.
|
|
482
|
-
const fontsPending = scheduleFontReload(
|
|
483
|
-
fontFamily,
|
|
484
|
-
() => !destroyed,
|
|
485
|
-
() => {
|
|
486
|
-
// Mark the reload owed, then recompile. render() flips the attribute to
|
|
487
|
-
// 'ready' once it actually recompiles, including the pendingResize replay
|
|
488
|
-
// after the entrance animation finishes.
|
|
489
|
-
fontsReloadPending = true;
|
|
490
|
-
resize();
|
|
491
|
-
},
|
|
492
|
-
);
|
|
493
|
-
container.dataset.ocFontsState = fontsPending ? 'pending' : 'ready';
|
|
494
|
-
|
|
495
|
-
// Setup responsive resizing
|
|
496
|
-
if (options?.responsive !== false) {
|
|
497
|
-
disconnectResize = observeResize(container, () => {
|
|
498
|
-
resize();
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
update,
|
|
504
|
-
resize,
|
|
505
|
-
export: exportChart,
|
|
506
|
-
destroy,
|
|
507
|
-
get layout(): TileMapLayout {
|
|
508
|
-
return currentLayout;
|
|
509
|
-
},
|
|
510
|
-
};
|
|
511
|
-
}
|