@opendata-ai/openchart-vanilla 7.7.0 → 7.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/static.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ThemeConfig, DarkMode, ChartSpec, LayerSpec } from '@opendata-ai/openchart-core';
1
+ import { ThemeConfig, DarkMode, ChartSpec, LayerSpec, TileMapSpec } from '@opendata-ai/openchart-core';
2
2
 
3
3
  interface StaticRenderOptions {
4
4
  width?: number;
@@ -24,6 +24,6 @@ interface StaticRenderOptions {
24
24
  * The entire render pipeline is synchronous; the global swap is safe as long
25
25
  * as no code schedules microtasks that outlive the call.
26
26
  */
27
- declare function renderStaticSVG(spec: ChartSpec | LayerSpec, options?: StaticRenderOptions): string;
27
+ declare function renderStaticSVG(spec: ChartSpec | LayerSpec | TileMapSpec, options?: StaticRenderOptions): string;
28
28
 
29
29
  export { type StaticRenderOptions, renderStaticSVG };
package/dist/static.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  SVG_NS,
3
3
  renderChartSVG,
4
+ renderTileMapSVG,
4
5
  resetSvgIdCounter
5
- } from "./chunk-PSMDJEXK.js";
6
+ } from "./chunk-KYRQV6KR.js";
6
7
 
7
8
  // src/static.ts
8
9
  import { createRequire } from "module";
9
- import { adaptForLightLineStroke, isLayerSpec } from "@opendata-ai/openchart-core";
10
- import { compileChart, compileLayer } from "@opendata-ai/openchart-engine";
10
+ import { adaptForLightLineStroke, isLayerSpec, isTileMapSpec } from "@opendata-ai/openchart-core";
11
+ import { compileChart, compileLayer, compileTileMap } from "@opendata-ai/openchart-engine";
11
12
  var esmRequire = createRequire(import.meta.url);
12
13
  var cachedWindow;
13
14
  function getHappyDomWindow() {
@@ -120,31 +121,40 @@ function renderStaticSVG(spec, options) {
120
121
  darkMode,
121
122
  watermark: options?.watermark
122
123
  };
123
- let layout;
124
- if (isLayerSpec(spec)) {
125
- layout = compileLayer(spec, compileOpts);
124
+ let svg;
125
+ let themeForStyle;
126
+ if (isTileMapSpec(spec)) {
127
+ const tileMapLayout = compileTileMap(spec, compileOpts);
128
+ svg = renderTileMapSVG(tileMapLayout, { animate: false });
129
+ themeForStyle = tileMapLayout.theme;
126
130
  } else {
127
- layout = compileChart(spec, compileOpts);
131
+ let layout;
132
+ if (isLayerSpec(spec)) {
133
+ layout = compileLayer(spec, compileOpts);
134
+ } else {
135
+ layout = compileChart(spec, compileOpts);
136
+ }
137
+ const container = win.document.createElement("div");
138
+ Object.defineProperty(container, "getBoundingClientRect", {
139
+ value: () => ({
140
+ width,
141
+ height,
142
+ top: 0,
143
+ left: 0,
144
+ right: width,
145
+ bottom: height,
146
+ x: 0,
147
+ y: 0,
148
+ toJSON: () => ({})
149
+ })
150
+ });
151
+ svg = renderChartSVG(layout, container, {
152
+ animate: false,
153
+ crosshair: false
154
+ });
155
+ stripInteractiveElements(svg);
156
+ themeForStyle = layout.theme;
128
157
  }
129
- const container = win.document.createElement("div");
130
- Object.defineProperty(container, "getBoundingClientRect", {
131
- value: () => ({
132
- width,
133
- height,
134
- top: 0,
135
- left: 0,
136
- right: width,
137
- bottom: height,
138
- x: 0,
139
- y: 0,
140
- toJSON: () => ({})
141
- })
142
- });
143
- const svg = renderChartSVG(layout, container, {
144
- animate: false,
145
- crosshair: false
146
- });
147
- stripInteractiveElements(svg);
148
158
  const doc = win.document;
149
159
  let defs = svg.querySelector("defs");
150
160
  if (!defs) {
@@ -152,7 +162,7 @@ function renderStaticSVG(spec, options) {
152
162
  svg.insertBefore(defs, svg.firstChild);
153
163
  }
154
164
  const styleEl = doc.createElementNS(SVG_NS, "style");
155
- styleEl.textContent = buildThemeStyleBlock(layout.theme);
165
+ styleEl.textContent = buildThemeStyleBlock(themeForStyle);
156
166
  defs.insertBefore(styleEl, defs.firstChild);
157
167
  const serializer = new win.XMLSerializer();
158
168
  return serializer.serializeToString(svg);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/static.ts"],"sourcesContent":["// Node.js only — do not import from browser code.\nimport { createRequire } from 'node:module';\nimport type {\n ChartLayout,\n ChartSpec,\n CompileOptions,\n DarkMode,\n LayerSpec,\n ResolvedTheme,\n ThemeConfig,\n} from '@opendata-ai/openchart-core';\nimport { adaptForLightLineStroke, isLayerSpec } from '@opendata-ai/openchart-core';\nimport { compileChart, compileLayer } from '@opendata-ai/openchart-engine';\nimport { SVG_NS } from './renderers/svg-dom';\nimport { resetSvgIdCounter } from './svg-ids';\nimport { renderChartSVG } from './svg-renderer';\n\nconst esmRequire = createRequire(import.meta.url);\n\nlet cachedWindow: typeof import('happy-dom').Window | undefined;\nfunction getHappyDomWindow(): typeof import('happy-dom').Window {\n if (cachedWindow) return cachedWindow;\n try {\n ({ Window: cachedWindow } = esmRequire('happy-dom') as typeof import('happy-dom'));\n } catch {\n throw new Error(\n \"renderStaticSVG requires 'happy-dom' as a peer dependency. Install it with: npm add happy-dom\",\n );\n }\n return cachedWindow;\n}\n\nlet rendering = false;\n\nexport interface StaticRenderOptions {\n width?: number;\n height?: number;\n theme?: ThemeConfig;\n /**\n * Dark mode setting. In static rendering `'auto'` resolves to light mode\n * since there is no `matchMedia` to query. Use `'force'` for dark output.\n */\n darkMode?: DarkMode;\n watermark?: boolean;\n}\n\nfunction resolveStaticDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n return false;\n}\n\nfunction buildThemeStyleBlock(theme: ResolvedTheme): string {\n const accent = theme.colors.categorical[0] ?? '#06b6d4';\n const bg =\n theme.colors.background === 'transparent'\n ? theme.isDark\n ? '#09090b'\n : '#ffffff'\n : theme.colors.background;\n\n const props = [\n `--oc-font-family: ${theme.fonts.family}`,\n `--oc-font-mono: ${theme.fonts.mono}`,\n `--oc-title-size: ${theme.chrome.title.fontSize}px`,\n `--oc-title-weight: ${theme.chrome.title.fontWeight}`,\n `--oc-title-tracking: -0.022em`, // sync with tokens.css\n `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,\n `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,\n `--oc-source-size: ${theme.chrome.source.fontSize}px`,\n `--oc-source-weight: ${theme.chrome.source.fontWeight}`,\n `--oc-body-size: ${theme.fonts.sizes.body}px`,\n `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,\n `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,\n `--oc-eyebrow-tracking: 0.08em`, // sync with tokens.css\n `--oc-bg: ${bg}`,\n `--oc-text: ${theme.colors.text}`,\n `--oc-text-muted: ${theme.colors.axis}`,\n `--oc-text-faint: ${theme.isDark ? '#52525b' : '#d4d4d8'}`,\n `--oc-gridline: ${theme.colors.gridline}`,\n `--oc-axis: ${theme.colors.axis}`,\n `--oc-border-radius: ${theme.borderRadius}px`,\n `--oc-accent: ${accent}`,\n `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,\n `--oc-positive: ${theme.colors.positive}`,\n `--oc-negative: ${theme.colors.negative}`,\n `--oc-legend-text: ${theme.isDark ? '#d0d6e0' : '#3f3f46'}`,\n `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,\n `--oc-space-4: ${theme.spacing.padding}px`,\n ];\n\n const rules = [\n `svg.oc-chart { ${props.join('; ')}; }`,\n `.oc-chrome { font-family: var(--oc-font-family); }`,\n `.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,\n `.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,\n `.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,\n `.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,\n `.oc-brand { font-size: 11px; font-weight: 510; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,\n `.oc-brand-dot { fill: var(--oc-accent); }`,\n `.oc-eyebrow-dot { fill: var(--oc-accent); }`,\n `.oc-metrics { font-family: var(--oc-font-family); }`,\n `.oc-metric-label { font-size: 10px; font-weight: 510; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--oc-text-muted); }`,\n `.oc-metric-value { font-size: 22px; font-weight: 510; letter-spacing: -0.01em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,\n `.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 510; }`,\n `.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 510; }`,\n `.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,\n `.oc-endpoint-labels { font-family: var(--oc-font-family); }`,\n `.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,\n `.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,\n `.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,\n `.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,\n `.oc-metric-secondary { fill: var(--oc-positive); font-size: 12px; font-weight: 400; }`,\n `.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,\n `.oc-legend-entry { cursor: default; }`,\n `.oc-legend text { fill: var(--oc-legend-text); }`,\n ];\n\n return rules.join('\\n');\n}\n\nfunction stripInteractiveElements(svg: Element): void {\n const selectors = ['[data-voronoi-overlay]', '[data-crosshair]', '[data-snap-dots]'];\n for (const selector of selectors) {\n const els = svg.querySelectorAll(selector);\n for (const el of els) {\n el.parentNode?.removeChild(el);\n }\n }\n}\n\n/**\n * Render a chart spec to a standalone SVG string without a browser DOM.\n *\n * Requires `happy-dom` as a peer dependency (`bun add happy-dom`).\n *\n * Not safe for concurrent invocation: the render pipeline relies on a global\n * SVG ID counter and a temporary global `document` swap, both of which are\n * single-threaded. In a server handling parallel requests, serialize calls\n * through a queue or mutex.\n *\n * The entire render pipeline is synchronous; the global swap is safe as long\n * as no code schedules microtasks that outlive the call.\n */\nexport function renderStaticSVG(\n spec: ChartSpec | LayerSpec,\n options?: StaticRenderOptions,\n): string {\n if (rendering) {\n throw new Error('renderStaticSVG is not reentrant — serialize calls through a queue or mutex');\n }\n rendering = true;\n\n const Window = getHappyDomWindow();\n const win = new Window({ url: 'about:blank' });\n\n const prevDocument = globalThis.document;\n const prevWindow = (globalThis as Record<string, unknown>).window;\n\n try {\n (globalThis as Record<string, unknown>).document = win.document;\n (globalThis as Record<string, unknown>).window = win;\n\n resetSvgIdCounter();\n\n const width = options?.width ?? 640;\n const height = options?.height ?? 420;\n const darkMode = resolveStaticDarkMode(options?.darkMode);\n\n const compileOpts: CompileOptions = {\n width,\n height,\n theme: options?.theme,\n darkMode,\n watermark: options?.watermark,\n };\n\n let layout: ChartLayout;\n if (isLayerSpec(spec)) {\n layout = compileLayer(spec, compileOpts);\n } else {\n layout = compileChart(spec, compileOpts);\n }\n\n const container = win.document.createElement('div');\n Object.defineProperty(container, 'getBoundingClientRect', {\n value: () => ({\n width,\n height,\n top: 0,\n left: 0,\n right: width,\n bottom: height,\n x: 0,\n y: 0,\n toJSON: () => ({}),\n }),\n });\n\n // renderChartSVG appends to container; the container and SVG are owned by\n // the happy-dom Window which is closed in the finally block.\n const svg = renderChartSVG(layout, container as unknown as HTMLElement, {\n animate: false,\n crosshair: false,\n });\n\n stripInteractiveElements(svg);\n\n const doc = win.document as unknown as Document;\n let defs = svg.querySelector('defs');\n if (!defs) {\n defs = doc.createElementNS(SVG_NS, 'defs');\n svg.insertBefore(defs as unknown as Node, svg.firstChild);\n }\n const styleEl = doc.createElementNS(SVG_NS, 'style');\n styleEl.textContent = buildThemeStyleBlock(layout.theme);\n defs.insertBefore(styleEl as unknown as Node, defs.firstChild);\n\n const serializer = new (\n win as unknown as { XMLSerializer: typeof XMLSerializer }\n ).XMLSerializer();\n return serializer.serializeToString(svg as unknown as Node);\n } finally {\n rendering = false;\n if (prevDocument !== undefined) {\n (globalThis as Record<string, unknown>).document = prevDocument;\n } else {\n delete (globalThis as Record<string, unknown>).document;\n }\n if (prevWindow !== undefined) {\n (globalThis as Record<string, unknown>).window = prevWindow;\n } else {\n delete (globalThis as Record<string, unknown>).window;\n }\n win.close();\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/static.ts"],"sourcesContent":["// Node.js only — do not import from browser code.\nimport { createRequire } from 'node:module';\nimport type {\n ChartLayout,\n ChartSpec,\n CompileOptions,\n DarkMode,\n LayerSpec,\n ResolvedTheme,\n ThemeConfig,\n TileMapSpec,\n} from '@opendata-ai/openchart-core';\nimport { adaptForLightLineStroke, isLayerSpec, isTileMapSpec } from '@opendata-ai/openchart-core';\nimport { compileChart, compileLayer, compileTileMap } from '@opendata-ai/openchart-engine';\nimport { SVG_NS } from './renderers/svg-dom';\nimport { resetSvgIdCounter } from './svg-ids';\nimport { renderChartSVG } from './svg-renderer';\nimport { renderTileMapSVG } from './tilemap-renderer';\n\nconst esmRequire = createRequire(import.meta.url);\n\nlet cachedWindow: typeof import('happy-dom').Window | undefined;\nfunction getHappyDomWindow(): typeof import('happy-dom').Window {\n if (cachedWindow) return cachedWindow;\n try {\n ({ Window: cachedWindow } = esmRequire('happy-dom') as typeof import('happy-dom'));\n } catch {\n throw new Error(\n \"renderStaticSVG requires 'happy-dom' as a peer dependency. Install it with: npm add happy-dom\",\n );\n }\n return cachedWindow;\n}\n\nlet rendering = false;\n\nexport interface StaticRenderOptions {\n width?: number;\n height?: number;\n theme?: ThemeConfig;\n /**\n * Dark mode setting. In static rendering `'auto'` resolves to light mode\n * since there is no `matchMedia` to query. Use `'force'` for dark output.\n */\n darkMode?: DarkMode;\n watermark?: boolean;\n}\n\nfunction resolveStaticDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n return false;\n}\n\nfunction buildThemeStyleBlock(theme: ResolvedTheme): string {\n const accent = theme.colors.categorical[0] ?? '#06b6d4';\n const bg =\n theme.colors.background === 'transparent'\n ? theme.isDark\n ? '#09090b'\n : '#ffffff'\n : theme.colors.background;\n\n const props = [\n `--oc-font-family: ${theme.fonts.family}`,\n `--oc-font-mono: ${theme.fonts.mono}`,\n `--oc-title-size: ${theme.chrome.title.fontSize}px`,\n `--oc-title-weight: ${theme.chrome.title.fontWeight}`,\n `--oc-title-tracking: -0.022em`, // sync with tokens.css\n `--oc-subtitle-size: ${theme.chrome.subtitle.fontSize}px`,\n `--oc-subtitle-weight: ${theme.chrome.subtitle.fontWeight}`,\n `--oc-source-size: ${theme.chrome.source.fontSize}px`,\n `--oc-source-weight: ${theme.chrome.source.fontWeight}`,\n `--oc-body-size: ${theme.fonts.sizes.body}px`,\n `--oc-eyebrow-size: ${theme.chrome.eyebrow.fontSize}px`,\n `--oc-eyebrow-weight: ${theme.chrome.eyebrow.fontWeight}`,\n `--oc-eyebrow-tracking: 0.08em`, // sync with tokens.css\n `--oc-bg: ${bg}`,\n `--oc-text: ${theme.colors.text}`,\n `--oc-text-muted: ${theme.colors.axis}`,\n `--oc-text-faint: ${theme.isDark ? '#52525b' : '#d4d4d8'}`,\n `--oc-gridline: ${theme.colors.gridline}`,\n `--oc-axis: ${theme.colors.axis}`,\n `--oc-border-radius: ${theme.borderRadius}px`,\n `--oc-accent: ${accent}`,\n `--oc-accent-strong: ${adaptForLightLineStroke(accent)}`,\n `--oc-positive: ${theme.colors.positive}`,\n `--oc-negative: ${theme.colors.negative}`,\n `--oc-legend-text: ${theme.isDark ? '#d0d6e0' : '#3f3f46'}`,\n `--oc-space-2: ${theme.spacing.chromeGap * 2}px`,\n `--oc-space-4: ${theme.spacing.padding}px`,\n ];\n\n const rules = [\n `svg.oc-chart { ${props.join('; ')}; }`,\n `.oc-chrome { font-family: var(--oc-font-family); }`,\n `.oc-eyebrow { font-size: var(--oc-eyebrow-size); font-weight: var(--oc-eyebrow-weight); letter-spacing: var(--oc-eyebrow-tracking); text-transform: uppercase; fill: var(--oc-accent); }`,\n `.oc-title { font-size: var(--oc-title-size); font-weight: var(--oc-title-weight); letter-spacing: var(--oc-title-tracking); fill: var(--oc-text); }`,\n `.oc-subtitle { font-size: var(--oc-subtitle-size); font-weight: var(--oc-subtitle-weight); fill: var(--oc-text-muted); }`,\n `.oc-source, .oc-byline, .oc-footer { font-size: var(--oc-source-size); font-weight: var(--oc-source-weight); fill: var(--oc-text-muted); }`,\n `.oc-brand { font-size: 11px; font-weight: 510; letter-spacing: 0.02em; fill: var(--oc-text-faint); }`,\n `.oc-brand-dot { fill: var(--oc-accent); }`,\n `.oc-eyebrow-dot { fill: var(--oc-accent); }`,\n `.oc-metrics { font-family: var(--oc-font-family); }`,\n `.oc-metric-label { font-size: 10px; font-weight: 510; letter-spacing: 0.08em; text-transform: uppercase; fill: var(--oc-text-muted); }`,\n `.oc-metric-value { font-size: 22px; font-weight: 510; letter-spacing: -0.01em; fill: var(--oc-text); font-variant-numeric: tabular-nums; }`,\n `.oc-metric-delta-up { fill: var(--oc-positive); font-size: 12px; font-weight: 510; }`,\n `.oc-metric-delta-down { fill: var(--oc-negative); font-size: 12px; font-weight: 510; }`,\n `.oc-axis-tick-inline { font-size: 11px; font-weight: 400; fill: var(--oc-text-muted); }`,\n `.oc-endpoint-labels { font-family: var(--oc-font-family); }`,\n `.oc-endpoint-label { fill: var(--oc-endpoint-label-color, var(--oc-text)); }`,\n `.oc-endpoint-value { fill: var(--oc-endpoint-value-color, var(--oc-text-muted)); }`,\n `.oc-endpoint-leader { stroke: var(--oc-endpoint-leader-color, currentColor); }`,\n `.oc-annotation-subtitle { fill: var(--oc-annotation-subtitle-color, var(--oc-text-muted)); }`,\n `.oc-metric-secondary { fill: var(--oc-positive); font-size: 12px; font-weight: 400; }`,\n `.oc-legend { font-family: var(--oc-font-family); font-size: var(--oc-body-size); }`,\n `.oc-legend-entry { cursor: default; }`,\n `.oc-legend text { fill: var(--oc-legend-text); }`,\n ];\n\n return rules.join('\\n');\n}\n\nfunction stripInteractiveElements(svg: Element): void {\n const selectors = ['[data-voronoi-overlay]', '[data-crosshair]', '[data-snap-dots]'];\n for (const selector of selectors) {\n const els = svg.querySelectorAll(selector);\n for (const el of els) {\n el.parentNode?.removeChild(el);\n }\n }\n}\n\n/**\n * Render a chart spec to a standalone SVG string without a browser DOM.\n *\n * Requires `happy-dom` as a peer dependency (`bun add happy-dom`).\n *\n * Not safe for concurrent invocation: the render pipeline relies on a global\n * SVG ID counter and a temporary global `document` swap, both of which are\n * single-threaded. In a server handling parallel requests, serialize calls\n * through a queue or mutex.\n *\n * The entire render pipeline is synchronous; the global swap is safe as long\n * as no code schedules microtasks that outlive the call.\n */\nexport function renderStaticSVG(\n spec: ChartSpec | LayerSpec | TileMapSpec,\n options?: StaticRenderOptions,\n): string {\n if (rendering) {\n throw new Error('renderStaticSVG is not reentrant — serialize calls through a queue or mutex');\n }\n rendering = true;\n\n const Window = getHappyDomWindow();\n const win = new Window({ url: 'about:blank' });\n\n const prevDocument = globalThis.document;\n const prevWindow = (globalThis as Record<string, unknown>).window;\n\n try {\n (globalThis as Record<string, unknown>).document = win.document;\n (globalThis as Record<string, unknown>).window = win;\n\n resetSvgIdCounter();\n\n const width = options?.width ?? 640;\n const height = options?.height ?? 420;\n const darkMode = resolveStaticDarkMode(options?.darkMode);\n\n const compileOpts: CompileOptions = {\n width,\n height,\n theme: options?.theme,\n darkMode,\n watermark: options?.watermark,\n };\n\n let svg: SVGElement;\n let themeForStyle: ResolvedTheme;\n\n if (isTileMapSpec(spec)) {\n const tileMapLayout = compileTileMap(spec, compileOpts);\n svg = renderTileMapSVG(tileMapLayout, { animate: false });\n themeForStyle = tileMapLayout.theme;\n } else {\n let layout: ChartLayout;\n if (isLayerSpec(spec)) {\n layout = compileLayer(spec, compileOpts);\n } else {\n layout = compileChart(spec, compileOpts);\n }\n\n const container = win.document.createElement('div');\n Object.defineProperty(container, 'getBoundingClientRect', {\n value: () => ({\n width,\n height,\n top: 0,\n left: 0,\n right: width,\n bottom: height,\n x: 0,\n y: 0,\n toJSON: () => ({}),\n }),\n });\n\n svg = renderChartSVG(layout, container as unknown as HTMLElement, {\n animate: false,\n crosshair: false,\n });\n\n stripInteractiveElements(svg);\n themeForStyle = layout.theme;\n }\n\n const doc = win.document as unknown as Document;\n let defs = svg.querySelector('defs');\n if (!defs) {\n defs = doc.createElementNS(SVG_NS, 'defs');\n svg.insertBefore(defs as unknown as Node, svg.firstChild);\n }\n const styleEl = doc.createElementNS(SVG_NS, 'style');\n styleEl.textContent = buildThemeStyleBlock(themeForStyle);\n defs.insertBefore(styleEl as unknown as Node, defs.firstChild);\n\n const serializer = new (\n win as unknown as { XMLSerializer: typeof XMLSerializer }\n ).XMLSerializer();\n return serializer.serializeToString(svg as unknown as Node);\n } finally {\n rendering = false;\n if (prevDocument !== undefined) {\n (globalThis as Record<string, unknown>).document = prevDocument;\n } else {\n delete (globalThis as Record<string, unknown>).document;\n }\n if (prevWindow !== undefined) {\n (globalThis as Record<string, unknown>).window = prevWindow;\n } else {\n delete (globalThis as Record<string, unknown>).window;\n }\n win.close();\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-vanilla",
3
- "version": "7.7.0",
3
+ "version": "7.8.0",
4
4
  "description": "Vanilla JS renderer for openchart: SVG charts, HTML tables, force-directed graphs",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -54,8 +54,8 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@floating-ui/dom": "^1.7.6",
57
- "@opendata-ai/openchart-core": "7.7.0",
58
- "@opendata-ai/openchart-engine": "7.7.0",
57
+ "@opendata-ai/openchart-core": "7.8.0",
58
+ "@opendata-ai/openchart-engine": "7.8.0",
59
59
  "d3-force": "^3.0.0",
60
60
  "d3-quadtree": "^3.0.1"
61
61
  },
package/src/static.ts CHANGED
@@ -8,12 +8,14 @@ import type {
8
8
  LayerSpec,
9
9
  ResolvedTheme,
10
10
  ThemeConfig,
11
+ TileMapSpec,
11
12
  } from '@opendata-ai/openchart-core';
12
- import { adaptForLightLineStroke, isLayerSpec } from '@opendata-ai/openchart-core';
13
- import { compileChart, compileLayer } from '@opendata-ai/openchart-engine';
13
+ import { adaptForLightLineStroke, isLayerSpec, isTileMapSpec } from '@opendata-ai/openchart-core';
14
+ import { compileChart, compileLayer, compileTileMap } from '@opendata-ai/openchart-engine';
14
15
  import { SVG_NS } from './renderers/svg-dom';
15
16
  import { resetSvgIdCounter } from './svg-ids';
16
17
  import { renderChartSVG } from './svg-renderer';
18
+ import { renderTileMapSVG } from './tilemap-renderer';
17
19
 
18
20
  const esmRequire = createRequire(import.meta.url);
19
21
 
@@ -142,7 +144,7 @@ function stripInteractiveElements(svg: Element): void {
142
144
  * as no code schedules microtasks that outlive the call.
143
145
  */
144
146
  export function renderStaticSVG(
145
- spec: ChartSpec | LayerSpec,
147
+ spec: ChartSpec | LayerSpec | TileMapSpec,
146
148
  options?: StaticRenderOptions,
147
149
  ): string {
148
150
  if (rendering) {
@@ -174,37 +176,45 @@ export function renderStaticSVG(
174
176
  watermark: options?.watermark,
175
177
  };
176
178
 
177
- let layout: ChartLayout;
178
- if (isLayerSpec(spec)) {
179
- layout = compileLayer(spec, compileOpts);
179
+ let svg: SVGElement;
180
+ let themeForStyle: ResolvedTheme;
181
+
182
+ if (isTileMapSpec(spec)) {
183
+ const tileMapLayout = compileTileMap(spec, compileOpts);
184
+ svg = renderTileMapSVG(tileMapLayout, { animate: false });
185
+ themeForStyle = tileMapLayout.theme;
180
186
  } else {
181
- layout = compileChart(spec, compileOpts);
187
+ let layout: ChartLayout;
188
+ if (isLayerSpec(spec)) {
189
+ layout = compileLayer(spec, compileOpts);
190
+ } else {
191
+ layout = compileChart(spec, compileOpts);
192
+ }
193
+
194
+ const container = win.document.createElement('div');
195
+ Object.defineProperty(container, 'getBoundingClientRect', {
196
+ value: () => ({
197
+ width,
198
+ height,
199
+ top: 0,
200
+ left: 0,
201
+ right: width,
202
+ bottom: height,
203
+ x: 0,
204
+ y: 0,
205
+ toJSON: () => ({}),
206
+ }),
207
+ });
208
+
209
+ svg = renderChartSVG(layout, container as unknown as HTMLElement, {
210
+ animate: false,
211
+ crosshair: false,
212
+ });
213
+
214
+ stripInteractiveElements(svg);
215
+ themeForStyle = layout.theme;
182
216
  }
183
217
 
184
- const container = win.document.createElement('div');
185
- Object.defineProperty(container, 'getBoundingClientRect', {
186
- value: () => ({
187
- width,
188
- height,
189
- top: 0,
190
- left: 0,
191
- right: width,
192
- bottom: height,
193
- x: 0,
194
- y: 0,
195
- toJSON: () => ({}),
196
- }),
197
- });
198
-
199
- // renderChartSVG appends to container; the container and SVG are owned by
200
- // the happy-dom Window which is closed in the finally block.
201
- const svg = renderChartSVG(layout, container as unknown as HTMLElement, {
202
- animate: false,
203
- crosshair: false,
204
- });
205
-
206
- stripInteractiveElements(svg);
207
-
208
218
  const doc = win.document as unknown as Document;
209
219
  let defs = svg.querySelector('defs');
210
220
  if (!defs) {
@@ -212,7 +222,7 @@ export function renderStaticSVG(
212
222
  svg.insertBefore(defs as unknown as Node, svg.firstChild);
213
223
  }
214
224
  const styleEl = doc.createElementNS(SVG_NS, 'style');
215
- styleEl.textContent = buildThemeStyleBlock(layout.theme);
225
+ styleEl.textContent = buildThemeStyleBlock(themeForStyle);
216
226
  defs.insertBefore(styleEl as unknown as Node, defs.firstChild);
217
227
 
218
228
  const serializer = new (
@@ -13,6 +13,7 @@ import type {
13
13
  } from '@opendata-ai/openchart-core';
14
14
  import { textAscent } from '@opendata-ai/openchart-core';
15
15
  import { renderLegend } from './renderers/legend';
16
+ import { nextSvgId } from './svg-ids';
16
17
 
17
18
  const SVG_NS = 'http://www.w3.org/2000/svg';
18
19
  const XLINK_NS = 'http://www.w3.org/1999/xlink';
@@ -23,8 +24,6 @@ const EASE_VAR_MAP: Record<string, string> = {
23
24
  snappy: 'var(--oc-ease-snappy)',
24
25
  };
25
26
 
26
- let gradientIdCounter = 0;
27
-
28
27
  // ---------------------------------------------------------------------------
29
28
  // Helpers
30
29
  // ---------------------------------------------------------------------------
@@ -322,7 +321,7 @@ function renderGradientLegend(parent: SVGElement, layout: TileMapLayout): void {
322
321
  parent.insertBefore(defs, parent.firstChild);
323
322
  }
324
323
 
325
- const gradientId = `oc-tilemap-legend-gradient-${gradientIdCounter++}`;
324
+ const gradientId = nextSvgId('oc-tilemap-legend-gradient');
326
325
  const grad = createSVGElement('linearGradient');
327
326
  grad.id = gradientId;
328
327
  grad.setAttribute('x1', '0%');