@opendata-ai/openchart-vanilla 8.9.0 → 8.9.1

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.
@@ -3,7 +3,7 @@ import {
3
3
  renderChromeElement,
4
4
  renderLegend,
5
5
  stampAnimationVars
6
- } from "./chunk-TP3GCWCO.js";
6
+ } from "./chunk-APMUBW72.js";
7
7
 
8
8
  // src/tilemap-renderer.ts
9
9
  var SVG_NS = "http://www.w3.org/2000/svg";
@@ -328,4 +328,4 @@ function renderTileMapSVG(layout, opts) {
328
328
  export {
329
329
  renderTileMapSVG
330
330
  };
331
- //# sourceMappingURL=chunk-YHZAH6EN.js.map
331
+ //# sourceMappingURL=chunk-Q7YEEF2X.js.map
@@ -232,51 +232,6 @@ var AnimationScheduler = class {
232
232
  }
233
233
  };
234
234
 
235
- // src/motion/tween.ts
236
- function resolveEase(ease) {
237
- return ease === "snappy" ? cubicOut : cubicInOut;
238
- }
239
- function prefersReducedMotion() {
240
- if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
241
- try {
242
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
243
- } catch {
244
- return false;
245
- }
246
- }
247
- function createTween(opts) {
248
- const { duration, ease, apply, onDone } = opts;
249
- let startTime = null;
250
- let finished = false;
251
- function settleAt(t) {
252
- apply(ease(t));
253
- }
254
- return {
255
- tick(now) {
256
- if (finished) return false;
257
- if (startTime === null) startTime = now;
258
- const elapsed = now - startTime;
259
- const raw = duration <= 0 ? 1 : Math.min(1, elapsed / duration);
260
- settleAt(raw);
261
- if (raw >= 1) {
262
- finished = true;
263
- onDone?.();
264
- return false;
265
- }
266
- return true;
267
- },
268
- finish() {
269
- if (finished) return;
270
- finished = true;
271
- settleAt(1);
272
- onDone?.();
273
- },
274
- cancel() {
275
- finished = true;
276
- }
277
- };
278
- }
279
-
280
235
  // src/spatial-index.ts
281
236
  import { quadtree } from "d3-quadtree";
282
237
  var SpatialIndex = class {
@@ -359,6 +314,51 @@ var SpatialIndex = class {
359
314
  }
360
315
  };
361
316
 
317
+ // src/motion/tween.ts
318
+ function resolveEase(ease) {
319
+ return ease === "snappy" ? cubicOut : cubicInOut;
320
+ }
321
+ function prefersReducedMotion() {
322
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
323
+ try {
324
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+ function createTween(opts) {
330
+ const { duration, ease, apply, onDone } = opts;
331
+ let startTime = null;
332
+ let finished = false;
333
+ function settleAt(t) {
334
+ apply(ease(t));
335
+ }
336
+ return {
337
+ tick(now) {
338
+ if (finished) return false;
339
+ if (startTime === null) startTime = now;
340
+ const elapsed = now - startTime;
341
+ const raw = duration <= 0 ? 1 : Math.min(1, elapsed / duration);
342
+ settleAt(raw);
343
+ if (raw >= 1) {
344
+ finished = true;
345
+ onDone?.();
346
+ return false;
347
+ }
348
+ return true;
349
+ },
350
+ finish() {
351
+ if (finished) return;
352
+ finished = true;
353
+ settleAt(1);
354
+ onDone?.();
355
+ },
356
+ cancel() {
357
+ finished = true;
358
+ }
359
+ };
360
+ }
361
+
362
362
  export {
363
363
  observeResize,
364
364
  resolveDarkMode,
@@ -374,4 +374,4 @@ export {
374
374
  prefersReducedMotion,
375
375
  createTween
376
376
  };
377
- //# sourceMappingURL=chunk-2OJZBMXM.js.map
377
+ //# sourceMappingURL=chunk-REH5UQCL.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resize-observer.ts","../src/tooltip.ts","../src/motion/easing.ts","../src/resolve-dark-mode.ts","../src/motion/scheduler.ts","../src/motion/tween.ts","../src/spatial-index.ts"],"sourcesContent":["/**\n * Resize observer: thin wrapper around ResizeObserver with debounce.\n *\n * Watches a container element for size changes and calls back with\n * the new width and height. Debounced at ~60fps (16ms) to avoid\n * excessive re-renders during drag resizes.\n */\n\nconst DEBOUNCE_MS = 16;\n\n/**\n * Observe a container element for size changes.\n *\n * @param container - The element to watch.\n * @param callback - Called with (width, height) when size changes.\n * @returns A cleanup function that disconnects the observer.\n */\nexport function observeResize(\n container: HTMLElement,\n callback: (width: number, height: number) => void,\n): () => void {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let lastDelivered: { width: number; height: number } | null = null;\n\n const observer = new ResizeObserver((entries) => {\n // Debounce to ~60fps\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n }\n timeoutId = setTimeout(() => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect;\n // Always deliver the first callback (the initial fire on insertion is\n // load-bearing, see .claude/rules/svg-animation.md). Only dedupe\n // subsequent deliveries whose size didn't actually change.\n if (lastDelivered && lastDelivered.width === width && lastDelivered.height === height) {\n continue;\n }\n lastDelivered = { width, height };\n callback(width, height);\n }\n timeoutId = null;\n }, DEBOUNCE_MS);\n });\n\n observer.observe(container);\n\n return () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n }\n observer.disconnect();\n };\n}\n","/**\n * Tooltip manager: creates and positions a floating tooltip element.\n *\n * Shows tooltip content near the mouse/touch position with viewport\n * edge avoidance via @floating-ui/dom. Touch support via tap-to-show,\n * tap-outside-to-hide.\n */\n\nimport type { Placement } from '@floating-ui/dom';\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom';\nimport type { TooltipContent } from '@opendata-ai/openchart-core';\n\nexport interface TooltipShowOptions {\n /**\n * Floating-ui placement. Defaults to 'bottom-start' for mark hover\n * (the legacy behavior). Line/area snap-tooltips pass 'right-start'\n * so the card sits beside the snapped point and flips to 'left-start'\n * via the flip middleware when crowding the right edge.\n */\n placement?: Placement;\n}\n\n/**\n * Content accepted by {@link TooltipManager.show}. A structured\n * {@link TooltipContent} is rendered by the manager; `{ text }` is inserted as\n * plain text (escaped via textContent); `{ element }` is a caller-owned node\n * trusted verbatim (the host owns sanitization).\n */\nexport type TooltipInput = TooltipContent | { text: string } | { element: HTMLElement };\n\nexport interface TooltipManager {\n /** Show the tooltip with content at a given position. */\n show(content: TooltipInput, x: number, y: number, opts?: TooltipShowOptions): void;\n /**\n * Re-anchor the open tooltip without re-rendering its content.\n *\n * The 3D graph re-anchors on every engine tick and every camera change, and\n * `show()` would re-run the host formatter and rewrite the DOM at frame rate\n * for content that has not changed.\n */\n move(x: number, y: number, opts?: TooltipShowOptions): void;\n /** Hide the tooltip. */\n hide(): void;\n /** Remove the tooltip element and clean up event listeners. */\n destroy(): void;\n}\n\nconst TOOLTIP_OFFSET = 12;\n\n/**\n * Create a tooltip manager attached to a container element.\n *\n * The manager creates a floating div positioned relative to the container.\n * Content is rendered as a title line with optional color indicator,\n * followed by a compact list of field-value pairs.\n *\n * @param container - The parent element for the tooltip.\n * @returns TooltipManager with show/hide/destroy methods.\n */\nexport function createTooltipManager(container: HTMLElement): TooltipManager {\n const tooltip = document.createElement('div');\n tooltip.className = 'oc-tooltip';\n tooltip.setAttribute('role', 'tooltip');\n\n container.style.position = container.style.position || 'relative';\n container.appendChild(tooltip);\n\n // Track last content to skip innerHTML when only position changes\n let lastContentKey = '';\n // Generation counter to discard stale async position callbacks\n let currentPositionId = 0;\n\n // Hide on tap-outside for touch devices\n const handleDocumentTouch = (e: Event): void => {\n if (!container.contains(e.target as Node)) {\n hide();\n }\n };\n document.addEventListener('touchstart', handleDocumentTouch);\n\n // WCAG 1.4.13: hover-triggered content must be dismissible without moving\n // the pointer. Document-level so it works while hovering, when the chart\n // container does not have keyboard focus.\n const handleDocumentKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape' && tooltip.style.display === 'block') {\n hide();\n }\n };\n document.addEventListener('keydown', handleDocumentKeydown);\n\n function show(input: TooltipInput, x: number, y: number, opts?: TooltipShowOptions): void {\n // Custom-element form: trusted verbatim (host owns sanitization).\n if ('element' in input) {\n lastContentKey = '';\n tooltip.replaceChildren(input.element);\n position(x, y, opts);\n return;\n }\n // Plain-text form: escaped via textContent, never innerHTML.\n if ('text' in input) {\n lastContentKey = '';\n tooltip.textContent = input.text;\n position(x, y, opts);\n return;\n }\n\n const content = input;\n // Fast content identity check: title + field count + first/last field values\n // Includes the emphasized row: moving the pointer between series at the\n // same x changes only which row is bold, and the fast path would skip it.\n const emphasisIndex = content.fields.findIndex((f) => f.emphasis);\n const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}|${emphasisIndex}`;\n\n if (contentKey !== lastContentKey) {\n lastContentKey = contentKey;\n\n let html = '';\n\n // Multi-series tooltips put a swatch on each row instead of in the\n // header, so detect that case once.\n const colorRowCount = content.fields.filter((f) => f.color && f.role !== 'total').length;\n const perRowSwatches = colorRowCount > 1;\n\n // Title row: header dot only when there's a single color (single-series)\n if (content.title) {\n const titleColor = perRowSwatches ? undefined : content.fields.find((f) => f.color)?.color;\n html += '<div class=\"oc-tooltip-header\">';\n if (titleColor) {\n html += `<span class=\"oc-tooltip-dot\" style=\"background:${esc(titleColor)}\"></span>`;\n }\n html += `<span class=\"oc-tooltip-title\">${esc(content.title)}</span>`;\n html += '</div>';\n }\n\n // Field rows\n if (content.fields.length > 0) {\n html += '<div class=\"oc-tooltip-body\">';\n for (const field of content.fields) {\n let rowClass = 'oc-tooltip-row';\n if (field.emphasis) rowClass += ' oc-tooltip-row--emphasis';\n if (field.role === 'total') rowClass += ' oc-tooltip-row--total';\n html += `<div class=\"${rowClass}\">`;\n html += '<span class=\"oc-tooltip-label\">';\n if (perRowSwatches && field.color) {\n html += `<span class=\"oc-tooltip-row-swatch\" style=\"background:${esc(field.color)}\"></span>`;\n }\n html += `${esc(field.label)}</span>`;\n html += `<span class=\"oc-tooltip-value\">${esc(field.value)}</span>`;\n html += '</div>';\n }\n html += '</div>';\n }\n\n tooltip.innerHTML = html;\n }\n\n position(x, y, opts);\n }\n\n /** Show and position the tooltip at a container-relative coordinate. */\n function position(x: number, y: number, opts?: TooltipShowOptions): void {\n tooltip.style.display = 'block';\n\n // Position with viewport-aware edge avoidance via @floating-ui/dom.\n // Uses a virtual element since the reference point is a coordinate,\n // not a DOM element.\n const positionId = ++currentPositionId;\n const virtualRef = {\n getBoundingClientRect() {\n const rect = container.getBoundingClientRect();\n return {\n x: rect.left + x,\n y: rect.top + y,\n width: 0,\n height: 0,\n top: rect.top + y,\n left: rect.left + x,\n right: rect.left + x,\n bottom: rect.top + y,\n };\n },\n };\n\n computePosition(virtualRef, tooltip, {\n placement: opts?.placement ?? 'bottom-start',\n middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: 8 })],\n }).then(({ x: fx, y: fy }) => {\n // Discard stale callbacks from earlier show() calls\n if (positionId !== currentPositionId) return;\n // computePosition returns coordinates relative to the tooltip's offset\n // parent (the container with position: relative), so apply directly.\n tooltip.style.left = `${fx}px`;\n tooltip.style.top = `${fy}px`;\n });\n }\n\n /** Re-anchor the tooltip at a new coordinate, leaving its content alone. */\n function move(x: number, y: number, opts?: TooltipShowOptions): void {\n // `position()` unhides the element, so moving a hidden tooltip would put\n // whatever content was last rendered back on screen. A move only ever\n // re-anchors something already visible.\n if (tooltip.style.display !== 'block') return;\n position(x, y, opts);\n }\n\n function hide(): void {\n tooltip.style.display = 'none';\n lastContentKey = '';\n }\n\n function destroy(): void {\n document.removeEventListener('touchstart', handleDocumentTouch);\n document.removeEventListener('keydown', handleDocumentKeydown);\n if (tooltip.parentNode) {\n tooltip.parentNode.removeChild(tooltip);\n }\n }\n\n return { show, move, hide, destroy };\n}\n\nfunction esc(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#x27;');\n}\n","/**\n * Shared easing and interpolation primitives.\n *\n * Single source of truth for the hand-rolled easings used by the canvas\n * scheduler (motion/tween.ts), the update-transition driver (transition.ts)\n * and the story tweens (story/tween.ts). No `d3-ease` dependency.\n */\n\n/** Linear (identity) easing. Required for stacked/segmented handoffs. */\nexport function linear(t: number): number {\n return t;\n}\n\n/** Cubic-out: decelerates into place. Maps to `'snappy'`. */\nexport function cubicOut(t: number): number {\n const f = 1 - t;\n return 1 - f * f * f;\n}\n\n/** Cubic-in-out: eases in and out. Maps to `'smooth'`. */\nexport function cubicInOut(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n}\n\nexport const clamp01 = (t: number): number => Math.min(1, Math.max(0, t));\n\nexport const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;\n","import type { DarkMode } from '@opendata-ai/openchart-core';\n\n/**\n * Resolve a DarkMode option to a boolean. 'auto' consults the OS preference\n * via matchMedia; environments without matchMedia (SSR, happy-dom) resolve\n * to light.\n */\nexport function resolveDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n if (mode === 'off' || mode === undefined) return false;\n if (typeof window !== 'undefined' && window.matchMedia) {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n return false;\n}\n","/**\n * Canvas animation scheduler.\n *\n * Owns the set of running rAF-driven animations and the first-frame arming.\n * The mount's render loop ticks the scheduler once per frame and re-arms rAF\n * only while animations are active — a continuous loop that costs nothing when\n * idle (the base render loop stays strictly dirty-flag).\n *\n * Shared by the graph canvas and the scatter canvas mark layer.\n *\n * Two hazards this design guards against:\n *\n * 1. **First-frame arming is the scheduler's job, not the call site's.** `add()`\n * invokes the injected `requestFrame` on the idle→active transition, so no\n * call site can forget to arm the first frame.\n *\n * 2. **Reentrancy.** A tween's `onDone` may call `update()` / `destroy()`, which\n * trigger `finishAll` / `cancelAll` mid-tick. `tick()` iterates a SNAPSHOT of\n * the animation list; `remove` / `cancelAll` are safe to call mid-tick.\n */\n\n/** A single rAF-driven animation. */\nexport interface Animation {\n /** Advance to `now` (ms). Returns true while running, false when complete. */\n tick(now: number): boolean;\n /** Jump to the final state and fire completion callbacks. */\n finish(): void;\n /** Stop immediately without applying the final state or firing completion. */\n cancel(): void;\n}\n\n/** Manages a set of active animations and drives frame arming. */\nexport class AnimationScheduler {\n private animations = new Set<Animation>();\n private readonly requestFrame: () => void;\n\n /**\n * @param requestFrame Called on the idle→active transition to arm the first\n * frame (typically the mount's `scheduleRender`).\n */\n constructor(requestFrame: () => void) {\n this.requestFrame = requestFrame;\n }\n\n /** Add an animation. Arms the first frame if the scheduler was idle. */\n add(a: Animation): void {\n const wasIdle = this.animations.size === 0;\n this.animations.add(a);\n if (wasIdle) this.requestFrame();\n }\n\n /** Remove an animation without finishing or cancelling it. Safe mid-tick. */\n remove(a: Animation): void {\n this.animations.delete(a);\n }\n\n /**\n * Advance all animations to `now`. Completed animations are removed. Returns\n * true if any animation ran this frame (so the caller marks the frame dirty).\n *\n * Iterates a snapshot so an `onDone` that mutates the set (via update/destroy)\n * can't corrupt the loop.\n */\n tick(now: number): boolean {\n if (this.animations.size === 0) return false;\n const snapshot = [...this.animations];\n let ran = false;\n for (const a of snapshot) {\n // An earlier animation's onDone may have removed this one mid-loop.\n if (!this.animations.has(a)) continue;\n ran = true;\n const running = a.tick(now);\n if (!running) this.animations.delete(a);\n }\n return ran;\n }\n\n /** True while any animation is active (drives rAF re-arming). */\n get active(): boolean {\n return this.animations.size > 0;\n }\n\n /** Cancel every animation (no final state, no onDone). Used on teardown. */\n cancelAll(): void {\n const snapshot = [...this.animations];\n this.animations.clear();\n for (const a of snapshot) a.cancel();\n }\n\n /** Finish every animation (snap to final, fire onDone). Used before update(). */\n finishAll(): void {\n const snapshot = [...this.animations];\n this.animations.clear();\n for (const a of snapshot) a.finish();\n }\n}\n","/**\n * Canvas motion primitives: easings, reduced-motion detection, and a generic\n * time-based tween.\n *\n * Canvas marks animate via a rAF scheduler (see scheduler.ts), NOT via CSS\n * keyframes like SVG charts. Only the easing vocabulary is shared with charts\n * (`'smooth'` / `'snappy'`). No `d3-ease` dependency — easings are hand-rolled,\n * matching `transition.ts`'s `cubicOut`.\n */\n\nimport type { AnimationEase } from '@opendata-ai/openchart-core';\nimport { cubicInOut, cubicOut } from './easing';\nimport type { Animation } from './scheduler';\n\n// ---------------------------------------------------------------------------\n// Easings (shared definitions live in easing.ts; re-exported for callers)\n// ---------------------------------------------------------------------------\n\nexport { cubicInOut, cubicOut, linear } from './easing';\n\n/** Resolve a named ease preset to an easing function. */\nexport function resolveEase(ease: AnimationEase): (t: number) => number {\n return ease === 'snappy' ? cubicOut : cubicInOut;\n}\n\n// ---------------------------------------------------------------------------\n// Reduced motion\n// ---------------------------------------------------------------------------\n\n/**\n * Whether the user prefers reduced motion. Mirrors `transition.ts`: guards\n * SSR/happy-dom where matchMedia may be missing or throw.\n */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n try {\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tween\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createTween}. */\nexport interface TweenOptions {\n /** Duration in ms. A duration <= 0 completes on the first tick. */\n duration: number;\n /** Easing function applied to the normalized 0..1 progress. */\n ease: (t: number) => number;\n /** Called each tick with the eased progress. */\n apply: (t: number) => void;\n /** Called once when the tween completes (not on cancel). */\n onDone?: () => void;\n}\n\n/**\n * Create a time-based tween as an {@link Animation}.\n *\n * The start time locks on the FIRST `tick(now)` call (not at construction), so\n * a tween added while the scheduler is idle starts cleanly on the next frame\n * and tests get deterministic progress from a controllable clock.\n */\nexport function createTween(opts: TweenOptions): Animation {\n const { duration, ease, apply, onDone } = opts;\n let startTime: number | null = null;\n let finished = false;\n\n function settleAt(t: number): void {\n apply(ease(t));\n }\n\n return {\n tick(now: number): boolean {\n if (finished) return false;\n if (startTime === null) startTime = now;\n const elapsed = now - startTime;\n const raw = duration <= 0 ? 1 : Math.min(1, elapsed / duration);\n settleAt(raw);\n if (raw >= 1) {\n finished = true;\n onDone?.();\n return false;\n }\n return true;\n },\n finish(): void {\n if (finished) return;\n finished = true;\n settleAt(1);\n onDone?.();\n },\n cancel(): void {\n finished = true;\n },\n };\n}\n","/**\n * Spatial index for fast node hit-testing.\n *\n * Wraps d3-quadtree to provide findNearest (accounts for node radius)\n * and findInRect queries. Tracks a generation counter to avoid unnecessary\n * rebuilds when positions haven't changed.\n */\n\nimport { type Quadtree, quadtree } from 'd3-quadtree';\n\n/** Minimum shape a spatial-index entry must have. */\nexport interface SpatialEntry {\n x: number;\n y: number;\n radius: number;\n}\n\nexport class SpatialIndex<T extends SpatialEntry = SpatialEntry> {\n private tree: Quadtree<T> | null = null;\n private nodes: T[] = [];\n private maxRadius = 0;\n private generation = 0;\n\n /** Rebuild the quadtree from the current set of positioned nodes. */\n rebuild(nodes: T[]): void {\n this.nodes = nodes;\n this.maxRadius = 0;\n for (const n of nodes) {\n if (n.radius > this.maxRadius) this.maxRadius = n.radius;\n }\n\n this.tree = quadtree<T>()\n .x((d) => d.x)\n .y((d) => d.y)\n .addAll(nodes);\n this.generation++;\n }\n\n /** Current generation counter. Increments on each rebuild. */\n getGeneration(): number {\n return this.generation;\n }\n\n /**\n * Find the nearest node to (x, y) within maxDistance.\n * Accounts for node radius: a hit occurs if the point is inside\n * the node circle (distance to center < node.radius), or if the\n * edge-to-edge distance is within maxDistance.\n */\n findNearest(x: number, y: number, maxDistance: number = Infinity): T | null {\n if (!this.tree || this.nodes.length === 0) return null;\n\n // The effective search radius for the quadtree needs to include\n // the largest node radius, because we might be \"inside\" a large node\n // whose center is far from our search point.\n const searchRadius = maxDistance + this.maxRadius;\n\n let best: T | null = null;\n let bestEffectiveDist = maxDistance + this.maxRadius + 1;\n\n this.tree.visit((node, x0, y0, x1, y1) => {\n // Closest point in this quad to our target\n const closestX = Math.max(x0, Math.min(x, x1));\n const closestY = Math.max(y0, Math.min(y, y1));\n const quadDist = Math.hypot(closestX - x, closestY - y);\n\n // Prune: if the closest edge of this quad is beyond searchRadius, skip\n if (quadDist > searchRadius) return true;\n\n // Check leaf data\n if (!node.length) {\n let current = node;\n do {\n const d = current.data;\n if (d) {\n const dist = Math.hypot(d.x - x, d.y - y);\n // Effective distance: subtract the node's radius.\n // If we're inside the circle, effective distance is 0.\n const effectiveDist = Math.max(0, dist - d.radius);\n if (effectiveDist <= maxDistance && effectiveDist < bestEffectiveDist) {\n bestEffectiveDist = effectiveDist;\n best = d;\n }\n }\n } while ((current = current.next!));\n }\n\n return false;\n });\n\n return best;\n }\n\n /** Find all nodes whose centers fall within the given rectangle. */\n findInRect(x1: number, y1: number, x2: number, y2: number): T[] {\n if (!this.tree) return [];\n\n const minX = Math.min(x1, x2);\n const minY = Math.min(y1, y2);\n const maxX = Math.max(x1, x2);\n const maxY = Math.max(y1, y2);\n\n const results: T[] = [];\n\n this.tree.visit((node, qx0, qy0, qx1, qy1) => {\n // If quad doesn't overlap the search rect, skip\n if (qx0 > maxX || qx1 < minX || qy0 > maxY || qy1 < minY) {\n return true;\n }\n\n // Check leaf nodes\n if (!node.length) {\n let current = node;\n do {\n const d = current.data;\n if (d && d.x >= minX && d.x <= maxX && d.y >= minY && d.y <= maxY) {\n results.push(d);\n }\n } while ((current = current.next!));\n }\n\n return false;\n });\n\n return results;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/resize-observer.ts","../src/tooltip.ts","../src/motion/easing.ts","../src/resolve-dark-mode.ts","../src/motion/scheduler.ts","../src/spatial-index.ts","../src/motion/tween.ts"],"sourcesContent":["/**\n * Resize observer: thin wrapper around ResizeObserver with debounce.\n *\n * Watches a container element for size changes and calls back with\n * the new width and height. Debounced at ~60fps (16ms) to avoid\n * excessive re-renders during drag resizes.\n */\n\nconst DEBOUNCE_MS = 16;\n\n/**\n * Observe a container element for size changes.\n *\n * @param container - The element to watch.\n * @param callback - Called with (width, height) when size changes.\n * @returns A cleanup function that disconnects the observer.\n */\nexport function observeResize(\n container: HTMLElement,\n callback: (width: number, height: number) => void,\n): () => void {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n let lastDelivered: { width: number; height: number } | null = null;\n\n const observer = new ResizeObserver((entries) => {\n // Debounce to ~60fps\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n }\n timeoutId = setTimeout(() => {\n for (const entry of entries) {\n const { width, height } = entry.contentRect;\n // Always deliver the first callback (the initial fire on insertion is\n // load-bearing, see .claude/rules/svg-animation.md). Only dedupe\n // subsequent deliveries whose size didn't actually change.\n if (lastDelivered && lastDelivered.width === width && lastDelivered.height === height) {\n continue;\n }\n lastDelivered = { width, height };\n callback(width, height);\n }\n timeoutId = null;\n }, DEBOUNCE_MS);\n });\n\n observer.observe(container);\n\n return () => {\n if (timeoutId !== null) {\n clearTimeout(timeoutId);\n }\n observer.disconnect();\n };\n}\n","/**\n * Tooltip manager: creates and positions a floating tooltip element.\n *\n * Shows tooltip content near the mouse/touch position with viewport\n * edge avoidance via @floating-ui/dom. Touch support via tap-to-show,\n * tap-outside-to-hide.\n */\n\nimport type { Placement } from '@floating-ui/dom';\nimport { computePosition, flip, offset, shift } from '@floating-ui/dom';\nimport type { TooltipContent } from '@opendata-ai/openchart-core';\n\nexport interface TooltipShowOptions {\n /**\n * Floating-ui placement. Defaults to 'bottom-start' for mark hover\n * (the legacy behavior). Line/area snap-tooltips pass 'right-start'\n * so the card sits beside the snapped point and flips to 'left-start'\n * via the flip middleware when crowding the right edge.\n */\n placement?: Placement;\n}\n\n/**\n * Content accepted by {@link TooltipManager.show}. A structured\n * {@link TooltipContent} is rendered by the manager; `{ text }` is inserted as\n * plain text (escaped via textContent); `{ element }` is a caller-owned node\n * trusted verbatim (the host owns sanitization).\n */\nexport type TooltipInput = TooltipContent | { text: string } | { element: HTMLElement };\n\nexport interface TooltipManager {\n /** Show the tooltip with content at a given position. */\n show(content: TooltipInput, x: number, y: number, opts?: TooltipShowOptions): void;\n /**\n * Re-anchor the open tooltip without re-rendering its content.\n *\n * The 3D graph re-anchors on every engine tick and every camera change, and\n * `show()` would re-run the host formatter and rewrite the DOM at frame rate\n * for content that has not changed.\n */\n move(x: number, y: number, opts?: TooltipShowOptions): void;\n /** Hide the tooltip. */\n hide(): void;\n /** Remove the tooltip element and clean up event listeners. */\n destroy(): void;\n}\n\nconst TOOLTIP_OFFSET = 12;\n\n/**\n * Create a tooltip manager attached to a container element.\n *\n * The manager creates a floating div positioned relative to the container.\n * Content is rendered as a title line with optional color indicator,\n * followed by a compact list of field-value pairs.\n *\n * @param container - The parent element for the tooltip.\n * @returns TooltipManager with show/hide/destroy methods.\n */\nexport function createTooltipManager(container: HTMLElement): TooltipManager {\n const tooltip = document.createElement('div');\n tooltip.className = 'oc-tooltip';\n tooltip.setAttribute('role', 'tooltip');\n\n container.style.position = container.style.position || 'relative';\n container.appendChild(tooltip);\n\n // Track last content to skip innerHTML when only position changes\n let lastContentKey = '';\n // Generation counter to discard stale async position callbacks\n let currentPositionId = 0;\n\n // Hide on tap-outside for touch devices\n const handleDocumentTouch = (e: Event): void => {\n if (!container.contains(e.target as Node)) {\n hide();\n }\n };\n document.addEventListener('touchstart', handleDocumentTouch);\n\n // WCAG 1.4.13: hover-triggered content must be dismissible without moving\n // the pointer. Document-level so it works while hovering, when the chart\n // container does not have keyboard focus.\n const handleDocumentKeydown = (e: KeyboardEvent): void => {\n if (e.key === 'Escape' && tooltip.style.display === 'block') {\n hide();\n }\n };\n document.addEventListener('keydown', handleDocumentKeydown);\n\n function show(input: TooltipInput, x: number, y: number, opts?: TooltipShowOptions): void {\n // Custom-element form: trusted verbatim (host owns sanitization).\n if ('element' in input) {\n lastContentKey = '';\n tooltip.replaceChildren(input.element);\n position(x, y, opts);\n return;\n }\n // Plain-text form: escaped via textContent, never innerHTML.\n if ('text' in input) {\n lastContentKey = '';\n tooltip.textContent = input.text;\n position(x, y, opts);\n return;\n }\n\n const content = input;\n // Fast content identity check: title + field count + first/last field values\n // Includes the emphasized row: moving the pointer between series at the\n // same x changes only which row is bold, and the fast path would skip it.\n const emphasisIndex = content.fields.findIndex((f) => f.emphasis);\n const contentKey = `${content.title}|${content.fields.length}|${content.fields[0]?.value}|${content.fields[content.fields.length - 1]?.value}|${emphasisIndex}`;\n\n if (contentKey !== lastContentKey) {\n lastContentKey = contentKey;\n\n let html = '';\n\n // Multi-series tooltips put a swatch on each row instead of in the\n // header, so detect that case once.\n const colorRowCount = content.fields.filter((f) => f.color && f.role !== 'total').length;\n const perRowSwatches = colorRowCount > 1;\n\n // Title row: header dot only when there's a single color (single-series)\n if (content.title) {\n const titleColor = perRowSwatches ? undefined : content.fields.find((f) => f.color)?.color;\n html += '<div class=\"oc-tooltip-header\">';\n if (titleColor) {\n html += `<span class=\"oc-tooltip-dot\" style=\"background:${esc(titleColor)}\"></span>`;\n }\n html += `<span class=\"oc-tooltip-title\">${esc(content.title)}</span>`;\n html += '</div>';\n }\n\n // Field rows\n if (content.fields.length > 0) {\n html += '<div class=\"oc-tooltip-body\">';\n for (const field of content.fields) {\n let rowClass = 'oc-tooltip-row';\n if (field.emphasis) rowClass += ' oc-tooltip-row--emphasis';\n if (field.role === 'total') rowClass += ' oc-tooltip-row--total';\n html += `<div class=\"${rowClass}\">`;\n html += '<span class=\"oc-tooltip-label\">';\n if (perRowSwatches && field.color) {\n html += `<span class=\"oc-tooltip-row-swatch\" style=\"background:${esc(field.color)}\"></span>`;\n }\n html += `${esc(field.label)}</span>`;\n html += `<span class=\"oc-tooltip-value\">${esc(field.value)}</span>`;\n html += '</div>';\n }\n html += '</div>';\n }\n\n tooltip.innerHTML = html;\n }\n\n position(x, y, opts);\n }\n\n /** Show and position the tooltip at a container-relative coordinate. */\n function position(x: number, y: number, opts?: TooltipShowOptions): void {\n tooltip.style.display = 'block';\n\n // Position with viewport-aware edge avoidance via @floating-ui/dom.\n // Uses a virtual element since the reference point is a coordinate,\n // not a DOM element.\n const positionId = ++currentPositionId;\n const virtualRef = {\n getBoundingClientRect() {\n const rect = container.getBoundingClientRect();\n return {\n x: rect.left + x,\n y: rect.top + y,\n width: 0,\n height: 0,\n top: rect.top + y,\n left: rect.left + x,\n right: rect.left + x,\n bottom: rect.top + y,\n };\n },\n };\n\n computePosition(virtualRef, tooltip, {\n placement: opts?.placement ?? 'bottom-start',\n middleware: [offset(TOOLTIP_OFFSET), flip(), shift({ padding: 8 })],\n }).then(({ x: fx, y: fy }) => {\n // Discard stale callbacks from earlier show() calls\n if (positionId !== currentPositionId) return;\n // computePosition returns coordinates relative to the tooltip's offset\n // parent (the container with position: relative), so apply directly.\n tooltip.style.left = `${fx}px`;\n tooltip.style.top = `${fy}px`;\n });\n }\n\n /** Re-anchor the tooltip at a new coordinate, leaving its content alone. */\n function move(x: number, y: number, opts?: TooltipShowOptions): void {\n // `position()` unhides the element, so moving a hidden tooltip would put\n // whatever content was last rendered back on screen. A move only ever\n // re-anchors something already visible.\n if (tooltip.style.display !== 'block') return;\n position(x, y, opts);\n }\n\n function hide(): void {\n tooltip.style.display = 'none';\n lastContentKey = '';\n }\n\n function destroy(): void {\n document.removeEventListener('touchstart', handleDocumentTouch);\n document.removeEventListener('keydown', handleDocumentKeydown);\n if (tooltip.parentNode) {\n tooltip.parentNode.removeChild(tooltip);\n }\n }\n\n return { show, move, hide, destroy };\n}\n\nfunction esc(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#x27;');\n}\n","/**\n * Shared easing and interpolation primitives.\n *\n * Single source of truth for the hand-rolled easings used by the canvas\n * scheduler (motion/tween.ts), the update-transition driver (transition.ts)\n * and the story tweens (story/tween.ts). No `d3-ease` dependency.\n */\n\n/** Linear (identity) easing. Required for stacked/segmented handoffs. */\nexport function linear(t: number): number {\n return t;\n}\n\n/** Cubic-out: decelerates into place. Maps to `'snappy'`. */\nexport function cubicOut(t: number): number {\n const f = 1 - t;\n return 1 - f * f * f;\n}\n\n/** Cubic-in-out: eases in and out. Maps to `'smooth'`. */\nexport function cubicInOut(t: number): number {\n return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;\n}\n\nexport const clamp01 = (t: number): number => Math.min(1, Math.max(0, t));\n\nexport const lerp = (a: number, b: number, t: number): number => a + (b - a) * t;\n","import type { DarkMode } from '@opendata-ai/openchart-core';\n\n/**\n * Resolve a DarkMode option to a boolean. 'auto' consults the OS preference\n * via matchMedia; environments without matchMedia (SSR, happy-dom) resolve\n * to light.\n */\nexport function resolveDarkMode(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n if (mode === 'off' || mode === undefined) return false;\n if (typeof window !== 'undefined' && window.matchMedia) {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n return false;\n}\n","/**\n * Canvas animation scheduler.\n *\n * Owns the set of running rAF-driven animations and the first-frame arming.\n * The mount's render loop ticks the scheduler once per frame and re-arms rAF\n * only while animations are active — a continuous loop that costs nothing when\n * idle (the base render loop stays strictly dirty-flag).\n *\n * Shared by the graph canvas and the scatter canvas mark layer.\n *\n * Two hazards this design guards against:\n *\n * 1. **First-frame arming is the scheduler's job, not the call site's.** `add()`\n * invokes the injected `requestFrame` on the idle→active transition, so no\n * call site can forget to arm the first frame.\n *\n * 2. **Reentrancy.** A tween's `onDone` may call `update()` / `destroy()`, which\n * trigger `finishAll` / `cancelAll` mid-tick. `tick()` iterates a SNAPSHOT of\n * the animation list; `remove` / `cancelAll` are safe to call mid-tick.\n */\n\n/** A single rAF-driven animation. */\nexport interface Animation {\n /** Advance to `now` (ms). Returns true while running, false when complete. */\n tick(now: number): boolean;\n /** Jump to the final state and fire completion callbacks. */\n finish(): void;\n /** Stop immediately without applying the final state or firing completion. */\n cancel(): void;\n}\n\n/** Manages a set of active animations and drives frame arming. */\nexport class AnimationScheduler {\n private animations = new Set<Animation>();\n private readonly requestFrame: () => void;\n\n /**\n * @param requestFrame Called on the idle→active transition to arm the first\n * frame (typically the mount's `scheduleRender`).\n */\n constructor(requestFrame: () => void) {\n this.requestFrame = requestFrame;\n }\n\n /** Add an animation. Arms the first frame if the scheduler was idle. */\n add(a: Animation): void {\n const wasIdle = this.animations.size === 0;\n this.animations.add(a);\n if (wasIdle) this.requestFrame();\n }\n\n /** Remove an animation without finishing or cancelling it. Safe mid-tick. */\n remove(a: Animation): void {\n this.animations.delete(a);\n }\n\n /**\n * Advance all animations to `now`. Completed animations are removed. Returns\n * true if any animation ran this frame (so the caller marks the frame dirty).\n *\n * Iterates a snapshot so an `onDone` that mutates the set (via update/destroy)\n * can't corrupt the loop.\n */\n tick(now: number): boolean {\n if (this.animations.size === 0) return false;\n const snapshot = [...this.animations];\n let ran = false;\n for (const a of snapshot) {\n // An earlier animation's onDone may have removed this one mid-loop.\n if (!this.animations.has(a)) continue;\n ran = true;\n const running = a.tick(now);\n if (!running) this.animations.delete(a);\n }\n return ran;\n }\n\n /** True while any animation is active (drives rAF re-arming). */\n get active(): boolean {\n return this.animations.size > 0;\n }\n\n /** Cancel every animation (no final state, no onDone). Used on teardown. */\n cancelAll(): void {\n const snapshot = [...this.animations];\n this.animations.clear();\n for (const a of snapshot) a.cancel();\n }\n\n /** Finish every animation (snap to final, fire onDone). Used before update(). */\n finishAll(): void {\n const snapshot = [...this.animations];\n this.animations.clear();\n for (const a of snapshot) a.finish();\n }\n}\n","/**\n * Spatial index for fast node hit-testing.\n *\n * Wraps d3-quadtree to provide findNearest (accounts for node radius)\n * and findInRect queries. Tracks a generation counter to avoid unnecessary\n * rebuilds when positions haven't changed.\n */\n\nimport { type Quadtree, quadtree } from 'd3-quadtree';\n\n/** Minimum shape a spatial-index entry must have. */\nexport interface SpatialEntry {\n x: number;\n y: number;\n radius: number;\n}\n\nexport class SpatialIndex<T extends SpatialEntry = SpatialEntry> {\n private tree: Quadtree<T> | null = null;\n private nodes: T[] = [];\n private maxRadius = 0;\n private generation = 0;\n\n /** Rebuild the quadtree from the current set of positioned nodes. */\n rebuild(nodes: T[]): void {\n this.nodes = nodes;\n this.maxRadius = 0;\n for (const n of nodes) {\n if (n.radius > this.maxRadius) this.maxRadius = n.radius;\n }\n\n this.tree = quadtree<T>()\n .x((d) => d.x)\n .y((d) => d.y)\n .addAll(nodes);\n this.generation++;\n }\n\n /** Current generation counter. Increments on each rebuild. */\n getGeneration(): number {\n return this.generation;\n }\n\n /**\n * Find the nearest node to (x, y) within maxDistance.\n * Accounts for node radius: a hit occurs if the point is inside\n * the node circle (distance to center < node.radius), or if the\n * edge-to-edge distance is within maxDistance.\n */\n findNearest(x: number, y: number, maxDistance: number = Infinity): T | null {\n if (!this.tree || this.nodes.length === 0) return null;\n\n // The effective search radius for the quadtree needs to include\n // the largest node radius, because we might be \"inside\" a large node\n // whose center is far from our search point.\n const searchRadius = maxDistance + this.maxRadius;\n\n let best: T | null = null;\n let bestEffectiveDist = maxDistance + this.maxRadius + 1;\n\n this.tree.visit((node, x0, y0, x1, y1) => {\n // Closest point in this quad to our target\n const closestX = Math.max(x0, Math.min(x, x1));\n const closestY = Math.max(y0, Math.min(y, y1));\n const quadDist = Math.hypot(closestX - x, closestY - y);\n\n // Prune: if the closest edge of this quad is beyond searchRadius, skip\n if (quadDist > searchRadius) return true;\n\n // Check leaf data\n if (!node.length) {\n let current = node;\n do {\n const d = current.data;\n if (d) {\n const dist = Math.hypot(d.x - x, d.y - y);\n // Effective distance: subtract the node's radius.\n // If we're inside the circle, effective distance is 0.\n const effectiveDist = Math.max(0, dist - d.radius);\n if (effectiveDist <= maxDistance && effectiveDist < bestEffectiveDist) {\n bestEffectiveDist = effectiveDist;\n best = d;\n }\n }\n } while ((current = current.next!));\n }\n\n return false;\n });\n\n return best;\n }\n\n /** Find all nodes whose centers fall within the given rectangle. */\n findInRect(x1: number, y1: number, x2: number, y2: number): T[] {\n if (!this.tree) return [];\n\n const minX = Math.min(x1, x2);\n const minY = Math.min(y1, y2);\n const maxX = Math.max(x1, x2);\n const maxY = Math.max(y1, y2);\n\n const results: T[] = [];\n\n this.tree.visit((node, qx0, qy0, qx1, qy1) => {\n // If quad doesn't overlap the search rect, skip\n if (qx0 > maxX || qx1 < minX || qy0 > maxY || qy1 < minY) {\n return true;\n }\n\n // Check leaf nodes\n if (!node.length) {\n let current = node;\n do {\n const d = current.data;\n if (d && d.x >= minX && d.x <= maxX && d.y >= minY && d.y <= maxY) {\n results.push(d);\n }\n } while ((current = current.next!));\n }\n\n return false;\n });\n\n return results;\n }\n}\n","/**\n * Canvas motion primitives: easings, reduced-motion detection, and a generic\n * time-based tween.\n *\n * Canvas marks animate via a rAF scheduler (see scheduler.ts), NOT via CSS\n * keyframes like SVG charts. Only the easing vocabulary is shared with charts\n * (`'smooth'` / `'snappy'`). No `d3-ease` dependency — easings are hand-rolled,\n * matching `transition.ts`'s `cubicOut`.\n */\n\nimport type { AnimationEase } from '@opendata-ai/openchart-core';\nimport { cubicInOut, cubicOut } from './easing';\nimport type { Animation } from './scheduler';\n\n// ---------------------------------------------------------------------------\n// Easings (shared definitions live in easing.ts; re-exported for callers)\n// ---------------------------------------------------------------------------\n\nexport { cubicInOut, cubicOut, linear } from './easing';\n\n/** Resolve a named ease preset to an easing function. */\nexport function resolveEase(ease: AnimationEase): (t: number) => number {\n return ease === 'snappy' ? cubicOut : cubicInOut;\n}\n\n// ---------------------------------------------------------------------------\n// Reduced motion\n// ---------------------------------------------------------------------------\n\n/**\n * Whether the user prefers reduced motion. Mirrors `transition.ts`: guards\n * SSR/happy-dom where matchMedia may be missing or throw.\n */\nexport function prefersReducedMotion(): boolean {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;\n try {\n return window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n } catch {\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tween\n// ---------------------------------------------------------------------------\n\n/** Options for {@link createTween}. */\nexport interface TweenOptions {\n /** Duration in ms. A duration <= 0 completes on the first tick. */\n duration: number;\n /** Easing function applied to the normalized 0..1 progress. */\n ease: (t: number) => number;\n /** Called each tick with the eased progress. */\n apply: (t: number) => void;\n /** Called once when the tween completes (not on cancel). */\n onDone?: () => void;\n}\n\n/**\n * Create a time-based tween as an {@link Animation}.\n *\n * The start time locks on the FIRST `tick(now)` call (not at construction), so\n * a tween added while the scheduler is idle starts cleanly on the next frame\n * and tests get deterministic progress from a controllable clock.\n */\nexport function createTween(opts: TweenOptions): Animation {\n const { duration, ease, apply, onDone } = opts;\n let startTime: number | null = null;\n let finished = false;\n\n function settleAt(t: number): void {\n apply(ease(t));\n }\n\n return {\n tick(now: number): boolean {\n if (finished) return false;\n if (startTime === null) startTime = now;\n const elapsed = now - startTime;\n const raw = duration <= 0 ? 1 : Math.min(1, elapsed / duration);\n settleAt(raw);\n if (raw >= 1) {\n finished = true;\n onDone?.();\n return false;\n }\n return true;\n },\n finish(): void {\n if (finished) return;\n finished = true;\n settleAt(1);\n onDone?.();\n },\n cancel(): void {\n finished = true;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,17 @@
1
+ // src/deprecations.ts
2
+ var emitted = /* @__PURE__ */ new WeakMap();
3
+ function warnDeprecated(message) {
4
+ const sink = console.warn;
5
+ let seen = emitted.get(sink);
6
+ if (!seen) {
7
+ seen = /* @__PURE__ */ new Set();
8
+ emitted.set(sink, seen);
9
+ }
10
+ if (seen.has(message)) return;
11
+ seen.add(message);
12
+ sink(`[openchart] ${message}`);
13
+ }
14
+
1
15
  // src/theme-tokens.ts
2
16
  import { adaptForLightLineStroke, cssTokenDefault } from "@opendata-ai/openchart-core";
3
17
  function resolvedSurface(theme) {
@@ -52,7 +66,8 @@ function stampThemeProperties(el, theme) {
52
66
  }
53
67
 
54
68
  export {
69
+ warnDeprecated,
55
70
  resolvedSurface,
56
71
  stampThemeProperties
57
72
  };
58
- //# sourceMappingURL=chunk-NG7CVCI2.js.map
73
+ //# sourceMappingURL=chunk-UC4IHEUO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/deprecations.ts","../src/theme-tokens.ts"],"sourcesContent":["const emitted = new WeakMap<typeof console.warn, Set<string>>();\n\n/** Runtime compatibility warnings are emitted once per process and console sink. */\nexport function warnDeprecated(message: string): void {\n const sink = console.warn;\n let seen = emitted.get(sink);\n if (!seen) {\n seen = new Set();\n emitted.set(sink, seen);\n }\n if (seen.has(message)) return;\n seen.add(message);\n sink(`[openchart] ${message}`);\n}\n","/**\n * Generate CSS custom properties from a resolved JS theme.\n *\n * Stamped on the .oc-root container at mount time so CSS consumers\n * read from the same source of truth as the JS engine. The static\n * values in tokens.css / dark.css serve as fallback defaults for\n * contexts where JS hasn't mounted (SSR, static HTML).\n */\n\nimport type { ResolvedTheme } from '@opendata-ai/openchart-core';\nimport { adaptForLightLineStroke, cssTokenDefault } from '@opendata-ai/openchart-core';\n\n/**\n * The opaque surface a theme paints on. A transparent theme background has no\n * color of its own, so it resolves to the static `--oc-bg` token for the mode --\n * the same value the engine's knockout strokes are cut in. Resolved once in\n * core, on `colors.neutral.surface`; this is a named read of it.\n */\nexport function resolvedSurface(theme: ResolvedTheme): string {\n return theme.colors.neutral.surface;\n}\n\nexport function stampThemeProperties(el: HTMLElement, theme: ResolvedTheme): void {\n const accent = theme.colors.categorical[0] ?? cssTokenDefault('--oc-accent', 'light');\n const bg = resolvedSurface(theme);\n\n const n = theme.colors.neutral;\n\n const props: [string, string][] = [\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-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-bg', bg],\n ['--oc-text', theme.colors.text],\n ['--oc-text-muted', theme.colors.axis],\n // Secondary grays are derived from the theme's own text/background pair\n // (ResolvedTheme.colors.neutral), so a warm or cool theme gets warm or\n // cool grays instead of zinc. One direction only: theme -> CSS.\n ['--oc-text-secondary', n.secondary],\n ['--oc-text-faint', n.faint],\n ['--oc-border', n.border],\n ['--oc-gray-100', n[100]],\n ['--oc-gray-200', n[200]],\n ['--oc-gray-300', n[300]],\n ['--oc-gray-400', n[400]],\n ['--oc-gray-600', n[600]],\n ['--oc-gray-800', n[800]],\n ['--oc-gridline', theme.colors.gridline],\n // --oc-axis is the hairline the axis line is drawn with, not the ink its\n // tick labels take (that is --oc-text-muted, above).\n ['--oc-axis', theme.colors.hairline],\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-space-2', `${theme.spacing.chromeGap * 2}px`],\n ['--oc-space-4', `${theme.spacing.padding}px`],\n ];\n\n for (const [name, value] of props) {\n el.style.setProperty(name, value);\n }\n}\n"],"mappings":";AAAA,IAAM,UAAU,oBAAI,QAA0C;AAGvD,SAAS,eAAe,SAAuB;AACpD,QAAM,OAAO,QAAQ;AACrB,MAAI,OAAO,QAAQ,IAAI,IAAI;AAC3B,MAAI,CAAC,MAAM;AACT,WAAO,oBAAI,IAAI;AACf,YAAQ,IAAI,MAAM,IAAI;AAAA,EACxB;AACA,MAAI,KAAK,IAAI,OAAO,EAAG;AACvB,OAAK,IAAI,OAAO;AAChB,OAAK,eAAe,OAAO,EAAE;AAC/B;;;ACHA,SAAS,yBAAyB,uBAAuB;AAQlD,SAAS,gBAAgB,OAA8B;AAC5D,SAAO,MAAM,OAAO,QAAQ;AAC9B;AAEO,SAAS,qBAAqB,IAAiB,OAA4B;AAChF,QAAM,SAAS,MAAM,OAAO,YAAY,CAAC,KAAK,gBAAgB,eAAe,OAAO;AACpF,QAAM,KAAK,gBAAgB,KAAK;AAEhC,QAAM,IAAI,MAAM,OAAO;AAEvB,QAAM,QAA4B;AAAA,IAChC,CAAC,oBAAoB,MAAM,MAAM,MAAM;AAAA,IACvC,CAAC,kBAAkB,MAAM,MAAM,IAAI;AAAA,IACnC,CAAC,mBAAmB,GAAG,MAAM,OAAO,MAAM,QAAQ,IAAI;AAAA,IACtD,CAAC,qBAAqB,GAAG,MAAM,OAAO,MAAM,UAAU,EAAE;AAAA,IACxD,CAAC,sBAAsB,GAAG,MAAM,OAAO,SAAS,QAAQ,IAAI;AAAA,IAC5D,CAAC,wBAAwB,GAAG,MAAM,OAAO,SAAS,UAAU,EAAE;AAAA,IAC9D,CAAC,oBAAoB,GAAG,MAAM,OAAO,OAAO,QAAQ,IAAI;AAAA,IACxD,CAAC,sBAAsB,GAAG,MAAM,OAAO,OAAO,UAAU,EAAE;AAAA,IAC1D,CAAC,kBAAkB,GAAG,MAAM,MAAM,MAAM,IAAI,IAAI;AAAA,IAChD,CAAC,qBAAqB,GAAG,MAAM,OAAO,QAAQ,QAAQ,IAAI;AAAA,IAC1D,CAAC,uBAAuB,GAAG,MAAM,OAAO,QAAQ,UAAU,EAAE;AAAA,IAC5D,CAAC,WAAW,EAAE;AAAA,IACd,CAAC,aAAa,MAAM,OAAO,IAAI;AAAA,IAC/B,CAAC,mBAAmB,MAAM,OAAO,IAAI;AAAA;AAAA;AAAA;AAAA,IAIrC,CAAC,uBAAuB,EAAE,SAAS;AAAA,IACnC,CAAC,mBAAmB,EAAE,KAAK;AAAA,IAC3B,CAAC,eAAe,EAAE,MAAM;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,EAAE,GAAG,CAAC;AAAA,IACxB,CAAC,iBAAiB,MAAM,OAAO,QAAQ;AAAA;AAAA;AAAA,IAGvC,CAAC,aAAa,MAAM,OAAO,QAAQ;AAAA,IACnC,CAAC,sBAAsB,GAAG,MAAM,YAAY,IAAI;AAAA,IAChD,CAAC,eAAe,MAAM;AAAA,IACtB,CAAC,sBAAsB,wBAAwB,MAAM,CAAC;AAAA,IACtD,CAAC,iBAAiB,MAAM,OAAO,QAAQ;AAAA,IACvC,CAAC,iBAAiB,MAAM,OAAO,QAAQ;AAAA,IACvC,CAAC,gBAAgB,GAAG,MAAM,QAAQ,YAAY,CAAC,IAAI;AAAA,IACnD,CAAC,gBAAgB,GAAG,MAAM,QAAQ,OAAO,IAAI;AAAA,EAC/C;AAEA,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO;AACjC,OAAG,MAAM,YAAY,MAAM,KAAK;AAAA,EAClC;AACF;","names":[]}
@@ -180,33 +180,6 @@ declare function resolveVisibleLabels(nodes: LabelCandidate[], forced: Set<strin
180
180
  /** Above this edge count `edgeWidth` maps to opacity steps, not cylinder radius. */
181
181
  declare const LINK_WIDTH_MAX_EDGES = 2000;
182
182
 
183
- /**
184
- * The 3D graph renderer: a `GraphInstance` backed by `3d-force-graph`.
185
- *
186
- * Registered against `dimensions: 3` by `./index`. `createGraph()` builds the
187
- * shared shell (wrapper, chrome, legend slot, tooltip manager, resize wiring)
188
- * and hands it here; this module owns the WebGL scene, the d3-force-3d
189
- * simulation the library drives, the per-node and per-link three.js objects,
190
- * emphasis state, the label budget, tooltip anchoring, and disposal.
191
- *
192
- * Three library facts shape almost everything below and are easy to get wrong:
193
- *
194
- * 1. `nodeOpacity` and `linkOpacity` are GLOBAL numbers, not accessors. Every
195
- * per-object opacity (dim, highlight, entrance, search) therefore needs a
196
- * custom object with its own material. See `nodes.ts` / `links.ts`.
197
- * 2. `refresh()` rebuilds every node and link object. It must never run on
198
- * hover; the hover path only writes `material.opacity`.
199
- * 3. `rendererConfig` and `controlType` are CONSTRUCTOR options, not chained
200
- * setters.
201
- *
202
- * Differences from 2D that are deliberate and documented in RFC 27: no
203
- * keyboard navigation, no node drag, no cursor repulsion or springy drag, and
204
- * no wall-clock warmup budget. A structural `update()` DOES animate like 2D
205
- * (enter fade, exit ghosts, churn-scaled local reheat), but it reaches the
206
- * impulse indirectly: `graphData()` forces alpha to 1 and the library exposes no
207
- * alpha setter, so `beginUpdateReheat` damps it from outside instead.
208
- */
209
-
210
183
  /**
211
184
  * The camera distance that `getCamera().k === 1` refers to. 3D has no zoom
212
185
  * scalar, so `k` is expressed as `FIT_DISTANCE / cameraDistance`: a `k` of 2 is
@@ -14,17 +14,18 @@ import {
14
14
  reheatAlpha,
15
15
  resolveHighlightTarget,
16
16
  seedNodePositions
17
- } from "../chunk-YBVDBY6P.js";
17
+ } from "../chunk-GX6BWMJD.js";
18
18
  import {
19
19
  AnimationScheduler,
20
20
  createTween,
21
21
  linear,
22
22
  prefersReducedMotion,
23
23
  resolveEase
24
- } from "../chunk-2OJZBMXM.js";
24
+ } from "../chunk-REH5UQCL.js";
25
25
  import {
26
- resolvedSurface
27
- } from "../chunk-NG7CVCI2.js";
26
+ resolvedSurface,
27
+ warnDeprecated
28
+ } from "../chunk-UC4IHEUO.js";
28
29
 
29
30
  // src/graph-3d/mount.ts
30
31
  import { buildEdgeTooltip } from "@opendata-ai/openchart-engine";
@@ -1689,7 +1690,10 @@ function createGraph3DRenderer(ctx) {
1689
1690
  update,
1690
1691
  // `update()` already routes an id-set-preserving change through the
1691
1692
  // material-mutation path, which never touches `graphData()`.
1692
- updateVisuals: update,
1693
+ updateVisuals: (spec) => {
1694
+ warnDeprecated("updateVisuals is deprecated; use update.");
1695
+ update(spec);
1696
+ },
1693
1697
  search,
1694
1698
  clearSearch,
1695
1699
  zoomToFit,