@opendata-ai/openchart-vanilla 2.1.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 +24 -3
- package/dist/index.js +198 -11
- package/dist/index.js.map +1 -1
- package/dist/simulation-worker.js +9 -1
- package/package.json +3 -3
- package/src/__tests__/export.test.ts +29 -3
- package/src/export.ts +70 -0
- package/src/graph/canvas-renderer.ts +25 -0
- package/src/graph/interaction.ts +8 -0
- package/src/graph/simulation-worker.ts +19 -8
- package/src/graph/simulation.ts +16 -8
- package/src/graph/types.ts +1 -0
- package/src/graph/worker-protocol.ts +3 -0
- package/src/graph-mount.ts +161 -1
- package/src/index.ts +2 -2
- package/src/mount.ts +21 -7
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
|
*
|
|
@@ -74,10 +90,14 @@ interface GraphMountOptions {
|
|
|
74
90
|
responsive?: boolean;
|
|
75
91
|
onNodeClick?: (node: Record<string, unknown>) => void;
|
|
76
92
|
onNodeDoubleClick?: (node: Record<string, unknown>) => void;
|
|
93
|
+
onNodeHover?: (node: Record<string, unknown> | null) => void;
|
|
94
|
+
onEdgeHover?: (edge: Record<string, unknown> | null) => void;
|
|
77
95
|
onSelectionChange?: (nodeIds: string[]) => void;
|
|
78
96
|
}
|
|
79
97
|
interface GraphInstance {
|
|
80
98
|
update(spec: GraphSpec): void;
|
|
99
|
+
/** Re-compile encoding/legend/chrome without restarting the simulation. Preserves node positions. */
|
|
100
|
+
updateVisuals(spec: GraphSpec): void;
|
|
81
101
|
search(query: string): void;
|
|
82
102
|
clearSearch(): void;
|
|
83
103
|
zoomToFit(): void;
|
|
@@ -116,7 +136,7 @@ interface MountOptions extends ChartEventHandlers {
|
|
|
116
136
|
/** Enable responsive resizing. Defaults to true. */
|
|
117
137
|
responsive?: boolean;
|
|
118
138
|
}
|
|
119
|
-
interface ExportOptions extends
|
|
139
|
+
interface ExportOptions extends JPGExportOptions {
|
|
120
140
|
}
|
|
121
141
|
interface ChartInstance {
|
|
122
142
|
/** Re-compile and re-render with a new spec. */
|
|
@@ -126,8 +146,9 @@ interface ChartInstance {
|
|
|
126
146
|
/** Export the chart. */
|
|
127
147
|
export(format: 'svg'): string;
|
|
128
148
|
export(format: 'png', options?: ExportOptions): Promise<Blob>;
|
|
149
|
+
export(format: 'jpg', options?: ExportOptions): Promise<Blob>;
|
|
129
150
|
export(format: 'csv'): string;
|
|
130
|
-
export(format: 'svg' | 'png' | 'csv', options?: ExportOptions): string | Promise<Blob>;
|
|
151
|
+
export(format: 'svg' | 'png' | 'jpg' | 'csv', options?: ExportOptions): string | Promise<Blob>;
|
|
131
152
|
/** Remove all DOM elements and disconnect observers. */
|
|
132
153
|
destroy(): void;
|
|
133
154
|
/** The current compiled layout (for hooks / debugging). */
|
|
@@ -324,4 +345,4 @@ interface TooltipManager {
|
|
|
324
345
|
*/
|
|
325
346
|
declare function createTooltipManager(container: HTMLElement): TooltipManager;
|
|
326
347
|
|
|
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 };
|
|
348
|
+
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]);
|
|
@@ -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,
|
|
@@ -169,7 +212,8 @@ var GraphCanvasRenderer = class {
|
|
|
169
212
|
visibleEdges,
|
|
170
213
|
hasActiveNode,
|
|
171
214
|
connectedNodeIds,
|
|
172
|
-
isGesturing ? null : searchMatches
|
|
215
|
+
isGesturing ? null : searchMatches,
|
|
216
|
+
hoveredEdgeId
|
|
173
217
|
);
|
|
174
218
|
this.drawNodesBatched(
|
|
175
219
|
ctx,
|
|
@@ -218,11 +262,17 @@ var GraphCanvasRenderer = class {
|
|
|
218
262
|
// -------------------------------------------------------------------------
|
|
219
263
|
// Batched edge drawing
|
|
220
264
|
// -------------------------------------------------------------------------
|
|
221
|
-
drawEdgesBatched(ctx, edges, hasActiveNode, connectedNodeIds, searchMatches) {
|
|
265
|
+
drawEdgesBatched(ctx, edges, hasActiveNode, connectedNodeIds, searchMatches, hoveredEdgeId) {
|
|
222
266
|
const dimmedEdges = [];
|
|
223
267
|
const defaultEdges = [];
|
|
224
268
|
const connectedEdges = [];
|
|
269
|
+
let hoveredEdge = null;
|
|
225
270
|
for (const edge of edges) {
|
|
271
|
+
const edgeId = `${edge.source}->${edge.target}`;
|
|
272
|
+
if (edgeId === hoveredEdgeId) {
|
|
273
|
+
hoveredEdge = edge;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
226
276
|
const isConnected = hasActiveNode && connectedNodeIds.has(edge.source) && connectedNodeIds.has(edge.target);
|
|
227
277
|
const isDimmed = hasActiveNode && !isConnected;
|
|
228
278
|
if (isConnected) {
|
|
@@ -236,6 +286,19 @@ var GraphCanvasRenderer = class {
|
|
|
236
286
|
this.drawEdgeGroupBatched(ctx, dimmedEdges, EDGE_ALPHA_DIMMED, searchMatches);
|
|
237
287
|
this.drawEdgeGroupBatched(ctx, defaultEdges, EDGE_ALPHA_DEFAULT, searchMatches);
|
|
238
288
|
this.drawEdgeGroupBatched(ctx, connectedEdges, EDGE_ALPHA_CONNECTED, searchMatches);
|
|
289
|
+
if (hoveredEdge) {
|
|
290
|
+
const dash = DASH_PATTERNS[hoveredEdge.style] ?? DASH_PATTERNS.solid;
|
|
291
|
+
ctx.setLineDash(dash);
|
|
292
|
+
ctx.strokeStyle = hoveredEdge.stroke;
|
|
293
|
+
ctx.lineWidth = hoveredEdge.strokeWidth * 2;
|
|
294
|
+
ctx.globalAlpha = EDGE_ALPHA_CONNECTED;
|
|
295
|
+
ctx.beginPath();
|
|
296
|
+
ctx.moveTo(hoveredEdge.sourceX, hoveredEdge.sourceY);
|
|
297
|
+
ctx.lineTo(hoveredEdge.targetX, hoveredEdge.targetY);
|
|
298
|
+
ctx.stroke();
|
|
299
|
+
ctx.setLineDash([]);
|
|
300
|
+
ctx.globalAlpha = 1;
|
|
301
|
+
}
|
|
239
302
|
}
|
|
240
303
|
/**
|
|
241
304
|
* Draw a group of edges at a given alpha, batched by (stroke, strokeWidth, style).
|
|
@@ -718,6 +781,10 @@ var GraphInteractionManager = class {
|
|
|
718
781
|
}
|
|
719
782
|
const hitId = this.hitTest(x, y);
|
|
720
783
|
this.callbacks.onHoverChange(hitId);
|
|
784
|
+
if (!hitId) {
|
|
785
|
+
const graph = this.transform.screenToGraph(x, y);
|
|
786
|
+
this.callbacks.onBackgroundHover?.(graph.x, graph.y, x, y);
|
|
787
|
+
}
|
|
721
788
|
this.canvas.style.cursor = hitId ? "pointer" : "default";
|
|
722
789
|
}
|
|
723
790
|
onMouseUp(e) {
|
|
@@ -1216,13 +1283,18 @@ var SimulationManager = class _SimulationManager {
|
|
|
1216
1283
|
community: n.community
|
|
1217
1284
|
}));
|
|
1218
1285
|
this.syncNodeMap = new Map(this.syncNodes.map((n) => [n.id, n]));
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1286
|
+
const linkForce = forceLink(edges.map((e) => ({ ...e }))).id((d) => d.id).distance(config.linkDistance);
|
|
1287
|
+
if (config.linkStrength != null) {
|
|
1288
|
+
linkForce.strength(config.linkStrength);
|
|
1289
|
+
}
|
|
1290
|
+
const padding = config.collisionPadding ?? 2;
|
|
1291
|
+
this.syncSim = forceSimulation(this.syncNodes).force("link", linkForce).force("charge", forceManyBody().strength(config.chargeStrength)).force(
|
|
1223
1292
|
"collide",
|
|
1224
|
-
forceCollide().radius((d) => d.radius +
|
|
1293
|
+
forceCollide().radius((d) => d.radius + padding)
|
|
1225
1294
|
).force("gravityX", forceX(0).strength(0.05)).force("gravityY", forceY(0).strength(0.05)).alphaDecay(config.alphaDecay).velocityDecay(config.velocityDecay).stop();
|
|
1295
|
+
if (config.centerForce !== false) {
|
|
1296
|
+
this.syncSim.force("center", forceCenter(0, 0));
|
|
1297
|
+
}
|
|
1226
1298
|
if (config.clustering) {
|
|
1227
1299
|
const clusterFn = forceCluster(this.syncNodes, config.clustering.strength);
|
|
1228
1300
|
this.syncSim.force("cluster", clusterFn);
|
|
@@ -1467,6 +1539,7 @@ function createGraph(container, spec, options) {
|
|
|
1467
1539
|
let positionedEdges = [];
|
|
1468
1540
|
let adjacencyMap = /* @__PURE__ */ new Map();
|
|
1469
1541
|
let hoveredNodeId = null;
|
|
1542
|
+
let hoveredEdgeId = null;
|
|
1470
1543
|
let selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1471
1544
|
let animFrameId = null;
|
|
1472
1545
|
let needsRender = false;
|
|
@@ -1527,6 +1600,38 @@ function createGraph(container, spec, options) {
|
|
|
1527
1600
|
const node = compilation.nodes.find((n) => n.id === nodeId);
|
|
1528
1601
|
return node?.data ?? {};
|
|
1529
1602
|
}
|
|
1603
|
+
function pointToSegmentDist(px, py, ax, ay, bx, by) {
|
|
1604
|
+
const dx = bx - ax;
|
|
1605
|
+
const dy = by - ay;
|
|
1606
|
+
const lenSq = dx * dx + dy * dy;
|
|
1607
|
+
if (lenSq === 0) return Math.hypot(px - ax, py - ay);
|
|
1608
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / lenSq));
|
|
1609
|
+
return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
|
|
1610
|
+
}
|
|
1611
|
+
function hitTestEdge(graphX, graphY, threshold) {
|
|
1612
|
+
let bestDist = threshold;
|
|
1613
|
+
let bestEdgeId = null;
|
|
1614
|
+
for (const edge of positionedEdges) {
|
|
1615
|
+
const dist = pointToSegmentDist(
|
|
1616
|
+
graphX,
|
|
1617
|
+
graphY,
|
|
1618
|
+
edge.sourceX,
|
|
1619
|
+
edge.sourceY,
|
|
1620
|
+
edge.targetX,
|
|
1621
|
+
edge.targetY
|
|
1622
|
+
);
|
|
1623
|
+
if (dist < bestDist) {
|
|
1624
|
+
bestDist = dist;
|
|
1625
|
+
bestEdgeId = `${edge.source}->${edge.target}`;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return bestEdgeId;
|
|
1629
|
+
}
|
|
1630
|
+
function edgeDataById(edgeId) {
|
|
1631
|
+
const [source, target] = edgeId.split("->");
|
|
1632
|
+
const edge = compilation.edges.find((e) => e.source === source && e.target === target);
|
|
1633
|
+
return edge?.data ?? null;
|
|
1634
|
+
}
|
|
1530
1635
|
function createDOM() {
|
|
1531
1636
|
const { width, height } = getContainerDimensions();
|
|
1532
1637
|
const isDark = resolveDarkMode(options?.darkMode);
|
|
@@ -1601,7 +1706,10 @@ function createGraph(container, spec, options) {
|
|
|
1601
1706
|
clustering: config.clustering,
|
|
1602
1707
|
alphaDecay: config.alphaDecay,
|
|
1603
1708
|
velocityDecay: config.velocityDecay,
|
|
1604
|
-
collisionRadius: config.collisionRadius
|
|
1709
|
+
collisionRadius: config.collisionRadius,
|
|
1710
|
+
collisionPadding: config.collisionPadding,
|
|
1711
|
+
linkStrength: config.linkStrength,
|
|
1712
|
+
centerForce: config.centerForce
|
|
1605
1713
|
});
|
|
1606
1714
|
simulation.onTick((positions, _alpha) => {
|
|
1607
1715
|
if (destroyed) return;
|
|
@@ -1661,6 +1769,7 @@ function createGraph(container, spec, options) {
|
|
|
1661
1769
|
edges: positionedEdges,
|
|
1662
1770
|
transform: { x: transform.x, y: transform.y, k: transform.k },
|
|
1663
1771
|
hoveredNodeId,
|
|
1772
|
+
hoveredEdgeId,
|
|
1664
1773
|
selectedNodeIds,
|
|
1665
1774
|
adjacencyMap,
|
|
1666
1775
|
theme: compilation.theme,
|
|
@@ -1683,7 +1792,16 @@ function createGraph(container, spec, options) {
|
|
|
1683
1792
|
hoveredNodeId = nodeId;
|
|
1684
1793
|
needsRender = true;
|
|
1685
1794
|
scheduleRender();
|
|
1795
|
+
if (nodeId) {
|
|
1796
|
+
options?.onNodeHover?.(nodeDataById(nodeId));
|
|
1797
|
+
} else {
|
|
1798
|
+
options?.onNodeHover?.(null);
|
|
1799
|
+
}
|
|
1686
1800
|
if (nodeId && tooltipManager) {
|
|
1801
|
+
if (hoveredEdgeId) {
|
|
1802
|
+
hoveredEdgeId = null;
|
|
1803
|
+
options?.onEdgeHover?.(null);
|
|
1804
|
+
}
|
|
1687
1805
|
const content = compilation.tooltipDescriptors.get(nodeId);
|
|
1688
1806
|
if (content) {
|
|
1689
1807
|
const node = positionedNodes.find((n) => n.id === nodeId);
|
|
@@ -1692,10 +1810,35 @@ function createGraph(container, spec, options) {
|
|
|
1692
1810
|
tooltipManager.show(content, screen.x, screen.y);
|
|
1693
1811
|
}
|
|
1694
1812
|
}
|
|
1695
|
-
} else {
|
|
1813
|
+
} else if (!nodeId) {
|
|
1696
1814
|
tooltipManager?.hide();
|
|
1697
1815
|
}
|
|
1698
1816
|
},
|
|
1817
|
+
onBackgroundHover(graphX, graphY, screenX, screenY) {
|
|
1818
|
+
const transform = interactionManager?.getTransform();
|
|
1819
|
+
const threshold = 5 / (transform?.k ?? 1);
|
|
1820
|
+
const edgeId = hitTestEdge(graphX, graphY, threshold);
|
|
1821
|
+
if (edgeId !== hoveredEdgeId) {
|
|
1822
|
+
hoveredEdgeId = edgeId;
|
|
1823
|
+
needsRender = true;
|
|
1824
|
+
scheduleRender();
|
|
1825
|
+
if (edgeId) {
|
|
1826
|
+
const data = edgeDataById(edgeId);
|
|
1827
|
+
options?.onEdgeHover?.(data);
|
|
1828
|
+
if (tooltipManager && data) {
|
|
1829
|
+
const fields = Object.entries(data).filter(([key]) => key !== "source" && key !== "target").filter(([, value]) => value != null).map(([key, value]) => ({
|
|
1830
|
+
label: key,
|
|
1831
|
+
value: typeof value === "number" ? value.toLocaleString() : String(value)
|
|
1832
|
+
}));
|
|
1833
|
+
const [source, target] = edgeId.split("->");
|
|
1834
|
+
tooltipManager.show({ title: `${source} \u2192 ${target}`, fields }, screenX, screenY);
|
|
1835
|
+
}
|
|
1836
|
+
} else {
|
|
1837
|
+
options?.onEdgeHover?.(null);
|
|
1838
|
+
tooltipManager?.hide();
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1699
1842
|
onSelectionChange(nodeIds) {
|
|
1700
1843
|
selectedNodeIds = new Set(nodeIds);
|
|
1701
1844
|
needsRender = true;
|
|
@@ -1821,9 +1964,40 @@ function createGraph(container, spec, options) {
|
|
|
1821
1964
|
initSimulation();
|
|
1822
1965
|
initInteraction();
|
|
1823
1966
|
hoveredNodeId = null;
|
|
1967
|
+
hoveredEdgeId = null;
|
|
1824
1968
|
selectedNodeIds = /* @__PURE__ */ new Set();
|
|
1825
1969
|
searchManager.clearSearch();
|
|
1826
1970
|
}
|
|
1971
|
+
function updateVisuals(newSpec) {
|
|
1972
|
+
if (destroyed) return;
|
|
1973
|
+
currentSpec = newSpec;
|
|
1974
|
+
const posMap = /* @__PURE__ */ new Map();
|
|
1975
|
+
for (const node of positionedNodes) {
|
|
1976
|
+
posMap.set(node.id, { x: node.x, y: node.y });
|
|
1977
|
+
}
|
|
1978
|
+
compilation = compile();
|
|
1979
|
+
adjacencyMap = buildAdjacencyMap(compilation.edges);
|
|
1980
|
+
positionedNodes = compilation.nodes.map((node) => {
|
|
1981
|
+
const pos = posMap.get(node.id) ?? { x: 0, y: 0 };
|
|
1982
|
+
return { ...node, x: pos.x, y: pos.y };
|
|
1983
|
+
});
|
|
1984
|
+
positionedEdges = compilation.edges.map((edge) => {
|
|
1985
|
+
const src = posMap.get(edge.source) ?? { x: 0, y: 0 };
|
|
1986
|
+
const tgt = posMap.get(edge.target) ?? { x: 0, y: 0 };
|
|
1987
|
+
return {
|
|
1988
|
+
...edge,
|
|
1989
|
+
sourceX: src.x,
|
|
1990
|
+
sourceY: src.y,
|
|
1991
|
+
targetX: tgt.x,
|
|
1992
|
+
targetY: tgt.y
|
|
1993
|
+
};
|
|
1994
|
+
});
|
|
1995
|
+
spatialIndex.rebuild(positionedNodes);
|
|
1996
|
+
renderChrome2();
|
|
1997
|
+
renderLegend2();
|
|
1998
|
+
needsRender = true;
|
|
1999
|
+
scheduleRender();
|
|
2000
|
+
}
|
|
1827
2001
|
function teardownSubsystems() {
|
|
1828
2002
|
if (animFrameId !== null) {
|
|
1829
2003
|
cancelAnimationFrame(animFrameId);
|
|
@@ -1873,6 +2047,8 @@ function createGraph(container, spec, options) {
|
|
|
1873
2047
|
return {
|
|
1874
2048
|
update() {
|
|
1875
2049
|
},
|
|
2050
|
+
updateVisuals() {
|
|
2051
|
+
},
|
|
1876
2052
|
search() {
|
|
1877
2053
|
},
|
|
1878
2054
|
clearSearch() {
|
|
@@ -1897,6 +2073,7 @@ function createGraph(container, spec, options) {
|
|
|
1897
2073
|
}
|
|
1898
2074
|
return {
|
|
1899
2075
|
update,
|
|
2076
|
+
updateVisuals,
|
|
1900
2077
|
search,
|
|
1901
2078
|
clearSearch,
|
|
1902
2079
|
zoomToFit,
|
|
@@ -3343,7 +3520,7 @@ function wireSeriesLabelDrag(svg, spec, onEdit, setDragging) {
|
|
|
3343
3520
|
}
|
|
3344
3521
|
};
|
|
3345
3522
|
}
|
|
3346
|
-
function wireLegendInteraction(svg, _layout, onLegendToggle) {
|
|
3523
|
+
function wireLegendInteraction(svg, _layout, onLegendToggle, onEdit) {
|
|
3347
3524
|
const legendEntries = svg.querySelectorAll("[data-legend-index]");
|
|
3348
3525
|
const cleanups = [];
|
|
3349
3526
|
const hiddenSeries = /* @__PURE__ */ new Set();
|
|
@@ -3356,11 +3533,13 @@ function wireLegendInteraction(svg, _layout, onLegendToggle) {
|
|
|
3356
3533
|
entry.setAttribute("opacity", "1");
|
|
3357
3534
|
entry.setAttribute("aria-label", `${label}: visible`);
|
|
3358
3535
|
onLegendToggle?.(label, true);
|
|
3536
|
+
onEdit?.({ type: "legend-toggle", series: label, hidden: false });
|
|
3359
3537
|
} else {
|
|
3360
3538
|
hiddenSeries.add(label);
|
|
3361
3539
|
entry.setAttribute("opacity", "0.3");
|
|
3362
3540
|
entry.setAttribute("aria-label", `${label}: hidden`);
|
|
3363
3541
|
onLegendToggle?.(label, false);
|
|
3542
|
+
onEdit?.({ type: "legend-toggle", series: label, hidden: true });
|
|
3364
3543
|
}
|
|
3365
3544
|
const marks = svg.querySelectorAll(".viz-mark");
|
|
3366
3545
|
for (const mark of marks) {
|
|
@@ -3593,7 +3772,12 @@ function createChart(container, spec, options) {
|
|
|
3593
3772
|
tooltipManager,
|
|
3594
3773
|
currentLayout
|
|
3595
3774
|
);
|
|
3596
|
-
cleanupLegend = wireLegendInteraction(
|
|
3775
|
+
cleanupLegend = wireLegendInteraction(
|
|
3776
|
+
svgElement,
|
|
3777
|
+
currentLayout,
|
|
3778
|
+
options?.onLegendToggle,
|
|
3779
|
+
options?.onEdit
|
|
3780
|
+
);
|
|
3597
3781
|
if (options?.onMarkClick || options?.onMarkHover || options?.onMarkLeave || options?.onAnnotationClick) {
|
|
3598
3782
|
const specAnnotations = "annotations" in currentSpec && Array.isArray(currentSpec.annotations) ? currentSpec.annotations : [];
|
|
3599
3783
|
cleanupChartEvents = wireChartEvents(svgElement, currentLayout, specAnnotations, options);
|
|
@@ -3659,6 +3843,8 @@ function createChart(container, spec, options) {
|
|
|
3659
3843
|
return exportSVG(svgElement);
|
|
3660
3844
|
case "png":
|
|
3661
3845
|
return exportPNG(svgElement, exportOptions);
|
|
3846
|
+
case "jpg":
|
|
3847
|
+
return exportJPG(svgElement, exportOptions);
|
|
3662
3848
|
case "csv":
|
|
3663
3849
|
return exportCSV(
|
|
3664
3850
|
"data" in currentSpec && Array.isArray(currentSpec.data) ? currentSpec.data : []
|
|
@@ -4828,6 +5014,7 @@ export {
|
|
|
4828
5014
|
createTable,
|
|
4829
5015
|
createTooltipManager,
|
|
4830
5016
|
exportCSV,
|
|
5017
|
+
exportJPG,
|
|
4831
5018
|
exportPNG,
|
|
4832
5019
|
exportSVG,
|
|
4833
5020
|
observeResize,
|