@opendata-ai/openchart-react 2.0.0 → 2.2.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/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { ChartLayout, ChartSpec, CompileOptions, TableLayout, TableSpec, VizSpec } from '@opendata-ai/openchart-engine';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import * as _opendata_ai_openchart_core from '@opendata-ai/openchart-core';
3
4
  import { ChartEventHandlers, ChartSpec, GraphSpec, ThemeConfig, DarkMode, TableSpec, SortState, ChartLayout, VizSpec } from '@opendata-ai/openchart-core';
4
5
  import * as react from 'react';
5
6
  import { CSSProperties, ReactNode } from 'react';
@@ -62,13 +63,6 @@ interface DataTableProps {
62
63
  */
63
64
  declare function DataTable({ spec, theme: themeProp, darkMode, onRowClick, onSortChange, onSearchChange, onPageChange, className, style, sort, search, page, }: DataTableProps): react_jsx_runtime.JSX.Element;
64
65
 
65
- /**
66
- * useGraph: hook for imperative graph control.
67
- *
68
- * Provides a ref to pass to <Graph /> and exposes graph methods
69
- * (search, zoom, select) for programmatic control of the graph instance.
70
- */
71
-
72
66
  interface UseGraphReturn {
73
67
  /** Ref to pass to <Graph ref={ref} />. */
74
68
  ref: React.RefObject<GraphHandle | null>;
@@ -93,6 +87,8 @@ interface GraphHandle {
93
87
  zoomToNode: (nodeId: string) => void;
94
88
  selectNode: (nodeId: string) => void;
95
89
  getSelectedNodes: () => string[];
90
+ /** Re-compile encoding/legend/chrome without restarting the simulation. */
91
+ updateVisuals: (spec: _opendata_ai_openchart_core.GraphSpec) => void;
96
92
  /** The underlying GraphInstance from the vanilla adapter. */
97
93
  instance: GraphInstance | null;
98
94
  }
@@ -118,6 +114,10 @@ interface GraphProps {
118
114
  onNodeClick?: (node: Record<string, unknown>) => void;
119
115
  /** Callback when a node is double-clicked. */
120
116
  onNodeDoubleClick?: (node: Record<string, unknown>) => void;
117
+ /** Callback when a node is hovered (null when hover ends). */
118
+ onNodeHover?: (node: Record<string, unknown> | null) => void;
119
+ /** Callback when an edge is hovered (null when hover ends). */
120
+ onEdgeHover?: (edge: Record<string, unknown> | null) => void;
121
121
  /** Callback when selection changes. */
122
122
  onSelectionChange?: (nodeIds: string[]) => void;
123
123
  /** CSS class name for the wrapper div. */
package/dist/index.js CHANGED
@@ -249,6 +249,8 @@ var Graph = forwardRef(function Graph2({
249
249
  darkMode,
250
250
  onNodeClick,
251
251
  onNodeDoubleClick,
252
+ onNodeHover,
253
+ onEdgeHover,
252
254
  onSelectionChange,
253
255
  className,
254
256
  style
@@ -261,7 +263,13 @@ var Graph = forwardRef(function Graph2({
261
263
  const graphRef = useRef3(null);
262
264
  const specRef = useRef3("");
263
265
  const handlersRef = useRef3({});
264
- handlersRef.current = { onNodeClick, onNodeDoubleClick, onSelectionChange };
266
+ handlersRef.current = {
267
+ onNodeClick,
268
+ onNodeDoubleClick,
269
+ onNodeHover,
270
+ onEdgeHover,
271
+ onSelectionChange
272
+ };
265
273
  const stableOnNodeClick = useCallback3(
266
274
  (node) => handlersRef.current.onNodeClick?.(node),
267
275
  []
@@ -270,6 +278,14 @@ var Graph = forwardRef(function Graph2({
270
278
  (node) => handlersRef.current.onNodeDoubleClick?.(node),
271
279
  []
272
280
  );
281
+ const stableOnNodeHover = useCallback3(
282
+ (node) => handlersRef.current.onNodeHover?.(node),
283
+ []
284
+ );
285
+ const stableOnEdgeHover = useCallback3(
286
+ (edge) => handlersRef.current.onEdgeHover?.(edge),
287
+ []
288
+ );
273
289
  const stableOnSelectionChange = useCallback3(
274
290
  (nodeIds) => handlersRef.current.onSelectionChange?.(nodeIds),
275
291
  []
@@ -295,6 +311,9 @@ var Graph = forwardRef(function Graph2({
295
311
  getSelectedNodes() {
296
312
  return graphRef.current?.getSelectedNodes() ?? [];
297
313
  },
314
+ updateVisuals(spec2) {
315
+ graphRef.current?.updateVisuals(spec2);
316
+ },
298
317
  get instance() {
299
318
  return graphRef.current;
300
319
  }
@@ -309,6 +328,8 @@ var Graph = forwardRef(function Graph2({
309
328
  darkMode: resolvedDarkMode,
310
329
  onNodeClick: stableOnNodeClick,
311
330
  onNodeDoubleClick: stableOnNodeDoubleClick,
331
+ onNodeHover: stableOnNodeHover,
332
+ onEdgeHover: stableOnEdgeHover,
312
333
  onSelectionChange: stableOnSelectionChange,
313
334
  responsive: true
314
335
  };
@@ -323,16 +344,32 @@ var Graph = forwardRef(function Graph2({
323
344
  resolvedDarkMode,
324
345
  stableOnNodeClick,
325
346
  stableOnNodeDoubleClick,
347
+ stableOnNodeHover,
348
+ stableOnEdgeHover,
326
349
  stableOnSelectionChange
327
350
  ]);
328
351
  useEffect3(() => {
329
352
  const graph = graphRef.current;
330
353
  if (!graph) return;
331
354
  const specString = JSON.stringify(spec);
332
- if (specString !== specRef.current) {
333
- specRef.current = specString;
334
- graph.update(spec);
355
+ if (specString === specRef.current) return;
356
+ const prevSpec = specRef.current;
357
+ specRef.current = specString;
358
+ if (prevSpec) {
359
+ try {
360
+ const prev = JSON.parse(prevSpec);
361
+ const sameNodes = prev.nodes.length === spec.nodes.length && prev.nodes.every((n, i) => n.id === spec.nodes[i].id);
362
+ const sameEdges = prev.edges.length === spec.edges.length && prev.edges.every(
363
+ (e, i) => e.source === spec.edges[i].source && e.target === spec.edges[i].target
364
+ );
365
+ if (sameNodes && sameEdges) {
366
+ graph.updateVisuals(spec);
367
+ return;
368
+ }
369
+ } catch {
370
+ }
335
371
  }
372
+ graph.update(spec);
336
373
  }, [spec]);
337
374
  return /* @__PURE__ */ jsx4(
338
375
  "div",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/Chart.tsx","../src/ThemeContext.tsx","../src/DataTable.tsx","../src/Graph.tsx","../src/hooks.ts","../src/hooks/useGraph.ts","../src/hooks/useTable.ts","../src/hooks/useTableState.ts","../src/Visualization.tsx"],"sourcesContent":["/**\n * React Chart component: thin wrapper around the vanilla adapter.\n *\n * Mounts a chart instance on render, updates when spec changes,\n * and cleans up on unmount. All heavy lifting is done by the vanilla\n * createChart() function.\n */\n\nimport type {\n ChartEventHandlers,\n ChartSpec,\n DarkMode,\n GraphSpec,\n ThemeConfig,\n} from '@opendata-ai/openchart-core';\nimport { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';\nimport { type CSSProperties, useCallback, useEffect, useRef } from 'react';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface ChartProps extends ChartEventHandlers {\n /** The visualization spec to render. */\n spec: ChartSpec | GraphSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Callback when a data point is clicked. @deprecated Use onMarkClick instead. */\n onDataPointClick?: (data: Record<string, unknown>) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * React component that renders a visualization from a spec.\n *\n * Uses the vanilla adapter internally. The spec is compiled and rendered\n * as SVG inside a wrapper div. Spec changes trigger re-renders via the\n * vanilla adapter's update() method.\n */\nexport function Chart({\n spec,\n theme: themeProp,\n darkMode,\n onDataPointClick,\n onMarkClick,\n onMarkHover,\n onMarkLeave,\n onLegendToggle,\n onAnnotationClick,\n onAnnotationEdit,\n onEdit,\n className,\n style,\n}: ChartProps) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<ChartInstance | null>(null);\n const specRef = useRef<string>('');\n\n // Store event handlers in refs so they don't trigger chart recreation.\n // Inline arrow functions create new references every render, which would\n // destroy and recreate the entire chart instance without this pattern.\n const handlersRef = useRef<{\n onDataPointClick?: ChartProps['onDataPointClick'];\n onMarkClick?: ChartProps['onMarkClick'];\n onMarkHover?: ChartProps['onMarkHover'];\n onMarkLeave?: ChartProps['onMarkLeave'];\n onLegendToggle?: ChartProps['onLegendToggle'];\n onAnnotationClick?: ChartProps['onAnnotationClick'];\n onAnnotationEdit?: ChartProps['onAnnotationEdit'];\n onEdit?: ChartProps['onEdit'];\n }>({});\n handlersRef.current = {\n onDataPointClick,\n onMarkClick,\n onMarkHover,\n onMarkLeave,\n onLegendToggle,\n onAnnotationClick,\n onAnnotationEdit,\n onEdit,\n };\n\n // Stable callback wrappers that read from refs\n const stableOnDataPointClick = useCallback(\n (data: Record<string, unknown>) => handlersRef.current.onDataPointClick?.(data),\n [],\n );\n const stableOnMarkClick = useCallback(\n (event: import('@opendata-ai/openchart-core').MarkEvent) =>\n handlersRef.current.onMarkClick?.(event),\n [],\n );\n const stableOnMarkHover = useCallback(\n (event: import('@opendata-ai/openchart-core').MarkEvent) =>\n handlersRef.current.onMarkHover?.(event),\n [],\n );\n const stableOnMarkLeave = useCallback(() => handlersRef.current.onMarkLeave?.(), []);\n const stableOnLegendToggle = useCallback(\n (series: string, visible: boolean) => handlersRef.current.onLegendToggle?.(series, visible),\n [],\n );\n const stableOnAnnotationClick = useCallback(\n (annotation: import('@opendata-ai/openchart-core').Annotation, event: MouseEvent) =>\n handlersRef.current.onAnnotationClick?.(annotation, event),\n [],\n );\n const stableOnAnnotationEdit = useCallback(\n (\n annotation: import('@opendata-ai/openchart-core').TextAnnotation,\n updatedOffset: import('@opendata-ai/openchart-core').AnnotationOffset,\n ) => handlersRef.current.onAnnotationEdit?.(annotation, updatedOffset),\n [],\n );\n const stableOnEdit = useCallback(\n (edit: import('@opendata-ai/openchart-core').ElementEdit) => handlersRef.current.onEdit?.(edit),\n [],\n );\n\n // Mount chart and recreate when theme/darkMode change.\n // Event handlers use stable refs so they don't trigger recreation.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in Effect 2\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const options: MountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onDataPointClick: stableOnDataPointClick,\n onMarkClick: stableOnMarkClick,\n onMarkHover: stableOnMarkHover,\n onMarkLeave: stableOnMarkLeave,\n onLegendToggle: stableOnLegendToggle,\n onAnnotationClick: stableOnAnnotationClick,\n // Only include editing callbacks when the consumer provides them.\n // The stable wrappers are always truthy, so gating on handlersRef\n // avoids adding unstable prop references to the effect deps.\n ...(handlersRef.current.onAnnotationEdit ? { onAnnotationEdit: stableOnAnnotationEdit } : {}),\n ...(handlersRef.current.onEdit ? { onEdit: stableOnEdit } : {}),\n responsive: true,\n };\n\n chartRef.current = createChart(container, spec, options);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n chartRef.current?.destroy();\n chartRef.current = null;\n };\n // Only recreate when theme or darkMode change. Event handlers use stable refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n theme,\n resolvedDarkMode,\n stableOnAnnotationClick,\n stableOnDataPointClick,\n stableOnEdit,\n stableOnLegendToggle,\n stableOnMarkClick,\n stableOnMarkHover,\n stableOnMarkLeave,\n stableOnAnnotationEdit,\n ]);\n\n // Update chart when spec changes\n useEffect(() => {\n const chart = chartRef.current;\n if (!chart) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== specRef.current) {\n specRef.current = specString;\n chart.update(spec);\n }\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-chart-root ${className}` : 'viz-chart-root'}\n style={style}\n />\n );\n}\n","/**\n * Theme context: provides a theme and dark mode preference to all\n * descendant Chart, DataTable, and Graph components without prop drilling.\n *\n * Components use the context values as fallbacks when no explicit\n * `theme` or `darkMode` prop is passed.\n */\n\nimport type { DarkMode, ThemeConfig } from '@opendata-ai/openchart-core';\nimport { createContext, type ReactNode, useContext } from 'react';\n\nconst VizThemeContext = createContext<ThemeConfig | undefined>(undefined);\nconst VizDarkModeContext = createContext<DarkMode | undefined>(undefined);\n\n/** Read the current theme from the nearest VizThemeProvider. */\nexport function useVizTheme(): ThemeConfig | undefined {\n return useContext(VizThemeContext);\n}\n\n/** Read the current dark mode preference from the nearest VizThemeProvider. */\nexport function useVizDarkMode(): DarkMode | undefined {\n return useContext(VizDarkModeContext);\n}\n\nexport interface VizThemeProviderProps {\n /** Theme config to provide to descendant viz components. */\n theme: ThemeConfig | undefined;\n /** Dark mode preference to provide to descendant viz components. */\n darkMode?: DarkMode;\n children: ReactNode;\n}\n\n/** Provides a theme and dark mode preference to all nested Chart, DataTable, and Graph components. */\nexport function VizThemeProvider({ theme, darkMode, children }: VizThemeProviderProps) {\n return (\n <VizThemeContext.Provider value={theme}>\n <VizDarkModeContext.Provider value={darkMode}>{children}</VizDarkModeContext.Provider>\n </VizThemeContext.Provider>\n );\n}\n","/**\n * DataTable component: React wrapper around the vanilla table adapter.\n *\n * Mounts a table instance on render, updates when spec changes,\n * and cleans up on unmount. Supports both controlled and uncontrolled modes\n * for sort, search, and pagination state.\n */\n\nimport type { DarkMode, SortState, TableSpec, ThemeConfig } from '@opendata-ai/openchart-core';\nimport {\n createTable,\n type TableInstance,\n type TableMountOptions,\n} from '@opendata-ai/openchart-vanilla';\nimport { type CSSProperties, useCallback, useEffect, useRef } from 'react';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface DataTableProps {\n /** The table spec to render. */\n spec: TableSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Row click handler. */\n onRowClick?: (row: Record<string, unknown>) => void;\n /** Callback when sort changes. */\n onSortChange?: (sort: SortState | null) => void;\n /** Callback when search changes. */\n onSearchChange?: (query: string) => void;\n /** Callback when page changes. */\n onPageChange?: (page: number) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n /** Controlled sort state. */\n sort?: SortState | null;\n /** Controlled search query. */\n search?: string;\n /** Controlled page number. */\n page?: number;\n}\n\n/**\n * React component that renders a data table from a TableSpec.\n *\n * Uses the vanilla adapter internally. Supports controlled and uncontrolled\n * modes for sort, search, and pagination state.\n */\nexport function DataTable({\n spec,\n theme: themeProp,\n darkMode,\n onRowClick,\n onSortChange,\n onSearchChange,\n onPageChange,\n className,\n style,\n sort,\n search,\n page,\n}: DataTableProps) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const tableRef = useRef<TableInstance | null>(null);\n\n // Store event handlers in refs so they don't trigger table recreation.\n const handlersRef = useRef<{\n onRowClick?: DataTableProps['onRowClick'];\n onSortChange?: DataTableProps['onSortChange'];\n onSearchChange?: DataTableProps['onSearchChange'];\n onPageChange?: DataTableProps['onPageChange'];\n }>({});\n handlersRef.current = { onRowClick, onSortChange, onSearchChange, onPageChange };\n\n // Stable callback wrappers that read from refs\n const stableOnRowClick = useCallback(\n (row: Record<string, unknown>) => handlersRef.current.onRowClick?.(row),\n [],\n );\n const stableOnStateChange = useCallback(\n (state: { sort?: SortState | null; search?: string; page?: number }) => {\n if (state.sort !== undefined) handlersRef.current.onSortChange?.(state.sort);\n if (state.search !== undefined) handlersRef.current.onSearchChange?.(state.search);\n if (state.page !== undefined) handlersRef.current.onPageChange?.(state.page);\n },\n [],\n );\n\n const prevSpecRef = useRef<string>('');\n\n // Determine if we're in controlled mode\n const isControlled = sort !== undefined || search !== undefined || page !== undefined;\n\n // Effect 1: Mount/unmount. Only recreate when structural options change.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec, sort, search, page intentionally excluded - handled via update()/setState() in Effects 2-3\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const mountOptions: TableMountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onRowClick: stableOnRowClick,\n responsive: true,\n onStateChange: stableOnStateChange,\n };\n\n if (isControlled) {\n mountOptions.externalState = {\n sort: sort ?? null,\n search: search ?? '',\n page: page ?? 0,\n };\n }\n\n tableRef.current = createTable(container, spec, mountOptions);\n prevSpecRef.current = JSON.stringify(spec);\n\n return () => {\n tableRef.current?.destroy();\n tableRef.current = null;\n };\n // Only recreate on structural option changes (theme, darkMode, onRowClick).\n // Controlled state updates are handled in Effect 2.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, resolvedDarkMode, isControlled, stableOnRowClick, stableOnStateChange]);\n\n // Effect 2: Sync controlled state without remounting.\n useEffect(() => {\n const table = tableRef.current;\n if (!table || !isControlled) return;\n\n table.setState({\n sort: sort ?? null,\n search: search ?? '',\n page: page ?? 0,\n });\n }, [sort, search, page, isControlled]);\n\n // Effect 3: Sync spec changes via update().\n useEffect(() => {\n const table = tableRef.current;\n if (!table) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== prevSpecRef.current) {\n prevSpecRef.current = specString;\n table.update(spec);\n }\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-table-root ${className}` : 'viz-table-root'}\n style={style}\n />\n );\n}\n","/**\n * React Graph component: thin wrapper around the vanilla adapter.\n *\n * Mounts a graph instance on render, updates when spec changes,\n * and cleans up on unmount. All heavy lifting is done by the vanilla\n * createGraph() function.\n *\n * Supports forwardRef for imperative control via useGraph() hook.\n */\n\nimport type { DarkMode, GraphSpec, ThemeConfig } from '@opendata-ai/openchart-core';\nimport {\n createGraph,\n type GraphInstance,\n type GraphMountOptions,\n} from '@opendata-ai/openchart-vanilla';\nimport {\n type CSSProperties,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react';\nimport type { GraphHandle } from './hooks/useGraph';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface GraphProps {\n /** The graph spec to render. */\n spec: GraphSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Callback when a node is clicked. */\n onNodeClick?: (node: Record<string, unknown>) => void;\n /** Callback when a node is double-clicked. */\n onNodeDoubleClick?: (node: Record<string, unknown>) => void;\n /** Callback when selection changes. */\n onSelectionChange?: (nodeIds: string[]) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * React component that renders a force-directed graph from a GraphSpec.\n *\n * Uses the vanilla adapter internally. The spec is compiled and rendered\n * on a canvas inside a wrapper div. Spec changes trigger re-renders via the\n * vanilla adapter's update() method.\n *\n * Supports ref for imperative control via useGraph() hook:\n * ```tsx\n * const { ref, search, zoomToFit } = useGraph();\n * return <Graph ref={ref} spec={spec} />;\n * ```\n */\nexport const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(\n {\n spec,\n theme: themeProp,\n darkMode,\n onNodeClick,\n onNodeDoubleClick,\n onSelectionChange,\n className,\n style,\n },\n ref,\n) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const graphRef = useRef<GraphInstance | null>(null);\n const specRef = useRef<string>('');\n\n // Store event handlers in refs so they don't trigger graph recreation.\n // Inline arrow functions create new references every render, which would\n // destroy and recreate the entire graph instance without this pattern.\n const handlersRef = useRef<{\n onNodeClick?: GraphProps['onNodeClick'];\n onNodeDoubleClick?: GraphProps['onNodeDoubleClick'];\n onSelectionChange?: GraphProps['onSelectionChange'];\n }>({});\n handlersRef.current = { onNodeClick, onNodeDoubleClick, onSelectionChange };\n\n // Stable callback wrappers that read from refs\n const stableOnNodeClick = useCallback(\n (node: Record<string, unknown>) => handlersRef.current.onNodeClick?.(node),\n [],\n );\n const stableOnNodeDoubleClick = useCallback(\n (node: Record<string, unknown>) => handlersRef.current.onNodeDoubleClick?.(node),\n [],\n );\n const stableOnSelectionChange = useCallback(\n (nodeIds: string[]) => handlersRef.current.onSelectionChange?.(nodeIds),\n [],\n );\n\n // Expose imperative handle for useGraph() hook\n useImperativeHandle(\n ref,\n () => ({\n search(query: string) {\n graphRef.current?.search(query);\n },\n clearSearch() {\n graphRef.current?.clearSearch();\n },\n zoomToFit() {\n graphRef.current?.zoomToFit();\n },\n zoomToNode(nodeId: string) {\n graphRef.current?.zoomToNode(nodeId);\n },\n selectNode(nodeId: string) {\n graphRef.current?.selectNode(nodeId);\n },\n getSelectedNodes() {\n return graphRef.current?.getSelectedNodes() ?? [];\n },\n get instance() {\n return graphRef.current;\n },\n }),\n [],\n );\n\n // Mount graph and recreate when theme/darkMode change.\n // Event handlers use stable refs so they don't trigger recreation.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in Effect 2\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const options: GraphMountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onNodeClick: stableOnNodeClick,\n onNodeDoubleClick: stableOnNodeDoubleClick,\n onSelectionChange: stableOnSelectionChange,\n responsive: true,\n };\n\n graphRef.current = createGraph(container, spec, options);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n graphRef.current?.destroy();\n graphRef.current = null;\n };\n // Only recreate when theme or darkMode change. Event handlers use stable refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n theme,\n resolvedDarkMode,\n stableOnNodeClick,\n stableOnNodeDoubleClick,\n stableOnSelectionChange,\n ]);\n\n // Update graph when spec changes\n useEffect(() => {\n const graph = graphRef.current;\n if (!graph) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== specRef.current) {\n specRef.current = specString;\n graph.update(spec);\n }\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-graph-root ${className}` : 'viz-graph-root'}\n style={style}\n />\n );\n});\n","/**\n * React hooks for chart lifecycle and dark mode resolution.\n *\n * useChart: manual control over a chart instance (for advanced usage).\n * useDarkMode: resolves the DarkMode preference to a boolean.\n */\n\nimport type { ChartLayout, ChartSpec, DarkMode, GraphSpec } from '@opendata-ai/openchart-core';\nimport { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';\nimport { useEffect, useRef, useState } from 'react';\n\n// ---------------------------------------------------------------------------\n// useChart\n// ---------------------------------------------------------------------------\n\nexport interface UseChartOptions {\n /** Theme overrides. */\n theme?: MountOptions['theme'];\n /** Dark mode setting. */\n darkMode?: MountOptions['darkMode'];\n /** Data point click handler. */\n onDataPointClick?: MountOptions['onDataPointClick'];\n /** Enable responsive resizing. Defaults to true. */\n responsive?: boolean;\n}\n\nexport interface UseChartReturn {\n /** Ref to attach to the container div. */\n ref: React.RefObject<HTMLDivElement | null>;\n /** The chart instance (null until mounted). */\n chart: ChartInstance | null;\n /** The current compiled layout (null until mounted). */\n layout: ChartLayout | null;\n}\n\n/**\n * Hook for manual chart lifecycle control.\n *\n * Attach the returned ref to a container div. The chart mounts\n * automatically and updates when the spec changes.\n *\n * @param spec - The visualization spec.\n * @param options - Mount options.\n * @returns { ref, chart, layout }\n */\nexport function useChart(spec: ChartSpec | GraphSpec, options?: UseChartOptions): UseChartReturn {\n const ref = useRef<HTMLDivElement | null>(null);\n const chartRef = useRef<ChartInstance | null>(null);\n const [layout, setLayout] = useState<ChartLayout | null>(null);\n const specRef = useRef<string>('');\n\n // Mount / unmount\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in the update effect\n useEffect(() => {\n const container = ref.current;\n if (!container) return;\n\n const mountOpts: MountOptions = {\n theme: options?.theme,\n darkMode: options?.darkMode,\n onDataPointClick: options?.onDataPointClick,\n responsive: options?.responsive,\n };\n\n const chart = createChart(container, spec, mountOpts);\n chartRef.current = chart;\n setLayout(chart.layout);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n chart.destroy();\n chartRef.current = null;\n setLayout(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [options?.theme, options?.darkMode, options?.onDataPointClick, options?.responsive]);\n\n // Update on spec change\n useEffect(() => {\n const chart = chartRef.current;\n if (!chart) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== specRef.current) {\n specRef.current = specString;\n chart.update(spec);\n setLayout(chart.layout);\n }\n }, [spec]);\n\n return {\n ref,\n chart: chartRef.current,\n layout,\n };\n}\n\n// ---------------------------------------------------------------------------\n// useDarkMode\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a DarkMode preference to a boolean.\n *\n * - \"force\" -> true\n * - \"off\" -> false\n * - \"auto\" -> matches system preference (reactive to changes)\n *\n * @param mode - The dark mode preference.\n * @returns Whether dark mode is active.\n */\nexport function useDarkMode(mode?: DarkMode): boolean {\n const [isDark, setIsDark] = useState(() => resolveInitial(mode));\n\n useEffect(() => {\n if (mode !== 'auto') {\n setIsDark(mode === 'force');\n return;\n }\n\n if (typeof window === 'undefined' || !window.matchMedia) {\n setIsDark(false);\n return;\n }\n\n const mq = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mq.matches);\n\n const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);\n mq.addEventListener('change', handler);\n return () => mq.removeEventListener('change', handler);\n }, [mode]);\n\n return isDark;\n}\n\nfunction resolveInitial(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n if (mode === 'off' || mode === undefined) return false;\n // \"auto\"\n if (typeof window !== 'undefined' && window.matchMedia) {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n return false;\n}\n","/**\n * useGraph: hook for imperative graph control.\n *\n * Provides a ref to pass to <Graph /> and exposes graph methods\n * (search, zoom, select) for programmatic control of the graph instance.\n */\n\nimport type { GraphInstance } from '@opendata-ai/openchart-vanilla';\nimport { useCallback, useRef } from 'react';\n\nexport interface UseGraphReturn {\n /** Ref to pass to <Graph ref={ref} />. */\n ref: React.RefObject<GraphHandle | null>;\n /** Search for nodes matching a query string. */\n search: (query: string) => void;\n /** Clear the current search. */\n clearSearch: () => void;\n /** Zoom to fit all nodes in view. */\n zoomToFit: () => void;\n /** Zoom and center on a specific node. */\n zoomToNode: (nodeId: string) => void;\n /** Select a node by id. */\n selectNode: (nodeId: string) => void;\n /** Get the currently selected node ids. */\n getSelectedNodes: () => string[];\n}\n\n/** Handle exposed by Graph component via forwardRef. */\nexport interface GraphHandle {\n search: (query: string) => void;\n clearSearch: () => void;\n zoomToFit: () => void;\n zoomToNode: (nodeId: string) => void;\n selectNode: (nodeId: string) => void;\n getSelectedNodes: () => string[];\n /** The underlying GraphInstance from the vanilla adapter. */\n instance: GraphInstance | null;\n}\n\n/**\n * Hook for imperative graph control.\n *\n * Usage:\n * ```tsx\n * const { ref, search, zoomToFit } = useGraph();\n * return <Graph ref={ref} spec={spec} />;\n * ```\n */\nexport function useGraph(): UseGraphReturn {\n const ref = useRef<GraphHandle | null>(null);\n\n const search = useCallback((query: string) => {\n ref.current?.search(query);\n }, []);\n\n const clearSearch = useCallback(() => {\n ref.current?.clearSearch();\n }, []);\n\n const zoomToFit = useCallback(() => {\n ref.current?.zoomToFit();\n }, []);\n\n const zoomToNode = useCallback((nodeId: string) => {\n ref.current?.zoomToNode(nodeId);\n }, []);\n\n const selectNode = useCallback((nodeId: string) => {\n ref.current?.selectNode(nodeId);\n }, []);\n\n const getSelectedNodes = useCallback((): string[] => {\n return ref.current?.getSelectedNodes() ?? [];\n }, []);\n\n return {\n ref,\n search,\n clearSearch,\n zoomToFit,\n zoomToNode,\n selectNode,\n getSelectedNodes,\n };\n}\n","/**\n * useTable: hook for manual table lifecycle control.\n *\n * Attaches to a container ref, mounts a vanilla table instance,\n * and exposes the instance and current state.\n */\n\nimport type { TableSpec } from '@opendata-ai/openchart-core';\nimport {\n createTable,\n type TableInstance,\n type TableMountOptions,\n type TableState,\n} from '@opendata-ai/openchart-vanilla';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface UseTableReturn {\n /** Ref to attach to the container div. */\n ref: React.RefObject<HTMLDivElement | null>;\n /** The table instance (null until mounted). */\n table: TableInstance | null;\n /** The current table state (sort, search, page). */\n state: TableState;\n}\n\n/**\n * Hook for manual table lifecycle control.\n *\n * Attach the returned ref to a container div. The table mounts\n * automatically and updates when the spec changes.\n *\n * @param spec - The table spec.\n * @param options - Mount options.\n * @returns { ref, table, state }\n */\nexport function useTable(spec: TableSpec, options?: TableMountOptions): UseTableReturn {\n const ref = useRef<HTMLDivElement | null>(null);\n const tableRef = useRef<TableInstance | null>(null);\n const [state, setState] = useState<TableState>({\n sort: null,\n search: '',\n page: 0,\n });\n\n const originalOnStateChange = options?.onStateChange;\n\n const handleStateChange = useCallback(\n (newState: TableState) => {\n setState(newState);\n originalOnStateChange?.(newState);\n },\n [originalOnStateChange],\n );\n\n // Mount / unmount\n useEffect(() => {\n const container = ref.current;\n if (!container) return;\n\n const mountOpts: TableMountOptions = {\n ...options,\n onStateChange: handleStateChange,\n };\n\n const table = createTable(container, spec, mountOpts);\n tableRef.current = table;\n setState(table.getState());\n\n return () => {\n table.destroy();\n tableRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n options?.theme,\n options?.darkMode,\n options?.onRowClick,\n options?.responsive,\n handleStateChange,\n options,\n spec,\n ]);\n\n // Update on spec change\n useEffect(() => {\n const table = tableRef.current;\n if (!table) return;\n\n table.update(spec);\n setState(table.getState());\n }, [spec]);\n\n return {\n ref,\n table: tableRef.current,\n state,\n };\n}\n","/**\n * useTableState: managed state hook for controlled table usage.\n *\n * Provides individual sort/search/page state with setters and a\n * resetState function to return to initial values.\n */\n\nimport type { SortState } from '@opendata-ai/openchart-core';\nimport { useCallback, useState } from 'react';\n\nexport interface UseTableStateReturn {\n sort: SortState | null;\n setSort: (sort: SortState | null) => void;\n search: string;\n setSearch: (query: string) => void;\n page: number;\n setPage: (page: number) => void;\n resetState: () => void;\n}\n\nexport interface UseTableStateOptions {\n sort?: SortState | null;\n search?: string;\n page?: number;\n}\n\n/**\n * Hook for managing table state (sort, search, page).\n *\n * Use with the DataTable component's controlled props:\n * ```tsx\n * const { sort, search, page, setSort, setSearch, setPage } = useTableState();\n * <DataTable\n * spec={spec}\n * sort={sort}\n * search={search}\n * page={page}\n * onSortChange={setSort}\n * onSearchChange={setSearch}\n * onPageChange={setPage}\n * />\n * ```\n */\nexport function useTableState(initialState?: UseTableStateOptions): UseTableStateReturn {\n const [sort, setSortInternal] = useState<SortState | null>(initialState?.sort ?? null);\n const [search, setSearchInternal] = useState(initialState?.search ?? '');\n const [page, setPageInternal] = useState(initialState?.page ?? 0);\n\n const setSort = useCallback((newSort: SortState | null) => {\n setSortInternal(newSort);\n }, []);\n\n const setSearch = useCallback((query: string) => {\n setSearchInternal(query);\n }, []);\n\n const setPage = useCallback((newPage: number) => {\n setPageInternal(newPage);\n }, []);\n\n const resetState = useCallback(() => {\n setSortInternal(initialState?.sort ?? null);\n setSearchInternal(initialState?.search ?? '');\n setPageInternal(initialState?.page ?? 0);\n }, [initialState?.sort, initialState?.search, initialState?.page]);\n\n return {\n sort,\n setSort,\n search,\n setSearch,\n page,\n setPage,\n resetState,\n };\n}\n","/**\n * Visualization routing component: renders Chart, DataTable, or Graph\n * based on the spec type. Use this when rendering arbitrary VizSpec values.\n *\n * For event handlers, use the specific component (Chart, DataTable, Graph) directly.\n */\n\nimport type { DarkMode, ThemeConfig, VizSpec } from '@opendata-ai/openchart-core';\nimport { isGraphSpec, isTableSpec } from '@opendata-ai/openchart-core';\nimport type { CSSProperties } from 'react';\nimport { Chart } from './Chart';\nimport { DataTable } from './DataTable';\nimport { Graph } from './Graph';\n\nexport interface VisualizationProps {\n /** The visualization spec to render. */\n spec: VizSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * Routes a VizSpec to the appropriate rendering component.\n *\n * Accepts any VizSpec and renders it with the correct component based on the\n * spec's type field. For event handlers, use Chart, DataTable, or Graph directly.\n */\nexport function Visualization({ spec, theme, darkMode, className, style }: VisualizationProps) {\n if (isTableSpec(spec)) {\n return (\n <DataTable\n spec={spec}\n theme={theme}\n darkMode={darkMode}\n className={className}\n style={style}\n />\n );\n }\n if (isGraphSpec(spec)) {\n return (\n <Graph spec={spec} theme={theme} darkMode={darkMode} className={className} style={style} />\n );\n }\n return (\n <Chart spec={spec} theme={theme} darkMode={darkMode} className={className} style={style} />\n );\n}\n"],"mappings":"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","names":["jsx","useCallback","useEffect","useRef","jsx","useRef","useCallback","useEffect","useCallback","useEffect","useRef","jsx","Graph","useRef","useCallback","useEffect","createChart","useEffect","useRef","useCallback","useRef","createTable","useCallback","useEffect","useRef","useState","useCallback","useState","jsx"]}
1
+ {"version":3,"sources":["../src/Chart.tsx","../src/ThemeContext.tsx","../src/DataTable.tsx","../src/Graph.tsx","../src/hooks.ts","../src/hooks/useGraph.ts","../src/hooks/useTable.ts","../src/hooks/useTableState.ts","../src/Visualization.tsx"],"sourcesContent":["/**\n * React Chart component: thin wrapper around the vanilla adapter.\n *\n * Mounts a chart instance on render, updates when spec changes,\n * and cleans up on unmount. All heavy lifting is done by the vanilla\n * createChart() function.\n */\n\nimport type {\n ChartEventHandlers,\n ChartSpec,\n DarkMode,\n GraphSpec,\n ThemeConfig,\n} from '@opendata-ai/openchart-core';\nimport { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';\nimport { type CSSProperties, useCallback, useEffect, useRef } from 'react';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface ChartProps extends ChartEventHandlers {\n /** The visualization spec to render. */\n spec: ChartSpec | GraphSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Callback when a data point is clicked. @deprecated Use onMarkClick instead. */\n onDataPointClick?: (data: Record<string, unknown>) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * React component that renders a visualization from a spec.\n *\n * Uses the vanilla adapter internally. The spec is compiled and rendered\n * as SVG inside a wrapper div. Spec changes trigger re-renders via the\n * vanilla adapter's update() method.\n */\nexport function Chart({\n spec,\n theme: themeProp,\n darkMode,\n onDataPointClick,\n onMarkClick,\n onMarkHover,\n onMarkLeave,\n onLegendToggle,\n onAnnotationClick,\n onAnnotationEdit,\n onEdit,\n className,\n style,\n}: ChartProps) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const chartRef = useRef<ChartInstance | null>(null);\n const specRef = useRef<string>('');\n\n // Store event handlers in refs so they don't trigger chart recreation.\n // Inline arrow functions create new references every render, which would\n // destroy and recreate the entire chart instance without this pattern.\n const handlersRef = useRef<{\n onDataPointClick?: ChartProps['onDataPointClick'];\n onMarkClick?: ChartProps['onMarkClick'];\n onMarkHover?: ChartProps['onMarkHover'];\n onMarkLeave?: ChartProps['onMarkLeave'];\n onLegendToggle?: ChartProps['onLegendToggle'];\n onAnnotationClick?: ChartProps['onAnnotationClick'];\n onAnnotationEdit?: ChartProps['onAnnotationEdit'];\n onEdit?: ChartProps['onEdit'];\n }>({});\n handlersRef.current = {\n onDataPointClick,\n onMarkClick,\n onMarkHover,\n onMarkLeave,\n onLegendToggle,\n onAnnotationClick,\n onAnnotationEdit,\n onEdit,\n };\n\n // Stable callback wrappers that read from refs\n const stableOnDataPointClick = useCallback(\n (data: Record<string, unknown>) => handlersRef.current.onDataPointClick?.(data),\n [],\n );\n const stableOnMarkClick = useCallback(\n (event: import('@opendata-ai/openchart-core').MarkEvent) =>\n handlersRef.current.onMarkClick?.(event),\n [],\n );\n const stableOnMarkHover = useCallback(\n (event: import('@opendata-ai/openchart-core').MarkEvent) =>\n handlersRef.current.onMarkHover?.(event),\n [],\n );\n const stableOnMarkLeave = useCallback(() => handlersRef.current.onMarkLeave?.(), []);\n const stableOnLegendToggle = useCallback(\n (series: string, visible: boolean) => handlersRef.current.onLegendToggle?.(series, visible),\n [],\n );\n const stableOnAnnotationClick = useCallback(\n (annotation: import('@opendata-ai/openchart-core').Annotation, event: MouseEvent) =>\n handlersRef.current.onAnnotationClick?.(annotation, event),\n [],\n );\n const stableOnAnnotationEdit = useCallback(\n (\n annotation: import('@opendata-ai/openchart-core').TextAnnotation,\n updatedOffset: import('@opendata-ai/openchart-core').AnnotationOffset,\n ) => handlersRef.current.onAnnotationEdit?.(annotation, updatedOffset),\n [],\n );\n const stableOnEdit = useCallback(\n (edit: import('@opendata-ai/openchart-core').ElementEdit) => handlersRef.current.onEdit?.(edit),\n [],\n );\n\n // Mount chart and recreate when theme/darkMode change.\n // Event handlers use stable refs so they don't trigger recreation.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in Effect 2\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const options: MountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onDataPointClick: stableOnDataPointClick,\n onMarkClick: stableOnMarkClick,\n onMarkHover: stableOnMarkHover,\n onMarkLeave: stableOnMarkLeave,\n onLegendToggle: stableOnLegendToggle,\n onAnnotationClick: stableOnAnnotationClick,\n // Only include editing callbacks when the consumer provides them.\n // The stable wrappers are always truthy, so gating on handlersRef\n // avoids adding unstable prop references to the effect deps.\n ...(handlersRef.current.onAnnotationEdit ? { onAnnotationEdit: stableOnAnnotationEdit } : {}),\n ...(handlersRef.current.onEdit ? { onEdit: stableOnEdit } : {}),\n responsive: true,\n };\n\n chartRef.current = createChart(container, spec, options);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n chartRef.current?.destroy();\n chartRef.current = null;\n };\n // Only recreate when theme or darkMode change. Event handlers use stable refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n theme,\n resolvedDarkMode,\n stableOnAnnotationClick,\n stableOnDataPointClick,\n stableOnEdit,\n stableOnLegendToggle,\n stableOnMarkClick,\n stableOnMarkHover,\n stableOnMarkLeave,\n stableOnAnnotationEdit,\n ]);\n\n // Update chart when spec changes\n useEffect(() => {\n const chart = chartRef.current;\n if (!chart) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== specRef.current) {\n specRef.current = specString;\n chart.update(spec);\n }\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-chart-root ${className}` : 'viz-chart-root'}\n style={style}\n />\n );\n}\n","/**\n * Theme context: provides a theme and dark mode preference to all\n * descendant Chart, DataTable, and Graph components without prop drilling.\n *\n * Components use the context values as fallbacks when no explicit\n * `theme` or `darkMode` prop is passed.\n */\n\nimport type { DarkMode, ThemeConfig } from '@opendata-ai/openchart-core';\nimport { createContext, type ReactNode, useContext } from 'react';\n\nconst VizThemeContext = createContext<ThemeConfig | undefined>(undefined);\nconst VizDarkModeContext = createContext<DarkMode | undefined>(undefined);\n\n/** Read the current theme from the nearest VizThemeProvider. */\nexport function useVizTheme(): ThemeConfig | undefined {\n return useContext(VizThemeContext);\n}\n\n/** Read the current dark mode preference from the nearest VizThemeProvider. */\nexport function useVizDarkMode(): DarkMode | undefined {\n return useContext(VizDarkModeContext);\n}\n\nexport interface VizThemeProviderProps {\n /** Theme config to provide to descendant viz components. */\n theme: ThemeConfig | undefined;\n /** Dark mode preference to provide to descendant viz components. */\n darkMode?: DarkMode;\n children: ReactNode;\n}\n\n/** Provides a theme and dark mode preference to all nested Chart, DataTable, and Graph components. */\nexport function VizThemeProvider({ theme, darkMode, children }: VizThemeProviderProps) {\n return (\n <VizThemeContext.Provider value={theme}>\n <VizDarkModeContext.Provider value={darkMode}>{children}</VizDarkModeContext.Provider>\n </VizThemeContext.Provider>\n );\n}\n","/**\n * DataTable component: React wrapper around the vanilla table adapter.\n *\n * Mounts a table instance on render, updates when spec changes,\n * and cleans up on unmount. Supports both controlled and uncontrolled modes\n * for sort, search, and pagination state.\n */\n\nimport type { DarkMode, SortState, TableSpec, ThemeConfig } from '@opendata-ai/openchart-core';\nimport {\n createTable,\n type TableInstance,\n type TableMountOptions,\n} from '@opendata-ai/openchart-vanilla';\nimport { type CSSProperties, useCallback, useEffect, useRef } from 'react';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface DataTableProps {\n /** The table spec to render. */\n spec: TableSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Row click handler. */\n onRowClick?: (row: Record<string, unknown>) => void;\n /** Callback when sort changes. */\n onSortChange?: (sort: SortState | null) => void;\n /** Callback when search changes. */\n onSearchChange?: (query: string) => void;\n /** Callback when page changes. */\n onPageChange?: (page: number) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n /** Controlled sort state. */\n sort?: SortState | null;\n /** Controlled search query. */\n search?: string;\n /** Controlled page number. */\n page?: number;\n}\n\n/**\n * React component that renders a data table from a TableSpec.\n *\n * Uses the vanilla adapter internally. Supports controlled and uncontrolled\n * modes for sort, search, and pagination state.\n */\nexport function DataTable({\n spec,\n theme: themeProp,\n darkMode,\n onRowClick,\n onSortChange,\n onSearchChange,\n onPageChange,\n className,\n style,\n sort,\n search,\n page,\n}: DataTableProps) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const tableRef = useRef<TableInstance | null>(null);\n\n // Store event handlers in refs so they don't trigger table recreation.\n const handlersRef = useRef<{\n onRowClick?: DataTableProps['onRowClick'];\n onSortChange?: DataTableProps['onSortChange'];\n onSearchChange?: DataTableProps['onSearchChange'];\n onPageChange?: DataTableProps['onPageChange'];\n }>({});\n handlersRef.current = { onRowClick, onSortChange, onSearchChange, onPageChange };\n\n // Stable callback wrappers that read from refs\n const stableOnRowClick = useCallback(\n (row: Record<string, unknown>) => handlersRef.current.onRowClick?.(row),\n [],\n );\n const stableOnStateChange = useCallback(\n (state: { sort?: SortState | null; search?: string; page?: number }) => {\n if (state.sort !== undefined) handlersRef.current.onSortChange?.(state.sort);\n if (state.search !== undefined) handlersRef.current.onSearchChange?.(state.search);\n if (state.page !== undefined) handlersRef.current.onPageChange?.(state.page);\n },\n [],\n );\n\n const prevSpecRef = useRef<string>('');\n\n // Determine if we're in controlled mode\n const isControlled = sort !== undefined || search !== undefined || page !== undefined;\n\n // Effect 1: Mount/unmount. Only recreate when structural options change.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec, sort, search, page intentionally excluded - handled via update()/setState() in Effects 2-3\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const mountOptions: TableMountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onRowClick: stableOnRowClick,\n responsive: true,\n onStateChange: stableOnStateChange,\n };\n\n if (isControlled) {\n mountOptions.externalState = {\n sort: sort ?? null,\n search: search ?? '',\n page: page ?? 0,\n };\n }\n\n tableRef.current = createTable(container, spec, mountOptions);\n prevSpecRef.current = JSON.stringify(spec);\n\n return () => {\n tableRef.current?.destroy();\n tableRef.current = null;\n };\n // Only recreate on structural option changes (theme, darkMode, onRowClick).\n // Controlled state updates are handled in Effect 2.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [theme, resolvedDarkMode, isControlled, stableOnRowClick, stableOnStateChange]);\n\n // Effect 2: Sync controlled state without remounting.\n useEffect(() => {\n const table = tableRef.current;\n if (!table || !isControlled) return;\n\n table.setState({\n sort: sort ?? null,\n search: search ?? '',\n page: page ?? 0,\n });\n }, [sort, search, page, isControlled]);\n\n // Effect 3: Sync spec changes via update().\n useEffect(() => {\n const table = tableRef.current;\n if (!table) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== prevSpecRef.current) {\n prevSpecRef.current = specString;\n table.update(spec);\n }\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-table-root ${className}` : 'viz-table-root'}\n style={style}\n />\n );\n}\n","/**\n * React Graph component: thin wrapper around the vanilla adapter.\n *\n * Mounts a graph instance on render, updates when spec changes,\n * and cleans up on unmount. All heavy lifting is done by the vanilla\n * createGraph() function.\n *\n * Supports forwardRef for imperative control via useGraph() hook.\n */\n\nimport type { DarkMode, GraphSpec, ThemeConfig } from '@opendata-ai/openchart-core';\nimport {\n createGraph,\n type GraphInstance,\n type GraphMountOptions,\n} from '@opendata-ai/openchart-vanilla';\nimport {\n type CSSProperties,\n forwardRef,\n useCallback,\n useEffect,\n useImperativeHandle,\n useRef,\n} from 'react';\nimport type { GraphHandle } from './hooks/useGraph';\nimport { useVizDarkMode, useVizTheme } from './ThemeContext';\n\nexport interface GraphProps {\n /** The graph spec to render. */\n spec: GraphSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** Callback when a node is clicked. */\n onNodeClick?: (node: Record<string, unknown>) => void;\n /** Callback when a node is double-clicked. */\n onNodeDoubleClick?: (node: Record<string, unknown>) => void;\n /** Callback when a node is hovered (null when hover ends). */\n onNodeHover?: (node: Record<string, unknown> | null) => void;\n /** Callback when an edge is hovered (null when hover ends). */\n onEdgeHover?: (edge: Record<string, unknown> | null) => void;\n /** Callback when selection changes. */\n onSelectionChange?: (nodeIds: string[]) => void;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * React component that renders a force-directed graph from a GraphSpec.\n *\n * Uses the vanilla adapter internally. The spec is compiled and rendered\n * on a canvas inside a wrapper div. Spec changes trigger re-renders via the\n * vanilla adapter's update() method.\n *\n * Supports ref for imperative control via useGraph() hook:\n * ```tsx\n * const { ref, search, zoomToFit } = useGraph();\n * return <Graph ref={ref} spec={spec} />;\n * ```\n */\nexport const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(\n {\n spec,\n theme: themeProp,\n darkMode,\n onNodeClick,\n onNodeDoubleClick,\n onNodeHover,\n onEdgeHover,\n onSelectionChange,\n className,\n style,\n },\n ref,\n) {\n const contextTheme = useVizTheme();\n const contextDarkMode = useVizDarkMode();\n const theme = themeProp ?? contextTheme;\n const resolvedDarkMode = darkMode ?? contextDarkMode;\n const containerRef = useRef<HTMLDivElement>(null);\n const graphRef = useRef<GraphInstance | null>(null);\n const specRef = useRef<string>('');\n\n // Store event handlers in refs so they don't trigger graph recreation.\n // Inline arrow functions create new references every render, which would\n // destroy and recreate the entire graph instance without this pattern.\n const handlersRef = useRef<{\n onNodeClick?: GraphProps['onNodeClick'];\n onNodeDoubleClick?: GraphProps['onNodeDoubleClick'];\n onNodeHover?: GraphProps['onNodeHover'];\n onEdgeHover?: GraphProps['onEdgeHover'];\n onSelectionChange?: GraphProps['onSelectionChange'];\n }>({});\n handlersRef.current = {\n onNodeClick,\n onNodeDoubleClick,\n onNodeHover,\n onEdgeHover,\n onSelectionChange,\n };\n\n // Stable callback wrappers that read from refs\n const stableOnNodeClick = useCallback(\n (node: Record<string, unknown>) => handlersRef.current.onNodeClick?.(node),\n [],\n );\n const stableOnNodeDoubleClick = useCallback(\n (node: Record<string, unknown>) => handlersRef.current.onNodeDoubleClick?.(node),\n [],\n );\n const stableOnNodeHover = useCallback(\n (node: Record<string, unknown> | null) => handlersRef.current.onNodeHover?.(node),\n [],\n );\n const stableOnEdgeHover = useCallback(\n (edge: Record<string, unknown> | null) => handlersRef.current.onEdgeHover?.(edge),\n [],\n );\n const stableOnSelectionChange = useCallback(\n (nodeIds: string[]) => handlersRef.current.onSelectionChange?.(nodeIds),\n [],\n );\n\n // Expose imperative handle for useGraph() hook\n useImperativeHandle(\n ref,\n () => ({\n search(query: string) {\n graphRef.current?.search(query);\n },\n clearSearch() {\n graphRef.current?.clearSearch();\n },\n zoomToFit() {\n graphRef.current?.zoomToFit();\n },\n zoomToNode(nodeId: string) {\n graphRef.current?.zoomToNode(nodeId);\n },\n selectNode(nodeId: string) {\n graphRef.current?.selectNode(nodeId);\n },\n getSelectedNodes() {\n return graphRef.current?.getSelectedNodes() ?? [];\n },\n updateVisuals(spec: GraphSpec) {\n graphRef.current?.updateVisuals(spec);\n },\n get instance() {\n return graphRef.current;\n },\n }),\n [],\n );\n\n // Mount graph and recreate when theme/darkMode change.\n // Event handlers use stable refs so they don't trigger recreation.\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in Effect 2\n useEffect(() => {\n const container = containerRef.current;\n if (!container) return;\n\n const options: GraphMountOptions = {\n theme,\n darkMode: resolvedDarkMode,\n onNodeClick: stableOnNodeClick,\n onNodeDoubleClick: stableOnNodeDoubleClick,\n onNodeHover: stableOnNodeHover,\n onEdgeHover: stableOnEdgeHover,\n onSelectionChange: stableOnSelectionChange,\n responsive: true,\n };\n\n graphRef.current = createGraph(container, spec, options);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n graphRef.current?.destroy();\n graphRef.current = null;\n };\n // Only recreate when theme or darkMode change. Event handlers use stable refs.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n theme,\n resolvedDarkMode,\n stableOnNodeClick,\n stableOnNodeDoubleClick,\n stableOnNodeHover,\n stableOnEdgeHover,\n stableOnSelectionChange,\n ]);\n\n // Update graph when spec changes.\n // If only encoding/chrome/nodeOverrides changed (same node/edge IDs), use\n // updateVisuals() to avoid restarting the simulation.\n useEffect(() => {\n const graph = graphRef.current;\n if (!graph) return;\n\n const specString = JSON.stringify(spec);\n if (specString === specRef.current) return;\n\n // Check if this is a visual-only change (same node/edge IDs)\n const prevSpec = specRef.current;\n specRef.current = specString;\n\n if (prevSpec) {\n try {\n const prev = JSON.parse(prevSpec) as GraphSpec;\n const sameNodes =\n prev.nodes.length === spec.nodes.length &&\n prev.nodes.every((n, i) => n.id === spec.nodes[i].id);\n const sameEdges =\n prev.edges.length === spec.edges.length &&\n prev.edges.every(\n (e, i) => e.source === spec.edges[i].source && e.target === spec.edges[i].target,\n );\n\n if (sameNodes && sameEdges) {\n graph.updateVisuals(spec);\n return;\n }\n } catch {\n // Parse failed, fall through to full update\n }\n }\n\n graph.update(spec);\n }, [spec]);\n\n return (\n <div\n ref={containerRef}\n className={className ? `viz-graph-root ${className}` : 'viz-graph-root'}\n style={style}\n />\n );\n});\n","/**\n * React hooks for chart lifecycle and dark mode resolution.\n *\n * useChart: manual control over a chart instance (for advanced usage).\n * useDarkMode: resolves the DarkMode preference to a boolean.\n */\n\nimport type { ChartLayout, ChartSpec, DarkMode, GraphSpec } from '@opendata-ai/openchart-core';\nimport { type ChartInstance, createChart, type MountOptions } from '@opendata-ai/openchart-vanilla';\nimport { useEffect, useRef, useState } from 'react';\n\n// ---------------------------------------------------------------------------\n// useChart\n// ---------------------------------------------------------------------------\n\nexport interface UseChartOptions {\n /** Theme overrides. */\n theme?: MountOptions['theme'];\n /** Dark mode setting. */\n darkMode?: MountOptions['darkMode'];\n /** Data point click handler. */\n onDataPointClick?: MountOptions['onDataPointClick'];\n /** Enable responsive resizing. Defaults to true. */\n responsive?: boolean;\n}\n\nexport interface UseChartReturn {\n /** Ref to attach to the container div. */\n ref: React.RefObject<HTMLDivElement | null>;\n /** The chart instance (null until mounted). */\n chart: ChartInstance | null;\n /** The current compiled layout (null until mounted). */\n layout: ChartLayout | null;\n}\n\n/**\n * Hook for manual chart lifecycle control.\n *\n * Attach the returned ref to a container div. The chart mounts\n * automatically and updates when the spec changes.\n *\n * @param spec - The visualization spec.\n * @param options - Mount options.\n * @returns { ref, chart, layout }\n */\nexport function useChart(spec: ChartSpec | GraphSpec, options?: UseChartOptions): UseChartReturn {\n const ref = useRef<HTMLDivElement | null>(null);\n const chartRef = useRef<ChartInstance | null>(null);\n const [layout, setLayout] = useState<ChartLayout | null>(null);\n const specRef = useRef<string>('');\n\n // Mount / unmount\n // biome-ignore lint/correctness/useExhaustiveDependencies: spec intentionally excluded - spec changes handled via update() in the update effect\n useEffect(() => {\n const container = ref.current;\n if (!container) return;\n\n const mountOpts: MountOptions = {\n theme: options?.theme,\n darkMode: options?.darkMode,\n onDataPointClick: options?.onDataPointClick,\n responsive: options?.responsive,\n };\n\n const chart = createChart(container, spec, mountOpts);\n chartRef.current = chart;\n setLayout(chart.layout);\n specRef.current = JSON.stringify(spec);\n\n return () => {\n chart.destroy();\n chartRef.current = null;\n setLayout(null);\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [options?.theme, options?.darkMode, options?.onDataPointClick, options?.responsive]);\n\n // Update on spec change\n useEffect(() => {\n const chart = chartRef.current;\n if (!chart) return;\n\n const specString = JSON.stringify(spec);\n if (specString !== specRef.current) {\n specRef.current = specString;\n chart.update(spec);\n setLayout(chart.layout);\n }\n }, [spec]);\n\n return {\n ref,\n chart: chartRef.current,\n layout,\n };\n}\n\n// ---------------------------------------------------------------------------\n// useDarkMode\n// ---------------------------------------------------------------------------\n\n/**\n * Resolve a DarkMode preference to a boolean.\n *\n * - \"force\" -> true\n * - \"off\" -> false\n * - \"auto\" -> matches system preference (reactive to changes)\n *\n * @param mode - The dark mode preference.\n * @returns Whether dark mode is active.\n */\nexport function useDarkMode(mode?: DarkMode): boolean {\n const [isDark, setIsDark] = useState(() => resolveInitial(mode));\n\n useEffect(() => {\n if (mode !== 'auto') {\n setIsDark(mode === 'force');\n return;\n }\n\n if (typeof window === 'undefined' || !window.matchMedia) {\n setIsDark(false);\n return;\n }\n\n const mq = window.matchMedia('(prefers-color-scheme: dark)');\n setIsDark(mq.matches);\n\n const handler = (e: MediaQueryListEvent) => setIsDark(e.matches);\n mq.addEventListener('change', handler);\n return () => mq.removeEventListener('change', handler);\n }, [mode]);\n\n return isDark;\n}\n\nfunction resolveInitial(mode?: DarkMode): boolean {\n if (mode === 'force') return true;\n if (mode === 'off' || mode === undefined) return false;\n // \"auto\"\n if (typeof window !== 'undefined' && window.matchMedia) {\n return window.matchMedia('(prefers-color-scheme: dark)').matches;\n }\n return false;\n}\n","/**\n * useGraph: hook for imperative graph control.\n *\n * Provides a ref to pass to <Graph /> and exposes graph methods\n * (search, zoom, select) for programmatic control of the graph instance.\n */\n\nimport type { GraphInstance } from '@opendata-ai/openchart-vanilla';\nimport { useCallback, useRef } from 'react';\n\nexport interface UseGraphReturn {\n /** Ref to pass to <Graph ref={ref} />. */\n ref: React.RefObject<GraphHandle | null>;\n /** Search for nodes matching a query string. */\n search: (query: string) => void;\n /** Clear the current search. */\n clearSearch: () => void;\n /** Zoom to fit all nodes in view. */\n zoomToFit: () => void;\n /** Zoom and center on a specific node. */\n zoomToNode: (nodeId: string) => void;\n /** Select a node by id. */\n selectNode: (nodeId: string) => void;\n /** Get the currently selected node ids. */\n getSelectedNodes: () => string[];\n}\n\n/** Handle exposed by Graph component via forwardRef. */\nexport interface GraphHandle {\n search: (query: string) => void;\n clearSearch: () => void;\n zoomToFit: () => void;\n zoomToNode: (nodeId: string) => void;\n selectNode: (nodeId: string) => void;\n getSelectedNodes: () => string[];\n /** Re-compile encoding/legend/chrome without restarting the simulation. */\n updateVisuals: (spec: import('@opendata-ai/openchart-core').GraphSpec) => void;\n /** The underlying GraphInstance from the vanilla adapter. */\n instance: GraphInstance | null;\n}\n\n/**\n * Hook for imperative graph control.\n *\n * Usage:\n * ```tsx\n * const { ref, search, zoomToFit } = useGraph();\n * return <Graph ref={ref} spec={spec} />;\n * ```\n */\nexport function useGraph(): UseGraphReturn {\n const ref = useRef<GraphHandle | null>(null);\n\n const search = useCallback((query: string) => {\n ref.current?.search(query);\n }, []);\n\n const clearSearch = useCallback(() => {\n ref.current?.clearSearch();\n }, []);\n\n const zoomToFit = useCallback(() => {\n ref.current?.zoomToFit();\n }, []);\n\n const zoomToNode = useCallback((nodeId: string) => {\n ref.current?.zoomToNode(nodeId);\n }, []);\n\n const selectNode = useCallback((nodeId: string) => {\n ref.current?.selectNode(nodeId);\n }, []);\n\n const getSelectedNodes = useCallback((): string[] => {\n return ref.current?.getSelectedNodes() ?? [];\n }, []);\n\n return {\n ref,\n search,\n clearSearch,\n zoomToFit,\n zoomToNode,\n selectNode,\n getSelectedNodes,\n };\n}\n","/**\n * useTable: hook for manual table lifecycle control.\n *\n * Attaches to a container ref, mounts a vanilla table instance,\n * and exposes the instance and current state.\n */\n\nimport type { TableSpec } from '@opendata-ai/openchart-core';\nimport {\n createTable,\n type TableInstance,\n type TableMountOptions,\n type TableState,\n} from '@opendata-ai/openchart-vanilla';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nexport interface UseTableReturn {\n /** Ref to attach to the container div. */\n ref: React.RefObject<HTMLDivElement | null>;\n /** The table instance (null until mounted). */\n table: TableInstance | null;\n /** The current table state (sort, search, page). */\n state: TableState;\n}\n\n/**\n * Hook for manual table lifecycle control.\n *\n * Attach the returned ref to a container div. The table mounts\n * automatically and updates when the spec changes.\n *\n * @param spec - The table spec.\n * @param options - Mount options.\n * @returns { ref, table, state }\n */\nexport function useTable(spec: TableSpec, options?: TableMountOptions): UseTableReturn {\n const ref = useRef<HTMLDivElement | null>(null);\n const tableRef = useRef<TableInstance | null>(null);\n const [state, setState] = useState<TableState>({\n sort: null,\n search: '',\n page: 0,\n });\n\n const originalOnStateChange = options?.onStateChange;\n\n const handleStateChange = useCallback(\n (newState: TableState) => {\n setState(newState);\n originalOnStateChange?.(newState);\n },\n [originalOnStateChange],\n );\n\n // Mount / unmount\n useEffect(() => {\n const container = ref.current;\n if (!container) return;\n\n const mountOpts: TableMountOptions = {\n ...options,\n onStateChange: handleStateChange,\n };\n\n const table = createTable(container, spec, mountOpts);\n tableRef.current = table;\n setState(table.getState());\n\n return () => {\n table.destroy();\n tableRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n options?.theme,\n options?.darkMode,\n options?.onRowClick,\n options?.responsive,\n handleStateChange,\n options,\n spec,\n ]);\n\n // Update on spec change\n useEffect(() => {\n const table = tableRef.current;\n if (!table) return;\n\n table.update(spec);\n setState(table.getState());\n }, [spec]);\n\n return {\n ref,\n table: tableRef.current,\n state,\n };\n}\n","/**\n * useTableState: managed state hook for controlled table usage.\n *\n * Provides individual sort/search/page state with setters and a\n * resetState function to return to initial values.\n */\n\nimport type { SortState } from '@opendata-ai/openchart-core';\nimport { useCallback, useState } from 'react';\n\nexport interface UseTableStateReturn {\n sort: SortState | null;\n setSort: (sort: SortState | null) => void;\n search: string;\n setSearch: (query: string) => void;\n page: number;\n setPage: (page: number) => void;\n resetState: () => void;\n}\n\nexport interface UseTableStateOptions {\n sort?: SortState | null;\n search?: string;\n page?: number;\n}\n\n/**\n * Hook for managing table state (sort, search, page).\n *\n * Use with the DataTable component's controlled props:\n * ```tsx\n * const { sort, search, page, setSort, setSearch, setPage } = useTableState();\n * <DataTable\n * spec={spec}\n * sort={sort}\n * search={search}\n * page={page}\n * onSortChange={setSort}\n * onSearchChange={setSearch}\n * onPageChange={setPage}\n * />\n * ```\n */\nexport function useTableState(initialState?: UseTableStateOptions): UseTableStateReturn {\n const [sort, setSortInternal] = useState<SortState | null>(initialState?.sort ?? null);\n const [search, setSearchInternal] = useState(initialState?.search ?? '');\n const [page, setPageInternal] = useState(initialState?.page ?? 0);\n\n const setSort = useCallback((newSort: SortState | null) => {\n setSortInternal(newSort);\n }, []);\n\n const setSearch = useCallback((query: string) => {\n setSearchInternal(query);\n }, []);\n\n const setPage = useCallback((newPage: number) => {\n setPageInternal(newPage);\n }, []);\n\n const resetState = useCallback(() => {\n setSortInternal(initialState?.sort ?? null);\n setSearchInternal(initialState?.search ?? '');\n setPageInternal(initialState?.page ?? 0);\n }, [initialState?.sort, initialState?.search, initialState?.page]);\n\n return {\n sort,\n setSort,\n search,\n setSearch,\n page,\n setPage,\n resetState,\n };\n}\n","/**\n * Visualization routing component: renders Chart, DataTable, or Graph\n * based on the spec type. Use this when rendering arbitrary VizSpec values.\n *\n * For event handlers, use the specific component (Chart, DataTable, Graph) directly.\n */\n\nimport type { DarkMode, ThemeConfig, VizSpec } from '@opendata-ai/openchart-core';\nimport { isGraphSpec, isTableSpec } from '@opendata-ai/openchart-core';\nimport type { CSSProperties } from 'react';\nimport { Chart } from './Chart';\nimport { DataTable } from './DataTable';\nimport { Graph } from './Graph';\n\nexport interface VisualizationProps {\n /** The visualization spec to render. */\n spec: VizSpec;\n /** Theme overrides. */\n theme?: ThemeConfig;\n /** Dark mode: \"auto\", \"force\", or \"off\". */\n darkMode?: DarkMode;\n /** CSS class name for the wrapper div. */\n className?: string;\n /** Inline styles for the wrapper div. */\n style?: CSSProperties;\n}\n\n/**\n * Routes a VizSpec to the appropriate rendering component.\n *\n * Accepts any VizSpec and renders it with the correct component based on the\n * spec's type field. For event handlers, use Chart, DataTable, or Graph directly.\n */\nexport function Visualization({ spec, theme, darkMode, className, style }: VisualizationProps) {\n if (isTableSpec(spec)) {\n return (\n <DataTable\n spec={spec}\n theme={theme}\n darkMode={darkMode}\n className={className}\n style={style}\n />\n );\n }\n if (isGraphSpec(spec)) {\n return (\n <Graph spec={spec} theme={theme} darkMode={darkMode} className={className} style={style} />\n );\n }\n return (\n <Chart spec={spec} theme={theme} darkMode={darkMode} className={className} style={style} />\n );\n}\n"],"mappings":"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","names":["jsx","useCallback","useEffect","useRef","jsx","useRef","useCallback","useEffect","useCallback","useEffect","useRef","jsx","Graph","useRef","useCallback","spec","useEffect","createChart","useEffect","useRef","useCallback","useRef","createTable","useCallback","useEffect","useRef","useState","useCallback","useState","jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-react",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "React components for openchart: <Chart />, <DataTable />, <VizThemeProvider />",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -46,9 +46,9 @@
46
46
  "typecheck": "tsc --noEmit"
47
47
  },
48
48
  "dependencies": {
49
- "@opendata-ai/openchart-core": "2.0.0",
50
- "@opendata-ai/openchart-engine": "2.0.0",
51
- "@opendata-ai/openchart-vanilla": "2.0.0"
49
+ "@opendata-ai/openchart-core": "2.2.0",
50
+ "@opendata-ai/openchart-engine": "2.2.0",
51
+ "@opendata-ai/openchart-vanilla": "2.2.0"
52
52
  },
53
53
  "peerDependencies": {
54
54
  "react": ">=18.0.0",
package/src/Graph.tsx CHANGED
@@ -36,6 +36,10 @@ export interface GraphProps {
36
36
  onNodeClick?: (node: Record<string, unknown>) => void;
37
37
  /** Callback when a node is double-clicked. */
38
38
  onNodeDoubleClick?: (node: Record<string, unknown>) => void;
39
+ /** Callback when a node is hovered (null when hover ends). */
40
+ onNodeHover?: (node: Record<string, unknown> | null) => void;
41
+ /** Callback when an edge is hovered (null when hover ends). */
42
+ onEdgeHover?: (edge: Record<string, unknown> | null) => void;
39
43
  /** Callback when selection changes. */
40
44
  onSelectionChange?: (nodeIds: string[]) => void;
41
45
  /** CSS class name for the wrapper div. */
@@ -64,6 +68,8 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
64
68
  darkMode,
65
69
  onNodeClick,
66
70
  onNodeDoubleClick,
71
+ onNodeHover,
72
+ onEdgeHover,
67
73
  onSelectionChange,
68
74
  className,
69
75
  style,
@@ -84,9 +90,17 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
84
90
  const handlersRef = useRef<{
85
91
  onNodeClick?: GraphProps['onNodeClick'];
86
92
  onNodeDoubleClick?: GraphProps['onNodeDoubleClick'];
93
+ onNodeHover?: GraphProps['onNodeHover'];
94
+ onEdgeHover?: GraphProps['onEdgeHover'];
87
95
  onSelectionChange?: GraphProps['onSelectionChange'];
88
96
  }>({});
89
- handlersRef.current = { onNodeClick, onNodeDoubleClick, onSelectionChange };
97
+ handlersRef.current = {
98
+ onNodeClick,
99
+ onNodeDoubleClick,
100
+ onNodeHover,
101
+ onEdgeHover,
102
+ onSelectionChange,
103
+ };
90
104
 
91
105
  // Stable callback wrappers that read from refs
92
106
  const stableOnNodeClick = useCallback(
@@ -97,6 +111,14 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
97
111
  (node: Record<string, unknown>) => handlersRef.current.onNodeDoubleClick?.(node),
98
112
  [],
99
113
  );
114
+ const stableOnNodeHover = useCallback(
115
+ (node: Record<string, unknown> | null) => handlersRef.current.onNodeHover?.(node),
116
+ [],
117
+ );
118
+ const stableOnEdgeHover = useCallback(
119
+ (edge: Record<string, unknown> | null) => handlersRef.current.onEdgeHover?.(edge),
120
+ [],
121
+ );
100
122
  const stableOnSelectionChange = useCallback(
101
123
  (nodeIds: string[]) => handlersRef.current.onSelectionChange?.(nodeIds),
102
124
  [],
@@ -124,6 +146,9 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
124
146
  getSelectedNodes() {
125
147
  return graphRef.current?.getSelectedNodes() ?? [];
126
148
  },
149
+ updateVisuals(spec: GraphSpec) {
150
+ graphRef.current?.updateVisuals(spec);
151
+ },
127
152
  get instance() {
128
153
  return graphRef.current;
129
154
  },
@@ -143,6 +168,8 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
143
168
  darkMode: resolvedDarkMode,
144
169
  onNodeClick: stableOnNodeClick,
145
170
  onNodeDoubleClick: stableOnNodeDoubleClick,
171
+ onNodeHover: stableOnNodeHover,
172
+ onEdgeHover: stableOnEdgeHover,
146
173
  onSelectionChange: stableOnSelectionChange,
147
174
  responsive: true,
148
175
  };
@@ -161,19 +188,47 @@ export const Graph = forwardRef<GraphHandle, GraphProps>(function Graph(
161
188
  resolvedDarkMode,
162
189
  stableOnNodeClick,
163
190
  stableOnNodeDoubleClick,
191
+ stableOnNodeHover,
192
+ stableOnEdgeHover,
164
193
  stableOnSelectionChange,
165
194
  ]);
166
195
 
167
- // Update graph when spec changes
196
+ // Update graph when spec changes.
197
+ // If only encoding/chrome/nodeOverrides changed (same node/edge IDs), use
198
+ // updateVisuals() to avoid restarting the simulation.
168
199
  useEffect(() => {
169
200
  const graph = graphRef.current;
170
201
  if (!graph) return;
171
202
 
172
203
  const specString = JSON.stringify(spec);
173
- if (specString !== specRef.current) {
174
- specRef.current = specString;
175
- graph.update(spec);
204
+ if (specString === specRef.current) return;
205
+
206
+ // Check if this is a visual-only change (same node/edge IDs)
207
+ const prevSpec = specRef.current;
208
+ specRef.current = specString;
209
+
210
+ if (prevSpec) {
211
+ try {
212
+ const prev = JSON.parse(prevSpec) as GraphSpec;
213
+ const sameNodes =
214
+ prev.nodes.length === spec.nodes.length &&
215
+ prev.nodes.every((n, i) => n.id === spec.nodes[i].id);
216
+ const sameEdges =
217
+ prev.edges.length === spec.edges.length &&
218
+ prev.edges.every(
219
+ (e, i) => e.source === spec.edges[i].source && e.target === spec.edges[i].target,
220
+ );
221
+
222
+ if (sameNodes && sameEdges) {
223
+ graph.updateVisuals(spec);
224
+ return;
225
+ }
226
+ } catch {
227
+ // Parse failed, fall through to full update
228
+ }
176
229
  }
230
+
231
+ graph.update(spec);
177
232
  }, [spec]);
178
233
 
179
234
  return (
@@ -33,6 +33,8 @@ export interface GraphHandle {
33
33
  zoomToNode: (nodeId: string) => void;
34
34
  selectNode: (nodeId: string) => void;
35
35
  getSelectedNodes: () => string[];
36
+ /** Re-compile encoding/legend/chrome without restarting the simulation. */
37
+ updateVisuals: (spec: import('@opendata-ai/openchart-core').GraphSpec) => void;
36
38
  /** The underlying GraphInstance from the vanilla adapter. */
37
39
  instance: GraphInstance | null;
38
40
  }