@opendata-ai/openchart-vanilla 2.1.0 → 2.2.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.
package/dist/index.d.ts CHANGED
@@ -27,6 +27,22 @@ interface PNGExportOptions {
27
27
  * @returns A Promise resolving to the PNG Blob.
28
28
  */
29
29
  declare function exportPNG(svgElement: SVGElement, options?: PNGExportOptions): Promise<Blob>;
30
+ interface JPGExportOptions extends PNGExportOptions {
31
+ /** JPEG quality from 0 to 1. Defaults to 0.92. */
32
+ quality?: number;
33
+ }
34
+ /**
35
+ * Render an SVG element to a JPEG Blob via a canvas.
36
+ *
37
+ * Same pipeline as exportPNG but outputs JPEG with configurable quality.
38
+ * The canvas is filled with white before drawing to avoid transparent
39
+ * backgrounds rendering as black in JPEG format.
40
+ *
41
+ * @param svgElement - The rendered SVG element.
42
+ * @param options - Optional DPI scaling and JPEG quality.
43
+ * @returns A Promise resolving to the JPEG Blob.
44
+ */
45
+ declare function exportJPG(svgElement: SVGElement, options?: JPGExportOptions): Promise<Blob>;
30
46
  /**
31
47
  * Convert an array of data objects to a CSV string.
32
48
  *
@@ -72,12 +88,20 @@ interface GraphMountOptions {
72
88
  theme?: ThemeConfig;
73
89
  darkMode?: DarkMode;
74
90
  responsive?: boolean;
91
+ /** Show the built-in tooltip on node/edge hover. Defaults to true. */
92
+ tooltip?: boolean;
93
+ /** Show the built-in legend. Defaults to true. */
94
+ legend?: boolean;
75
95
  onNodeClick?: (node: Record<string, unknown>) => void;
76
96
  onNodeDoubleClick?: (node: Record<string, unknown>) => void;
97
+ onNodeHover?: (node: Record<string, unknown> | null) => void;
98
+ onEdgeHover?: (edge: Record<string, unknown> | null) => void;
77
99
  onSelectionChange?: (nodeIds: string[]) => void;
78
100
  }
79
101
  interface GraphInstance {
80
102
  update(spec: GraphSpec): void;
103
+ /** Re-compile encoding/legend/chrome without restarting the simulation. Preserves node positions. */
104
+ updateVisuals(spec: GraphSpec): void;
81
105
  search(query: string): void;
82
106
  clearSearch(): void;
83
107
  zoomToFit(): void;
@@ -116,7 +140,7 @@ interface MountOptions extends ChartEventHandlers {
116
140
  /** Enable responsive resizing. Defaults to true. */
117
141
  responsive?: boolean;
118
142
  }
119
- interface ExportOptions extends PNGExportOptions {
143
+ interface ExportOptions extends JPGExportOptions {
120
144
  }
121
145
  interface ChartInstance {
122
146
  /** Re-compile and re-render with a new spec. */
@@ -126,8 +150,9 @@ interface ChartInstance {
126
150
  /** Export the chart. */
127
151
  export(format: 'svg'): string;
128
152
  export(format: 'png', options?: ExportOptions): Promise<Blob>;
153
+ export(format: 'jpg', options?: ExportOptions): Promise<Blob>;
129
154
  export(format: 'csv'): string;
130
- export(format: 'svg' | 'png' | 'csv', options?: ExportOptions): string | Promise<Blob>;
155
+ export(format: 'svg' | 'png' | 'jpg' | 'csv', options?: ExportOptions): string | Promise<Blob>;
131
156
  /** Remove all DOM elements and disconnect observers. */
132
157
  destroy(): void;
133
158
  /** The current compiled layout (for hooks / debugging). */
@@ -324,4 +349,4 @@ interface TooltipManager {
324
349
  */
325
350
  declare function createTooltipManager(container: HTMLElement): TooltipManager;
326
351
 
327
- export { type ChartInstance, type ExportOptions, type GraphInstance, type GraphMountOptions, type KeyboardNavOptions, type MountOptions, type PNGExportOptions, type TableInstance, type TableMountOptions, type TableState, type TooltipManager, attachKeyboardNav, createChart, createGraph, createSimulationWorker, createTable, createTooltipManager, exportCSV, exportPNG, exportSVG, observeResize, registerMarkRenderer, renderBarCell, renderCategoryCell, renderCell, renderChartSVG, renderFlagCell, renderHeatmapCell, renderImageCell, renderSparklineCell, renderTable, renderTextCell };
352
+ export { type ChartInstance, type ExportOptions, type GraphInstance, type GraphMountOptions, type JPGExportOptions, type KeyboardNavOptions, type MountOptions, type PNGExportOptions, type TableInstance, type TableMountOptions, type TableState, type TooltipManager, attachKeyboardNav, createChart, createGraph, createSimulationWorker, createTable, createTooltipManager, exportCSV, exportJPG, exportPNG, exportSVG, observeResize, registerMarkRenderer, renderBarCell, renderCategoryCell, renderCell, renderChartSVG, renderFlagCell, renderHeatmapCell, renderImageCell, renderSparklineCell, renderTable, renderTextCell };
package/dist/index.js CHANGED
@@ -38,6 +38,48 @@ async function exportPNG(svgElement, options) {
38
38
  img.src = url;
39
39
  });
40
40
  }
41
+ async function exportJPG(svgElement, options) {
42
+ const dpi = options?.dpi ?? 2;
43
+ const quality = options?.quality ?? 0.92;
44
+ const svgString = exportSVG(svgElement);
45
+ const width = parseFloat(svgElement.getAttribute("width") || "600");
46
+ const height = parseFloat(svgElement.getAttribute("height") || "400");
47
+ const canvas = document.createElement("canvas");
48
+ canvas.width = width * dpi;
49
+ canvas.height = height * dpi;
50
+ const ctx = canvas.getContext("2d");
51
+ if (!ctx) {
52
+ throw new Error("Canvas 2D context not available");
53
+ }
54
+ ctx.fillStyle = "#ffffff";
55
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
56
+ ctx.scale(dpi, dpi);
57
+ const img = new Image();
58
+ const blob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
59
+ const url = URL.createObjectURL(blob);
60
+ return new Promise((resolve, reject) => {
61
+ img.onload = () => {
62
+ ctx.drawImage(img, 0, 0);
63
+ URL.revokeObjectURL(url);
64
+ canvas.toBlob(
65
+ (result) => {
66
+ if (result) {
67
+ resolve(result);
68
+ } else {
69
+ reject(new Error("Canvas toBlob returned null"));
70
+ }
71
+ },
72
+ "image/jpeg",
73
+ quality
74
+ );
75
+ };
76
+ img.onerror = () => {
77
+ URL.revokeObjectURL(url);
78
+ reject(new Error("Failed to load SVG as image"));
79
+ };
80
+ img.src = url;
81
+ });
82
+ }
41
83
  function exportCSV(data) {
42
84
  if (data.length === 0) return "";
43
85
  const headers = Object.keys(data[0]);
@@ -65,15 +107,15 @@ function createSimulationWorker() {
65
107
  import { compileGraph } from "@opendata-ai/openchart-engine";
66
108
 
67
109
  // src/graph/canvas-renderer.ts
68
- var LABEL_FONT_MIN = 10;
69
- var LABEL_FONT_MAX = 14;
110
+ var LABEL_FONT_MIN = 8;
111
+ var LABEL_FONT_MAX = 12;
70
112
  var EDGE_ALPHA_DEFAULT = 0.35;
71
113
  var EDGE_ALPHA_CONNECTED = 1;
72
114
  var EDGE_ALPHA_DIMMED = 0.05;
73
115
  var SEARCH_NON_MATCH_ALPHA = 0.15;
74
116
  var GLOW_NODE_THRESHOLD = 2e3;
75
- var GLOW_RADIUS_MULTIPLIER = 1.5;
76
- var GLOW_ALPHA = 0.2;
117
+ var GLOW_RADIUS_MULTIPLIER = 1.3;
118
+ var GLOW_ALPHA = 0.15;
77
119
  var CULL_MARGIN = 50;
78
120
  var TWO_PI = Math.PI * 2;
79
121
  var MIN_SCREEN_RADIUS = 2.5;
@@ -132,6 +174,7 @@ var GraphCanvasRenderer = class {
132
174
  edges,
133
175
  transform,
134
176
  hoveredNodeId,
177
+ hoveredEdgeId,
135
178
  selectedNodeIds,
136
179
  adjacencyMap,
137
180
  theme,
@@ -160,8 +203,10 @@ var GraphCanvasRenderer = class {
160
203
  ctx.save();
161
204
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
162
205
  ctx.clearRect(0, 0, cssWidth, cssHeight);
163
- ctx.fillStyle = theme.colors.background;
164
- ctx.fillRect(0, 0, cssWidth, cssHeight);
206
+ if (theme.colors.background !== "transparent") {
207
+ ctx.fillStyle = theme.colors.background;
208
+ ctx.fillRect(0, 0, cssWidth, cssHeight);
209
+ }
165
210
  ctx.translate(transform.x, transform.y);
166
211
  ctx.scale(transform.k, transform.k);
167
212
  this.drawEdgesBatched(
@@ -169,7 +214,8 @@ var GraphCanvasRenderer = class {
169
214
  visibleEdges,
170
215
  hasActiveNode,
171
216
  connectedNodeIds,
172
- isGesturing ? null : searchMatches
217
+ isGesturing ? null : searchMatches,
218
+ hoveredEdgeId
173
219
  );
174
220
  this.drawNodesBatched(
175
221
  ctx,
@@ -218,11 +264,17 @@ var GraphCanvasRenderer = class {
218
264
  // -------------------------------------------------------------------------
219
265
  // Batched edge drawing
220
266
  // -------------------------------------------------------------------------
221
- drawEdgesBatched(ctx, edges, hasActiveNode, connectedNodeIds, searchMatches) {
267
+ drawEdgesBatched(ctx, edges, hasActiveNode, connectedNodeIds, searchMatches, hoveredEdgeId) {
222
268
  const dimmedEdges = [];
223
269
  const defaultEdges = [];
224
270
  const connectedEdges = [];
271
+ let hoveredEdge = null;
225
272
  for (const edge of edges) {
273
+ const edgeId = `${edge.source}->${edge.target}`;
274
+ if (edgeId === hoveredEdgeId) {
275
+ hoveredEdge = edge;
276
+ continue;
277
+ }
226
278
  const isConnected = hasActiveNode && connectedNodeIds.has(edge.source) && connectedNodeIds.has(edge.target);
227
279
  const isDimmed = hasActiveNode && !isConnected;
228
280
  if (isConnected) {
@@ -236,6 +288,19 @@ var GraphCanvasRenderer = class {
236
288
  this.drawEdgeGroupBatched(ctx, dimmedEdges, EDGE_ALPHA_DIMMED, searchMatches);
237
289
  this.drawEdgeGroupBatched(ctx, defaultEdges, EDGE_ALPHA_DEFAULT, searchMatches);
238
290
  this.drawEdgeGroupBatched(ctx, connectedEdges, EDGE_ALPHA_CONNECTED, searchMatches);
291
+ if (hoveredEdge) {
292
+ const dash = DASH_PATTERNS[hoveredEdge.style] ?? DASH_PATTERNS.solid;
293
+ ctx.setLineDash(dash);
294
+ ctx.strokeStyle = hoveredEdge.stroke;
295
+ ctx.lineWidth = hoveredEdge.strokeWidth * 2;
296
+ ctx.globalAlpha = EDGE_ALPHA_CONNECTED;
297
+ ctx.beginPath();
298
+ ctx.moveTo(hoveredEdge.sourceX, hoveredEdge.sourceY);
299
+ ctx.lineTo(hoveredEdge.targetX, hoveredEdge.targetY);
300
+ ctx.stroke();
301
+ ctx.setLineDash([]);
302
+ ctx.globalAlpha = 1;
303
+ }
239
304
  }
240
305
  /**
241
306
  * Draw a group of edges at a given alpha, batched by (stroke, strokeWidth, style).
@@ -479,7 +544,7 @@ var GraphCanvasRenderer = class {
479
544
  // Labels (drawn individually, skipped during gestures)
480
545
  // -------------------------------------------------------------------------
481
546
  drawLabels(ctx, nodes, threshold, hoveredNodeId, selectedNodeIds, searchMatches, zoom, theme) {
482
- const rawSize = 12 / zoom;
547
+ const rawSize = 10 / zoom;
483
548
  const fontSize = Math.max(LABEL_FONT_MIN, Math.min(LABEL_FONT_MAX, rawSize));
484
549
  ctx.font = `${fontSize}px ${theme.fonts.family}`;
485
550
  ctx.textAlign = "center";
@@ -493,7 +558,7 @@ var GraphCanvasRenderer = class {
493
558
  if (!forced && node.labelPriority < threshold) continue;
494
559
  ctx.globalAlpha = dimmed ? SEARCH_NON_MATCH_ALPHA : 1;
495
560
  const labelY = node.y + node.radius + 3;
496
- ctx.strokeStyle = theme.colors.background;
561
+ ctx.strokeStyle = theme.colors.background === "transparent" ? "rgba(0, 0, 0, 0.7)" : theme.colors.background;
497
562
  ctx.lineWidth = 3;
498
563
  ctx.lineJoin = "round";
499
564
  ctx.strokeText(node.label, node.x, labelY);
@@ -718,6 +783,10 @@ var GraphInteractionManager = class {
718
783
  }
719
784
  const hitId = this.hitTest(x, y);
720
785
  this.callbacks.onHoverChange(hitId);
786
+ if (!hitId) {
787
+ const graph = this.transform.screenToGraph(x, y);
788
+ this.callbacks.onBackgroundHover?.(graph.x, graph.y, x, y);
789
+ }
721
790
  this.canvas.style.cursor = hitId ? "pointer" : "default";
722
791
  }
723
792
  onMouseUp(e) {
@@ -1216,13 +1285,18 @@ var SimulationManager = class _SimulationManager {
1216
1285
  community: n.community
1217
1286
  }));
1218
1287
  this.syncNodeMap = new Map(this.syncNodes.map((n) => [n.id, n]));
1219
- this.syncSim = forceSimulation(this.syncNodes).force(
1220
- "link",
1221
- forceLink(edges.map((e) => ({ ...e }))).id((d) => d.id).distance(config.linkDistance)
1222
- ).force("charge", forceManyBody().strength(config.chargeStrength)).force("center", forceCenter(0, 0)).force(
1288
+ const linkForce = forceLink(edges.map((e) => ({ ...e }))).id((d) => d.id).distance(config.linkDistance);
1289
+ if (config.linkStrength != null) {
1290
+ linkForce.strength(config.linkStrength);
1291
+ }
1292
+ const padding = config.collisionPadding ?? 2;
1293
+ this.syncSim = forceSimulation(this.syncNodes).force("link", linkForce).force("charge", forceManyBody().strength(config.chargeStrength)).force(
1223
1294
  "collide",
1224
- forceCollide().radius((d) => d.radius + 1)
1295
+ forceCollide().radius((d) => d.radius + padding)
1225
1296
  ).force("gravityX", forceX(0).strength(0.05)).force("gravityY", forceY(0).strength(0.05)).alphaDecay(config.alphaDecay).velocityDecay(config.velocityDecay).stop();
1297
+ if (config.centerForce !== false) {
1298
+ this.syncSim.force("center", forceCenter(0, 0));
1299
+ }
1226
1300
  if (config.clustering) {
1227
1301
  const clusterFn = forceCluster(this.syncNodes, config.clustering.strength);
1228
1302
  this.syncSim.force("cluster", clusterFn);
@@ -1467,6 +1541,7 @@ function createGraph(container, spec, options) {
1467
1541
  let positionedEdges = [];
1468
1542
  let adjacencyMap = /* @__PURE__ */ new Map();
1469
1543
  let hoveredNodeId = null;
1544
+ let hoveredEdgeId = null;
1470
1545
  let selectedNodeIds = /* @__PURE__ */ new Set();
1471
1546
  let animFrameId = null;
1472
1547
  let needsRender = false;
@@ -1527,6 +1602,38 @@ function createGraph(container, spec, options) {
1527
1602
  const node = compilation.nodes.find((n) => n.id === nodeId);
1528
1603
  return node?.data ?? {};
1529
1604
  }
1605
+ function pointToSegmentDist(px, py, ax, ay, bx, by) {
1606
+ const dx = bx - ax;
1607
+ const dy = by - ay;
1608
+ const lenSq = dx * dx + dy * dy;
1609
+ if (lenSq === 0) return Math.hypot(px - ax, py - ay);
1610
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
1611
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
1612
+ }
1613
+ function hitTestEdge(graphX, graphY, threshold) {
1614
+ let bestDist = threshold;
1615
+ let bestEdgeId = null;
1616
+ for (const edge of positionedEdges) {
1617
+ const dist = pointToSegmentDist(
1618
+ graphX,
1619
+ graphY,
1620
+ edge.sourceX,
1621
+ edge.sourceY,
1622
+ edge.targetX,
1623
+ edge.targetY
1624
+ );
1625
+ if (dist < bestDist) {
1626
+ bestDist = dist;
1627
+ bestEdgeId = `${edge.source}->${edge.target}`;
1628
+ }
1629
+ }
1630
+ return bestEdgeId;
1631
+ }
1632
+ function edgeDataById(edgeId) {
1633
+ const [source, target] = edgeId.split("->");
1634
+ const edge = compilation.edges.find((e) => e.source === source && e.target === target);
1635
+ return edge?.data ?? null;
1636
+ }
1530
1637
  function createDOM() {
1531
1638
  const { width, height } = getContainerDimensions();
1532
1639
  const isDark = resolveDarkMode(options?.darkMode);
@@ -1548,10 +1655,12 @@ function createGraph(container, spec, options) {
1548
1655
  canvas.setAttribute("aria-label", compilation.a11y.altText);
1549
1656
  }
1550
1657
  wrapper.appendChild(canvas);
1551
- legendEl = document.createElement("div");
1552
- legendEl.className = "viz-graph-legend";
1553
- renderLegend2();
1554
- wrapper.appendChild(legendEl);
1658
+ if (options?.legend !== false) {
1659
+ legendEl = document.createElement("div");
1660
+ legendEl.className = "viz-graph-legend";
1661
+ renderLegend2();
1662
+ wrapper.appendChild(legendEl);
1663
+ }
1555
1664
  container.appendChild(wrapper);
1556
1665
  const chromeHeight = chromeEl.getBoundingClientRect().height || 0;
1557
1666
  const canvasHeight = Math.max(height - chromeHeight, 200);
@@ -1601,7 +1710,10 @@ function createGraph(container, spec, options) {
1601
1710
  clustering: config.clustering,
1602
1711
  alphaDecay: config.alphaDecay,
1603
1712
  velocityDecay: config.velocityDecay,
1604
- collisionRadius: config.collisionRadius
1713
+ collisionRadius: config.collisionRadius,
1714
+ collisionPadding: config.collisionPadding,
1715
+ linkStrength: config.linkStrength,
1716
+ centerForce: config.centerForce
1605
1717
  });
1606
1718
  simulation.onTick((positions, _alpha) => {
1607
1719
  if (destroyed) return;
@@ -1661,6 +1773,7 @@ function createGraph(container, spec, options) {
1661
1773
  edges: positionedEdges,
1662
1774
  transform: { x: transform.x, y: transform.y, k: transform.k },
1663
1775
  hoveredNodeId,
1776
+ hoveredEdgeId,
1664
1777
  selectedNodeIds,
1665
1778
  adjacencyMap,
1666
1779
  theme: compilation.theme,
@@ -1672,7 +1785,9 @@ function createGraph(container, spec, options) {
1672
1785
  }
1673
1786
  function initInteraction() {
1674
1787
  if (!canvas) return;
1675
- tooltipManager = createTooltipManager(wrapper);
1788
+ if (options?.tooltip !== false) {
1789
+ tooltipManager = createTooltipManager(wrapper);
1790
+ }
1676
1791
  interactionManager = new GraphInteractionManager(canvas, spatialIndex, {
1677
1792
  onTransformChange(_transform) {
1678
1793
  markGesture();
@@ -1683,7 +1798,16 @@ function createGraph(container, spec, options) {
1683
1798
  hoveredNodeId = nodeId;
1684
1799
  needsRender = true;
1685
1800
  scheduleRender();
1801
+ if (nodeId) {
1802
+ options?.onNodeHover?.(nodeDataById(nodeId));
1803
+ } else {
1804
+ options?.onNodeHover?.(null);
1805
+ }
1686
1806
  if (nodeId && tooltipManager) {
1807
+ if (hoveredEdgeId) {
1808
+ hoveredEdgeId = null;
1809
+ options?.onEdgeHover?.(null);
1810
+ }
1687
1811
  const content = compilation.tooltipDescriptors.get(nodeId);
1688
1812
  if (content) {
1689
1813
  const node = positionedNodes.find((n) => n.id === nodeId);
@@ -1692,10 +1816,35 @@ function createGraph(container, spec, options) {
1692
1816
  tooltipManager.show(content, screen.x, screen.y);
1693
1817
  }
1694
1818
  }
1695
- } else {
1819
+ } else if (!nodeId) {
1696
1820
  tooltipManager?.hide();
1697
1821
  }
1698
1822
  },
1823
+ onBackgroundHover(graphX, graphY, screenX, screenY) {
1824
+ const transform = interactionManager?.getTransform();
1825
+ const threshold = 5 / (transform?.k ?? 1);
1826
+ const edgeId = hitTestEdge(graphX, graphY, threshold);
1827
+ if (edgeId !== hoveredEdgeId) {
1828
+ hoveredEdgeId = edgeId;
1829
+ needsRender = true;
1830
+ scheduleRender();
1831
+ if (edgeId) {
1832
+ const data = edgeDataById(edgeId);
1833
+ options?.onEdgeHover?.(data);
1834
+ if (tooltipManager && data) {
1835
+ const fields = Object.entries(data).filter(([key]) => key !== "source" && key !== "target").filter(([, value]) => value != null).map(([key, value]) => ({
1836
+ label: key,
1837
+ value: typeof value === "number" ? value.toLocaleString() : String(value)
1838
+ }));
1839
+ const [source, target] = edgeId.split("->");
1840
+ tooltipManager.show({ title: `${source} \u2192 ${target}`, fields }, screenX, screenY);
1841
+ }
1842
+ } else {
1843
+ options?.onEdgeHover?.(null);
1844
+ tooltipManager?.hide();
1845
+ }
1846
+ }
1847
+ },
1699
1848
  onSelectionChange(nodeIds) {
1700
1849
  selectedNodeIds = new Set(nodeIds);
1701
1850
  needsRender = true;
@@ -1821,9 +1970,40 @@ function createGraph(container, spec, options) {
1821
1970
  initSimulation();
1822
1971
  initInteraction();
1823
1972
  hoveredNodeId = null;
1973
+ hoveredEdgeId = null;
1824
1974
  selectedNodeIds = /* @__PURE__ */ new Set();
1825
1975
  searchManager.clearSearch();
1826
1976
  }
1977
+ function updateVisuals(newSpec) {
1978
+ if (destroyed) return;
1979
+ currentSpec = newSpec;
1980
+ const posMap = /* @__PURE__ */ new Map();
1981
+ for (const node of positionedNodes) {
1982
+ posMap.set(node.id, { x: node.x, y: node.y });
1983
+ }
1984
+ compilation = compile();
1985
+ adjacencyMap = buildAdjacencyMap(compilation.edges);
1986
+ positionedNodes = compilation.nodes.map((node) => {
1987
+ const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
1988
+ return { ...node, x: pos.x, y: pos.y };
1989
+ });
1990
+ positionedEdges = compilation.edges.map((edge) => {
1991
+ const src = posMap.get(edge.source) ?? { x: 0, y: 0 };
1992
+ const tgt = posMap.get(edge.target) ?? { x: 0, y: 0 };
1993
+ return {
1994
+ ...edge,
1995
+ sourceX: src.x,
1996
+ sourceY: src.y,
1997
+ targetX: tgt.x,
1998
+ targetY: tgt.y
1999
+ };
2000
+ });
2001
+ spatialIndex.rebuild(positionedNodes);
2002
+ renderChrome2();
2003
+ renderLegend2();
2004
+ needsRender = true;
2005
+ scheduleRender();
2006
+ }
1827
2007
  function teardownSubsystems() {
1828
2008
  if (animFrameId !== null) {
1829
2009
  cancelAnimationFrame(animFrameId);
@@ -1873,6 +2053,8 @@ function createGraph(container, spec, options) {
1873
2053
  return {
1874
2054
  update() {
1875
2055
  },
2056
+ updateVisuals() {
2057
+ },
1876
2058
  search() {
1877
2059
  },
1878
2060
  clearSearch() {
@@ -1897,6 +2079,7 @@ function createGraph(container, spec, options) {
1897
2079
  }
1898
2080
  return {
1899
2081
  update,
2082
+ updateVisuals,
1900
2083
  search,
1901
2084
  clearSearch,
1902
2085
  zoomToFit,
@@ -3343,7 +3526,7 @@ function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
3343
3526
  }
3344
3527
  };
3345
3528
  }
3346
- function wireLegendInteraction(svg, _layout, onLegendToggle) {
3529
+ function wireLegendInteraction(svg, _layout, onLegendToggle, onEdit) {
3347
3530
  const legendEntries = svg.querySelectorAll("[data-legend-index]");
3348
3531
  const cleanups = [];
3349
3532
  const hiddenSeries = /* @__PURE__ */ new Set();
@@ -3356,11 +3539,13 @@ function wireLegendInteraction(svg, _layout, onLegendToggle) {
3356
3539
  entry.setAttribute("opacity", "1");
3357
3540
  entry.setAttribute("aria-label", `${label}: visible`);
3358
3541
  onLegendToggle?.(label, true);
3542
+ onEdit?.({ type: "legend-toggle", series: label, hidden: false });
3359
3543
  } else {
3360
3544
  hiddenSeries.add(label);
3361
3545
  entry.setAttribute("opacity", "0.3");
3362
3546
  entry.setAttribute("aria-label", `${label}: hidden`);
3363
3547
  onLegendToggle?.(label, false);
3548
+ onEdit?.({ type: "legend-toggle", series: label, hidden: true });
3364
3549
  }
3365
3550
  const marks = svg.querySelectorAll(".viz-mark");
3366
3551
  for (const mark of marks) {
@@ -3593,7 +3778,12 @@ function createChart(container, spec, options) {
3593
3778
  tooltipManager,
3594
3779
  currentLayout
3595
3780
  );
3596
- cleanupLegend = wireLegendInteraction(svgElement, currentLayout, options?.onLegendToggle);
3781
+ cleanupLegend = wireLegendInteraction(
3782
+ svgElement,
3783
+ currentLayout,
3784
+ options?.onLegendToggle,
3785
+ options?.onEdit
3786
+ );
3597
3787
  if (options?.onMarkClick || options?.onMarkHover || options?.onMarkLeave || options?.onAnnotationClick) {
3598
3788
  const specAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
3599
3789
  cleanupChartEvents = wireChartEvents(svgElement, currentLayout, specAnnotations, options);
@@ -3659,6 +3849,8 @@ function createChart(container, spec, options) {
3659
3849
  return exportSVG(svgElement);
3660
3850
  case "png":
3661
3851
  return exportPNG(svgElement, exportOptions);
3852
+ case "jpg":
3853
+ return exportJPG(svgElement, exportOptions);
3662
3854
  case "csv":
3663
3855
  return exportCSV(
3664
3856
  "data" in currentSpec && Array.isArray(currentSpec.data) ? currentSpec.data : []
@@ -4828,6 +5020,7 @@ export {
4828
5020
  createTable,
4829
5021
  createTooltipManager,
4830
5022
  exportCSV,
5023
+ exportJPG,
4831
5024
  exportPNG,
4832
5025
  exportSVG,
4833
5026
  observeResize,