@opendata-ai/openchart-vanilla 6.5.2 → 6.6.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.js CHANGED
@@ -2788,7 +2788,7 @@ function createGraph(container, spec, options) {
2788
2788
  }
2789
2789
  chromeEl = document.createElement("div");
2790
2790
  chromeEl.className = "oc-graph-chrome";
2791
- renderChrome2();
2791
+ renderChrome3();
2792
2792
  wrapper.appendChild(chromeEl);
2793
2793
  canvas = document.createElement("canvas");
2794
2794
  canvas.className = "oc-graph-canvas";
@@ -2800,7 +2800,7 @@ function createGraph(container, spec, options) {
2800
2800
  if (options?.legend !== false) {
2801
2801
  legendEl = document.createElement("div");
2802
2802
  legendEl.className = "oc-graph-legend";
2803
- renderLegend2();
2803
+ renderLegend3();
2804
2804
  wrapper.appendChild(legendEl);
2805
2805
  }
2806
2806
  container.appendChild(wrapper);
@@ -2809,7 +2809,7 @@ function createGraph(container, spec, options) {
2809
2809
  renderer = new GraphCanvasRenderer(canvas);
2810
2810
  renderer.resize(width, canvasHeight);
2811
2811
  }
2812
- function renderChrome2() {
2812
+ function renderChrome3() {
2813
2813
  if (!chromeEl) return;
2814
2814
  let html = "";
2815
2815
  if (compilation.chrome.title) {
@@ -2825,7 +2825,7 @@ function createGraph(container, spec, options) {
2825
2825
  chromeEl.style.display = "";
2826
2826
  }
2827
2827
  }
2828
- function renderLegend2() {
2828
+ function renderLegend3() {
2829
2829
  if (!legendEl) return;
2830
2830
  const entries = compilation.legend.entries;
2831
2831
  if (entries.length === 0) {
@@ -3123,8 +3123,8 @@ function createGraph(container, spec, options) {
3123
3123
  compilation = compile();
3124
3124
  adjacencyMap = buildAdjacencyMap(compilation.edges);
3125
3125
  buildDataMaps();
3126
- renderChrome2();
3127
- renderLegend2();
3126
+ renderChrome3();
3127
+ renderLegend3();
3128
3128
  initSimulation();
3129
3129
  initInteraction();
3130
3130
  hoveredNodeId = null;
@@ -3158,8 +3158,8 @@ function createGraph(container, spec, options) {
3158
3158
  };
3159
3159
  });
3160
3160
  spatialIndex.rebuild(positionedNodes);
3161
- renderChrome2();
3162
- renderLegend2();
3161
+ renderChrome3();
3162
+ renderLegend3();
3163
3163
  needsRender = true;
3164
3164
  scheduleRender();
3165
3165
  }
@@ -4886,7 +4886,7 @@ function wireAnnotationDrag(svg, specAnnotations, onAnnotationEdit, onEdit, setD
4886
4886
  };
4887
4887
  }
4888
4888
  function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4889
- const SVG_NS2 = "http://www.w3.org/2000/svg";
4889
+ const SVG_NS3 = "http://www.w3.org/2000/svg";
4890
4890
  const cleanups = [];
4891
4891
  const annotationGroups = svg.querySelectorAll(".oc-annotation-text");
4892
4892
  for (const el of annotationGroups) {
@@ -4925,7 +4925,7 @@ function wireConnectorEndpointDrag(svg, specAnnotations, onEdit, setDragging) {
4925
4925
  const createdHandles = [];
4926
4926
  for (const ep of endpoints) {
4927
4927
  if (!Number.isFinite(ep.cx) || !Number.isFinite(ep.cy)) continue;
4928
- const handleEl = document.createElementNS(SVG_NS2, "circle");
4928
+ const handleEl = document.createElementNS(SVG_NS3, "circle");
4929
4929
  handleEl.setAttribute("class", "oc-connector-handle");
4930
4930
  handleEl.setAttribute("data-endpoint", ep.name);
4931
4931
  handleEl.setAttribute("cx", String(ep.cx));
@@ -6383,6 +6383,728 @@ function renderCell(cell) {
6383
6383
  }
6384
6384
  }
6385
6385
 
6386
+ // src/sankey-mount.ts
6387
+ import { compileSankey } from "@opendata-ai/openchart-engine";
6388
+
6389
+ // src/sankey-renderer.ts
6390
+ import { BRAND_MIN_WIDTH as BRAND_MIN_WIDTH3, estimateTextWidth as estimateTextWidth2 } from "@opendata-ai/openchart-core";
6391
+ import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
6392
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
6393
+ var XLINK_NS2 = "http://www.w3.org/1999/xlink";
6394
+ var BRAND_URL2 = "https://opendata.ai";
6395
+ var EASE_VAR_MAP2 = {
6396
+ smooth: "var(--oc-ease-smooth)",
6397
+ snappy: "var(--oc-ease-snappy)"
6398
+ };
6399
+ function createSVGElement2(tag) {
6400
+ return document.createElementNS(SVG_NS2, tag);
6401
+ }
6402
+ function setAttrs2(el, attrs) {
6403
+ for (const [key, value] of Object.entries(attrs)) {
6404
+ el.setAttribute(key, String(value));
6405
+ }
6406
+ }
6407
+ function applyTextStyle2(el, style) {
6408
+ setAttrs2(el, {
6409
+ "font-family": style.fontFamily,
6410
+ "font-size": style.fontSize,
6411
+ "font-weight": style.fontWeight
6412
+ });
6413
+ el.style.setProperty("fill", style.fill);
6414
+ if (style.textAnchor) {
6415
+ el.setAttribute("text-anchor", style.textAnchor);
6416
+ }
6417
+ if (style.dominantBaseline) {
6418
+ el.setAttribute("dominant-baseline", style.dominantBaseline);
6419
+ }
6420
+ if (style.fontVariant) {
6421
+ el.setAttribute("font-variant", style.fontVariant);
6422
+ }
6423
+ }
6424
+ function stampAnimationAttrs2(el, mark, fallbackIndex, animation) {
6425
+ if (!animation?.enabled) return;
6426
+ const idx = mark.animationIndex ?? fallbackIndex;
6427
+ el.setAttribute("data-animation-index", String(idx));
6428
+ el.style.setProperty("--oc-mark-index", String(idx));
6429
+ }
6430
+ function wrapText2(text, fontSize, fontWeight, maxWidth) {
6431
+ if (maxWidth <= 0) return [text];
6432
+ const AVG_CHAR_WIDTH = 0.55;
6433
+ const WEIGHT_FACTORS = {
6434
+ 100: 0.9,
6435
+ 200: 0.92,
6436
+ 300: 0.95,
6437
+ 400: 1,
6438
+ 500: 1.02,
6439
+ 600: 1.05,
6440
+ 700: 1.08,
6441
+ 800: 1.1,
6442
+ 900: 1.12
6443
+ };
6444
+ const weightFactor = WEIGHT_FACTORS[fontWeight] ?? 1;
6445
+ const charWidth = fontSize * AVG_CHAR_WIDTH * weightFactor;
6446
+ const maxChars = Math.floor(maxWidth / charWidth);
6447
+ if (text.length <= maxChars) return [text];
6448
+ const words = text.split(" ");
6449
+ const lines = [];
6450
+ let current = "";
6451
+ for (const word of words) {
6452
+ const candidate = current ? `${current} ${word}` : word;
6453
+ if (candidate.length > maxChars && current) {
6454
+ lines.push(current);
6455
+ current = word;
6456
+ } else {
6457
+ current = candidate;
6458
+ }
6459
+ }
6460
+ if (current) lines.push(current);
6461
+ return lines;
6462
+ }
6463
+ function renderChromeElement2(parent, element, className, chromeKey) {
6464
+ const text = createSVGElement2("text");
6465
+ setAttrs2(text, { x: element.x, y: element.y });
6466
+ applyTextStyle2(text, element.style);
6467
+ text.setAttribute("class", className);
6468
+ text.setAttribute("data-chrome-key", chromeKey);
6469
+ const lines = wrapText2(
6470
+ element.text,
6471
+ element.style.fontSize,
6472
+ element.style.fontWeight,
6473
+ element.maxWidth
6474
+ );
6475
+ if (lines.length === 1) {
6476
+ text.textContent = element.text;
6477
+ } else {
6478
+ const lineHeight = element.style.fontSize * (element.style.lineHeight ?? 1.3);
6479
+ for (let i = 0; i < lines.length; i++) {
6480
+ const tspan = createSVGElement2("tspan");
6481
+ setAttrs2(tspan, { x: element.x, dy: i === 0 ? 0 : lineHeight });
6482
+ tspan.textContent = lines[i];
6483
+ text.appendChild(tspan);
6484
+ }
6485
+ }
6486
+ parent.appendChild(text);
6487
+ }
6488
+ function renderChrome2(parent, layout) {
6489
+ const g = createSVGElement2("g");
6490
+ g.setAttribute("class", "oc-chrome");
6491
+ const { chrome } = layout;
6492
+ if (chrome.title) {
6493
+ renderChromeElement2(g, chrome.title, "oc-title", "title");
6494
+ }
6495
+ if (chrome.subtitle) {
6496
+ renderChromeElement2(g, chrome.subtitle, "oc-subtitle", "subtitle");
6497
+ }
6498
+ const bottomOffset = layout.area.y + layout.area.height;
6499
+ if (chrome.source) {
6500
+ renderChromeElement2(
6501
+ g,
6502
+ { ...chrome.source, y: bottomOffset + chrome.source.y },
6503
+ "oc-source",
6504
+ "source"
6505
+ );
6506
+ }
6507
+ if (chrome.byline) {
6508
+ renderChromeElement2(
6509
+ g,
6510
+ { ...chrome.byline, y: bottomOffset + chrome.byline.y },
6511
+ "oc-byline",
6512
+ "byline"
6513
+ );
6514
+ }
6515
+ if (chrome.footer) {
6516
+ renderChromeElement2(
6517
+ g,
6518
+ { ...chrome.footer, y: bottomOffset + chrome.footer.y },
6519
+ "oc-footer",
6520
+ "footer"
6521
+ );
6522
+ }
6523
+ parent.appendChild(g);
6524
+ }
6525
+ function renderBrand2(parent, layout) {
6526
+ if (layout.dimensions.width < BRAND_MIN_WIDTH3) return;
6527
+ const { width } = layout.dimensions;
6528
+ const padding = layout.theme.spacing.padding;
6529
+ const rightEdge = width - padding;
6530
+ const fill = layout.theme.colors.axis;
6531
+ const bottomOffset = layout.area.y + layout.area.height;
6532
+ const { chrome } = layout;
6533
+ const firstBottom = chrome.source ?? chrome.byline ?? chrome.footer;
6534
+ const chromeY = firstBottom ? bottomOffset + firstBottom.y : bottomOffset + layout.theme.spacing.chartToFooter;
6535
+ const a2 = createSVGElement2("a");
6536
+ a2.setAttribute("href", BRAND_URL2);
6537
+ a2.setAttributeNS(XLINK_NS2, "xlink:href", BRAND_URL2);
6538
+ a2.setAttribute("target", "_blank");
6539
+ a2.setAttribute("rel", "noopener");
6540
+ a2.setAttribute("class", "oc-chrome-ref");
6541
+ const text = createSVGElement2("text");
6542
+ setAttrs2(text, {
6543
+ x: rightEdge,
6544
+ y: chromeY,
6545
+ "dominant-baseline": "hanging",
6546
+ "text-anchor": "end",
6547
+ "font-family": layout.theme.fonts.family,
6548
+ "font-size": 20,
6549
+ "fill-opacity": 0.55
6550
+ });
6551
+ text.style.setProperty("fill", fill);
6552
+ const openSpan = createSVGElement2("tspan");
6553
+ setAttrs2(openSpan, { "font-weight": 500 });
6554
+ openSpan.textContent = "Open";
6555
+ const dataSpan = createSVGElement2("tspan");
6556
+ setAttrs2(dataSpan, { "font-weight": 600 });
6557
+ dataSpan.textContent = "Data";
6558
+ text.appendChild(openSpan);
6559
+ text.appendChild(dataSpan);
6560
+ a2.appendChild(text);
6561
+ parent.appendChild(a2);
6562
+ }
6563
+ function renderLegend2(parent, legend) {
6564
+ if (legend.entries.length === 0) return;
6565
+ const g = createSVGElement2("g");
6566
+ g.setAttribute("class", "oc-legend");
6567
+ g.setAttribute("role", "list");
6568
+ g.setAttribute("aria-label", "Chart legend");
6569
+ const isHorizontal = legend.position === "top" || legend.position === "bottom";
6570
+ let offsetX = legend.bounds.x;
6571
+ let offsetY = legend.bounds.y;
6572
+ for (let i = 0; i < legend.entries.length; i++) {
6573
+ const entry = legend.entries[i];
6574
+ if (isHorizontal && i > 0) {
6575
+ const labelWidth = estimateTextWidth2(
6576
+ entry.label,
6577
+ legend.labelStyle.fontSize,
6578
+ legend.labelStyle.fontWeight
6579
+ );
6580
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
6581
+ if (offsetX + entryWidth > legend.bounds.x + legend.bounds.width) {
6582
+ offsetX = legend.bounds.x;
6583
+ offsetY += legend.swatchSize + 6;
6584
+ }
6585
+ }
6586
+ const entryG = createSVGElement2("g");
6587
+ entryG.setAttribute("class", "oc-legend-entry");
6588
+ entryG.setAttribute("role", "listitem");
6589
+ entryG.setAttribute("data-legend-index", String(i));
6590
+ entryG.setAttribute("data-legend-label", entry.label);
6591
+ if (entry.overflow) {
6592
+ entryG.setAttribute("data-legend-overflow", "true");
6593
+ entryG.setAttribute("aria-label", entry.label);
6594
+ entryG.setAttribute("opacity", "0.5");
6595
+ } else {
6596
+ entryG.setAttribute(
6597
+ "aria-label",
6598
+ `${entry.label}: ${entry.active !== false ? "visible" : "hidden"}`
6599
+ );
6600
+ entryG.setAttribute("style", "cursor: pointer");
6601
+ if (entry.active === false) {
6602
+ entryG.setAttribute("opacity", "0.3");
6603
+ }
6604
+ }
6605
+ const rect = createSVGElement2("rect");
6606
+ setAttrs2(rect, {
6607
+ x: offsetX,
6608
+ y: offsetY,
6609
+ width: legend.swatchSize,
6610
+ height: legend.swatchSize,
6611
+ fill: entry.color,
6612
+ rx: 2
6613
+ });
6614
+ entryG.appendChild(rect);
6615
+ const label = createSVGElement2("text");
6616
+ setAttrs2(label, {
6617
+ x: offsetX + legend.swatchSize + legend.swatchGap,
6618
+ y: offsetY + legend.swatchSize / 2,
6619
+ "dominant-baseline": "central"
6620
+ });
6621
+ applyTextStyle2(label, legend.labelStyle);
6622
+ label.textContent = entry.label;
6623
+ entryG.appendChild(label);
6624
+ g.appendChild(entryG);
6625
+ if (isHorizontal) {
6626
+ const labelWidth = estimateTextWidth2(
6627
+ entry.label,
6628
+ legend.labelStyle.fontSize,
6629
+ legend.labelStyle.fontWeight
6630
+ );
6631
+ const entryWidth = legend.swatchSize + legend.swatchGap + labelWidth + legend.entryGap;
6632
+ offsetX += entryWidth;
6633
+ } else {
6634
+ offsetY += legend.swatchSize + legend.entryGap;
6635
+ }
6636
+ }
6637
+ parent.appendChild(g);
6638
+ }
6639
+ function buildNodePositionMap(nodes) {
6640
+ const map = /* @__PURE__ */ new Map();
6641
+ for (const node of nodes) {
6642
+ map.set(node.nodeId, { x: node.x, width: node.width });
6643
+ }
6644
+ return map;
6645
+ }
6646
+ function renderGradientDefs(defs, links, nodePositions) {
6647
+ for (let i = 0; i < links.length; i++) {
6648
+ const link = links[i];
6649
+ if (link.sourceColor === link.targetColor) continue;
6650
+ const gradId = `oc-sg-${link.sourceId}-${link.targetId}-${i}`;
6651
+ const gradient = createSVGElement2("linearGradient");
6652
+ gradient.setAttribute("id", gradId);
6653
+ gradient.setAttribute("gradientUnits", "userSpaceOnUse");
6654
+ const sourcePos = nodePositions.get(link.sourceId);
6655
+ const targetPos = nodePositions.get(link.targetId);
6656
+ const x1 = sourcePos ? sourcePos.x + sourcePos.width : 0;
6657
+ const x22 = targetPos ? targetPos.x : 0;
6658
+ gradient.setAttribute("x1", String(x1));
6659
+ gradient.setAttribute("x2", String(x22));
6660
+ const stop0 = createSVGElement2("stop");
6661
+ setAttrs2(stop0, { offset: "0%", "stop-color": link.sourceColor });
6662
+ gradient.appendChild(stop0);
6663
+ const stop1 = createSVGElement2("stop");
6664
+ setAttrs2(stop1, { offset: "100%", "stop-color": link.targetColor });
6665
+ gradient.appendChild(stop1);
6666
+ defs.appendChild(gradient);
6667
+ }
6668
+ }
6669
+ function renderLinks(parent, links, _nodePositions, animation) {
6670
+ const g = createSVGElement2("g");
6671
+ g.setAttribute("class", "oc-sankey-links");
6672
+ for (let i = 0; i < links.length; i++) {
6673
+ const link = links[i];
6674
+ const linkG = createSVGElement2("g");
6675
+ linkG.setAttribute("class", "oc-sankey-link");
6676
+ linkG.setAttribute("data-mark-id", `link-${link.sourceId}-${link.targetId}`);
6677
+ linkG.setAttribute("data-source", link.sourceId);
6678
+ linkG.setAttribute("data-target", link.targetId);
6679
+ if (link.aria?.label) {
6680
+ linkG.setAttribute("aria-label", link.aria.label);
6681
+ }
6682
+ stampAnimationAttrs2(linkG, link, i, animation);
6683
+ const path = createSVGElement2("path");
6684
+ path.setAttribute("d", link.path);
6685
+ path.setAttribute("stroke", "none");
6686
+ path.setAttribute("fill-opacity", String(link.fillOpacity));
6687
+ if (link.sourceColor !== link.targetColor) {
6688
+ const gradId = `oc-sg-${link.sourceId}-${link.targetId}-${i}`;
6689
+ path.setAttribute("fill", `url(#${gradId})`);
6690
+ } else {
6691
+ path.setAttribute("fill", link.sourceColor);
6692
+ }
6693
+ linkG.appendChild(path);
6694
+ g.appendChild(linkG);
6695
+ }
6696
+ parent.appendChild(g);
6697
+ }
6698
+ function renderNodes(parent, nodes, animation) {
6699
+ const g = createSVGElement2("g");
6700
+ g.setAttribute("class", "oc-sankey-nodes");
6701
+ for (let i = 0; i < nodes.length; i++) {
6702
+ const node = nodes[i];
6703
+ const nodeG = createSVGElement2("g");
6704
+ nodeG.setAttribute("class", "oc-sankey-node");
6705
+ nodeG.setAttribute("data-mark-id", `node-${node.nodeId}`);
6706
+ nodeG.setAttribute("data-node-id", node.nodeId);
6707
+ if (node.aria?.label) {
6708
+ nodeG.setAttribute("aria-label", node.aria.label);
6709
+ }
6710
+ stampAnimationAttrs2(nodeG, node, i, animation);
6711
+ const rect = createSVGElement2("rect");
6712
+ setAttrs2(rect, {
6713
+ x: node.x,
6714
+ y: node.y,
6715
+ width: node.width,
6716
+ height: Math.max(node.height, 1),
6717
+ // Ensure at least 1px visibility
6718
+ fill: node.fill,
6719
+ rx: node.cornerRadius
6720
+ });
6721
+ if (node.stroke) {
6722
+ rect.setAttribute("stroke", node.stroke);
6723
+ }
6724
+ if (node.strokeWidth) {
6725
+ rect.setAttribute("stroke-width", String(node.strokeWidth));
6726
+ }
6727
+ nodeG.appendChild(rect);
6728
+ g.appendChild(nodeG);
6729
+ }
6730
+ parent.appendChild(g);
6731
+ }
6732
+ function renderLabels(parent, nodes) {
6733
+ const g = createSVGElement2("g");
6734
+ g.setAttribute("class", "oc-sankey-labels");
6735
+ for (const node of nodes) {
6736
+ const { label } = node;
6737
+ if (!label.visible) continue;
6738
+ const text = createSVGElement2("text");
6739
+ setAttrs2(text, { x: label.x, y: label.y });
6740
+ applyTextStyle2(text, label.style);
6741
+ text.textContent = label.text;
6742
+ g.appendChild(text);
6743
+ }
6744
+ parent.appendChild(g);
6745
+ }
6746
+ function renderSankeySVG(layout, animation) {
6747
+ const { width, height } = layout.dimensions;
6748
+ const svg = createSVGElement2("svg");
6749
+ setAttrs2(svg, {
6750
+ viewBox: `0 0 ${width} ${height}`,
6751
+ xmlns: SVG_NS2,
6752
+ overflow: "visible"
6753
+ });
6754
+ svg.style.height = `${height}px`;
6755
+ svg.setAttribute("role", layout.a11y.role);
6756
+ svg.setAttribute("aria-label", layout.a11y.altText);
6757
+ const animate = animation?.enabled;
6758
+ const classes = animate ? "oc-chart oc-sankey oc-animate" : "oc-chart oc-sankey";
6759
+ svg.setAttribute("class", classes);
6760
+ if (animation?.enabled) {
6761
+ const totalMarks = layout.nodes.length + layout.links.length;
6762
+ const stagger = clampStaggerDelay2(animation.staggerDelay, totalMarks);
6763
+ svg.style.setProperty("--oc-animation-duration", `${animation.duration}ms`);
6764
+ svg.style.setProperty("--oc-animation-stagger", `${stagger}ms`);
6765
+ svg.style.setProperty("--oc-annotation-delay", `${animation.annotationDelay}ms`);
6766
+ const easeVar = EASE_VAR_MAP2[animation.ease] || EASE_VAR_MAP2.smooth;
6767
+ svg.style.setProperty("--oc-animation-ease", easeVar);
6768
+ }
6769
+ const bg = createSVGElement2("rect");
6770
+ bg.setAttribute("class", "oc-background");
6771
+ setAttrs2(bg, {
6772
+ x: 0,
6773
+ y: 0,
6774
+ width,
6775
+ height,
6776
+ fill: layout.theme.colors.background
6777
+ });
6778
+ svg.appendChild(bg);
6779
+ const nodePositions = buildNodePositionMap(layout.nodes);
6780
+ const defs = createSVGElement2("defs");
6781
+ renderGradientDefs(defs, layout.links, nodePositions);
6782
+ svg.appendChild(defs);
6783
+ renderLinks(svg, layout.links, nodePositions, animation);
6784
+ renderNodes(svg, layout.nodes, animation);
6785
+ renderLabels(svg, layout.nodes);
6786
+ renderLegend2(svg, layout.legend);
6787
+ renderChrome2(svg, layout);
6788
+ renderBrand2(svg, layout);
6789
+ return svg;
6790
+ }
6791
+
6792
+ // src/sankey-mount.ts
6793
+ function resolveDarkMode3(mode) {
6794
+ if (mode === "force") return true;
6795
+ if (mode === "off" || mode === void 0) return false;
6796
+ if (typeof window !== "undefined" && window.matchMedia) {
6797
+ return window.matchMedia("(prefers-color-scheme: dark)").matches;
6798
+ }
6799
+ return false;
6800
+ }
6801
+ var HIGHLIGHT_OPACITY = 0.7;
6802
+ var DIM_OPACITY = 0.15;
6803
+ function createSankey(container, spec, options) {
6804
+ let currentSpec = spec;
6805
+ let currentLayout;
6806
+ let destroyed = false;
6807
+ let svgElement = null;
6808
+ let tooltipManager = null;
6809
+ let cleanupTooltipEvents = null;
6810
+ let disconnectResize = null;
6811
+ let isFirstRender = true;
6812
+ let animationCleanup = null;
6813
+ let pendingResize = false;
6814
+ function getContainerDimensions() {
6815
+ const rect = container.getBoundingClientRect();
6816
+ return {
6817
+ width: Math.max(rect.width || 600, 100),
6818
+ height: Math.max(rect.height || 400, 100)
6819
+ };
6820
+ }
6821
+ function compile() {
6822
+ const { width, height } = getContainerDimensions();
6823
+ const darkMode = resolveDarkMode3(options?.darkMode);
6824
+ const compileOpts = {
6825
+ width,
6826
+ height,
6827
+ theme: options?.theme,
6828
+ darkMode
6829
+ };
6830
+ return compileSankey(currentSpec, compileOpts);
6831
+ }
6832
+ function wireTooltipAndInteraction(svg, layout) {
6833
+ const cleanups = [];
6834
+ const nodeElements = svg.querySelectorAll(".oc-sankey-node");
6835
+ for (const el of nodeElements) {
6836
+ const markId = el.getAttribute("data-mark-id");
6837
+ if (!markId) continue;
6838
+ const content = layout.tooltipDescriptors.get(markId);
6839
+ const nodeId = el.getAttribute("data-node-id");
6840
+ const nodeData = nodeId ? layout.nodes.find((n) => n.nodeId === nodeId)?.data ?? {} : {};
6841
+ const handleMouseEnter = (e) => {
6842
+ const mouseEvent = e;
6843
+ if (content && tooltipManager) {
6844
+ const svgRect = svg.getBoundingClientRect();
6845
+ const x3 = mouseEvent.clientX - svgRect.left;
6846
+ const y3 = mouseEvent.clientY - svgRect.top;
6847
+ tooltipManager.show(content, x3, y3);
6848
+ }
6849
+ options?.onNodeHover?.(nodeData);
6850
+ if (nodeId) highlightConnectedLinks(svg, nodeId, layout);
6851
+ };
6852
+ const handleMouseMove = (e) => {
6853
+ if (content && tooltipManager) {
6854
+ const mouseEvent = e;
6855
+ const svgRect = svg.getBoundingClientRect();
6856
+ const x3 = mouseEvent.clientX - svgRect.left;
6857
+ const y3 = mouseEvent.clientY - svgRect.top;
6858
+ tooltipManager.show(content, x3, y3);
6859
+ }
6860
+ };
6861
+ const handleMouseLeave = () => {
6862
+ tooltipManager?.hide();
6863
+ options?.onNodeHover?.(null);
6864
+ resetLinkOpacity(svg, layout);
6865
+ };
6866
+ const handleClick = () => {
6867
+ options?.onNodeClick?.(nodeData);
6868
+ };
6869
+ el.addEventListener("mouseenter", handleMouseEnter);
6870
+ el.addEventListener("mousemove", handleMouseMove);
6871
+ el.addEventListener("mouseleave", handleMouseLeave);
6872
+ el.addEventListener("click", handleClick);
6873
+ cleanups.push(() => {
6874
+ el.removeEventListener("mouseenter", handleMouseEnter);
6875
+ el.removeEventListener("mousemove", handleMouseMove);
6876
+ el.removeEventListener("mouseleave", handleMouseLeave);
6877
+ el.removeEventListener("click", handleClick);
6878
+ });
6879
+ }
6880
+ const linkElements = svg.querySelectorAll(".oc-sankey-link");
6881
+ for (const el of linkElements) {
6882
+ const markId = el.getAttribute("data-mark-id");
6883
+ if (!markId) continue;
6884
+ const content = layout.tooltipDescriptors.get(markId);
6885
+ const sourceId = el.getAttribute("data-source");
6886
+ const targetId = el.getAttribute("data-target");
6887
+ const linkData = findLinkData(layout, sourceId, targetId);
6888
+ const handleMouseEnter = (e) => {
6889
+ const mouseEvent = e;
6890
+ if (content && tooltipManager) {
6891
+ const svgRect = svg.getBoundingClientRect();
6892
+ const x3 = mouseEvent.clientX - svgRect.left;
6893
+ const y3 = mouseEvent.clientY - svgRect.top;
6894
+ tooltipManager.show(content, x3, y3);
6895
+ }
6896
+ options?.onLinkHover?.(linkData);
6897
+ };
6898
+ const handleMouseMove = (e) => {
6899
+ if (content && tooltipManager) {
6900
+ const mouseEvent = e;
6901
+ const svgRect = svg.getBoundingClientRect();
6902
+ const x3 = mouseEvent.clientX - svgRect.left;
6903
+ const y3 = mouseEvent.clientY - svgRect.top;
6904
+ tooltipManager.show(content, x3, y3);
6905
+ }
6906
+ };
6907
+ const handleMouseLeave = () => {
6908
+ tooltipManager?.hide();
6909
+ options?.onLinkHover?.(null);
6910
+ };
6911
+ const handleClick = () => {
6912
+ options?.onLinkClick?.(linkData);
6913
+ };
6914
+ el.addEventListener("mouseenter", handleMouseEnter);
6915
+ el.addEventListener("mousemove", handleMouseMove);
6916
+ el.addEventListener("mouseleave", handleMouseLeave);
6917
+ el.addEventListener("click", handleClick);
6918
+ cleanups.push(() => {
6919
+ el.removeEventListener("mouseenter", handleMouseEnter);
6920
+ el.removeEventListener("mousemove", handleMouseMove);
6921
+ el.removeEventListener("mouseleave", handleMouseLeave);
6922
+ el.removeEventListener("click", handleClick);
6923
+ });
6924
+ }
6925
+ return () => {
6926
+ for (const cleanup of cleanups) {
6927
+ cleanup();
6928
+ }
6929
+ };
6930
+ }
6931
+ function findLinkData(layout, sourceId, targetId) {
6932
+ if (!sourceId || !targetId) return {};
6933
+ const link = layout.links.find((l) => l.sourceId === sourceId && l.targetId === targetId);
6934
+ return link?.data ?? {};
6935
+ }
6936
+ function highlightConnectedLinks(svg, nodeId, _layout) {
6937
+ const linkElements = svg.querySelectorAll(".oc-sankey-link");
6938
+ for (const el of linkElements) {
6939
+ const source = el.getAttribute("data-source");
6940
+ const target = el.getAttribute("data-target");
6941
+ const path = el.querySelector("path");
6942
+ if (!path) continue;
6943
+ const isConnected = source === nodeId || target === nodeId;
6944
+ path.setAttribute("fill-opacity", String(isConnected ? HIGHLIGHT_OPACITY : DIM_OPACITY));
6945
+ }
6946
+ }
6947
+ function resetLinkOpacity(svg, layout) {
6948
+ const linkElements = svg.querySelectorAll(".oc-sankey-link");
6949
+ for (const el of linkElements) {
6950
+ const path = el.querySelector("path");
6951
+ if (!path) continue;
6952
+ const source = el.getAttribute("data-source");
6953
+ const target = el.getAttribute("data-target");
6954
+ const link = layout.links.find((l) => l.sourceId === source && l.targetId === target);
6955
+ path.setAttribute("fill-opacity", String(link?.fillOpacity ?? 0.35));
6956
+ }
6957
+ }
6958
+ function render() {
6959
+ if (destroyed) return;
6960
+ if (animationCleanup) {
6961
+ animationCleanup();
6962
+ animationCleanup = null;
6963
+ }
6964
+ if (svgElement) {
6965
+ cancelAnimations(svgElement);
6966
+ }
6967
+ if (cleanupTooltipEvents) {
6968
+ cleanupTooltipEvents();
6969
+ cleanupTooltipEvents = null;
6970
+ }
6971
+ if (svgElement?.parentNode) {
6972
+ svgElement.parentNode.removeChild(svgElement);
6973
+ }
6974
+ currentLayout = compile();
6975
+ const shouldAnimate = isFirstRender && currentLayout.animation?.enabled;
6976
+ isFirstRender = false;
6977
+ const animation = shouldAnimate ? currentLayout.animation : void 0;
6978
+ svgElement = renderSankeySVG(currentLayout, animation);
6979
+ container.appendChild(svgElement);
6980
+ const isDark = resolveDarkMode3(options?.darkMode);
6981
+ if (isDark) {
6982
+ container.classList.add("oc-dark");
6983
+ } else {
6984
+ container.classList.remove("oc-dark");
6985
+ }
6986
+ if (options?.tooltip !== false && svgElement) {
6987
+ if (!tooltipManager) {
6988
+ tooltipManager = createTooltipManager(container);
6989
+ }
6990
+ cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
6991
+ }
6992
+ if (shouldAnimate && svgElement) {
6993
+ animationCleanup = setupAnimationCleanup(svgElement, () => {
6994
+ animationCleanup = null;
6995
+ if (pendingResize) {
6996
+ pendingResize = false;
6997
+ resize();
6998
+ }
6999
+ });
7000
+ }
7001
+ }
7002
+ function update(newSpec) {
7003
+ if (destroyed) return;
7004
+ currentSpec = newSpec;
7005
+ isFirstRender = true;
7006
+ render();
7007
+ }
7008
+ function resize() {
7009
+ if (destroyed) return;
7010
+ if (animationCleanup) {
7011
+ pendingResize = true;
7012
+ return;
7013
+ }
7014
+ render();
7015
+ }
7016
+ function doExport(format, exportOptions) {
7017
+ if (!svgElement) {
7018
+ throw new Error("Sankey is not rendered yet");
7019
+ }
7020
+ switch (format) {
7021
+ case "svg":
7022
+ return exportSVG(svgElement);
7023
+ case "svg-with-fonts":
7024
+ return exportSVGWithFonts(svgElement, exportOptions);
7025
+ case "png":
7026
+ return exportPNG(svgElement, exportOptions);
7027
+ case "jpg":
7028
+ return exportJPG(svgElement, exportOptions);
7029
+ default:
7030
+ throw new Error(`Unsupported export format: ${format}`);
7031
+ }
7032
+ }
7033
+ function destroy() {
7034
+ if (destroyed) return;
7035
+ destroyed = true;
7036
+ if (animationCleanup) {
7037
+ animationCleanup();
7038
+ animationCleanup = null;
7039
+ pendingResize = false;
7040
+ }
7041
+ if (svgElement) {
7042
+ cancelAnimations(svgElement);
7043
+ }
7044
+ if (disconnectResize) {
7045
+ disconnectResize();
7046
+ disconnectResize = null;
7047
+ }
7048
+ if (cleanupTooltipEvents) {
7049
+ cleanupTooltipEvents();
7050
+ cleanupTooltipEvents = null;
7051
+ }
7052
+ if (tooltipManager) {
7053
+ tooltipManager.destroy();
7054
+ tooltipManager = null;
7055
+ }
7056
+ if (svgElement?.parentNode) {
7057
+ svgElement.parentNode.removeChild(svgElement);
7058
+ }
7059
+ svgElement = null;
7060
+ container.classList.remove("oc-dark");
7061
+ }
7062
+ try {
7063
+ currentLayout = compile();
7064
+ const shouldAnimate = currentLayout.animation?.enabled;
7065
+ isFirstRender = false;
7066
+ const animation = shouldAnimate ? currentLayout.animation : void 0;
7067
+ svgElement = renderSankeySVG(currentLayout, animation);
7068
+ container.appendChild(svgElement);
7069
+ const isDark = resolveDarkMode3(options?.darkMode);
7070
+ if (isDark) {
7071
+ container.classList.add("oc-dark");
7072
+ } else {
7073
+ container.classList.remove("oc-dark");
7074
+ }
7075
+ if (options?.tooltip !== false && svgElement) {
7076
+ tooltipManager = createTooltipManager(container);
7077
+ cleanupTooltipEvents = wireTooltipAndInteraction(svgElement, currentLayout);
7078
+ }
7079
+ if (shouldAnimate && svgElement) {
7080
+ animationCleanup = setupAnimationCleanup(svgElement, () => {
7081
+ animationCleanup = null;
7082
+ if (pendingResize) {
7083
+ pendingResize = false;
7084
+ resize();
7085
+ }
7086
+ });
7087
+ }
7088
+ } catch (err) {
7089
+ console.error("[viz] Sankey mount failed:", err);
7090
+ throw err;
7091
+ }
7092
+ if (options?.responsive !== false) {
7093
+ disconnectResize = observeResize(container, () => {
7094
+ resize();
7095
+ });
7096
+ }
7097
+ return {
7098
+ update,
7099
+ resize,
7100
+ export: doExport,
7101
+ destroy,
7102
+ get layout() {
7103
+ return currentLayout;
7104
+ }
7105
+ };
7106
+ }
7107
+
6386
7108
  // src/table-keyboard.ts
6387
7109
  function attachKeyboardNav(options) {
6388
7110
  const { wrapper, onSort, onClearSearch, onAnnounce } = options;
@@ -6576,12 +7298,12 @@ import { compileTable } from "@opendata-ai/openchart-engine";
6576
7298
 
6577
7299
  // src/table-renderer.ts
6578
7300
  import { BRAND_FONT_SIZE as BRAND_FONT_SIZE3 } from "@opendata-ai/openchart-core";
6579
- import { clampStaggerDelay as clampStaggerDelay2 } from "@opendata-ai/openchart-engine";
6580
- var EASE_VAR_MAP2 = {
7301
+ import { clampStaggerDelay as clampStaggerDelay3 } from "@opendata-ai/openchart-engine";
7302
+ var EASE_VAR_MAP3 = {
6581
7303
  smooth: "var(--oc-ease-smooth)",
6582
7304
  snappy: "var(--oc-ease-snappy)"
6583
7305
  };
6584
- var BRAND_URL2 = "https://tryopendata.ai";
7306
+ var BRAND_URL3 = "https://tryopendata.ai";
6585
7307
  function renderChromeBlock(layout, position) {
6586
7308
  const chrome = layout.chrome;
6587
7309
  if (position === "header") {
@@ -6826,7 +7548,7 @@ function renderTable(layout, container, opts) {
6826
7548
  brand.className = "oc-table-ref";
6827
7549
  brand.style.cssText = "text-align: right; padding: 4px 8px;";
6828
7550
  const brandLink = document.createElement("a");
6829
- brandLink.href = BRAND_URL2;
7551
+ brandLink.href = BRAND_URL3;
6830
7552
  brandLink.target = "_blank";
6831
7553
  brandLink.rel = "noopener";
6832
7554
  brandLink.style.cssText = `font-size: ${BRAND_FONT_SIZE3}px; font-weight: 600; color: ${brandColor}; opacity: 0.55; text-decoration: none; font-family: ${theme ? theme.fonts.family : "sans-serif"};`;
@@ -6836,11 +7558,11 @@ function renderTable(layout, container, opts) {
6836
7558
  if (opts?.animate && layout.animation?.enabled) {
6837
7559
  const anim = layout.animation;
6838
7560
  const rowCount = layout.rows.length;
6839
- const stagger = clampStaggerDelay2(anim.staggerDelay, rowCount);
7561
+ const stagger = clampStaggerDelay3(anim.staggerDelay, rowCount);
6840
7562
  const s = wrapper.style;
6841
7563
  s.setProperty("--oc-animation-duration", `${anim.duration}ms`);
6842
7564
  s.setProperty("--oc-animation-stagger", `${stagger}ms`);
6843
- s.setProperty("--oc-animation-ease", EASE_VAR_MAP2[anim.ease] || EASE_VAR_MAP2.smooth);
7565
+ s.setProperty("--oc-animation-ease", EASE_VAR_MAP3[anim.ease] || EASE_VAR_MAP3.smooth);
6844
7566
  wrapper.classList.add("oc-animate");
6845
7567
  }
6846
7568
  container.appendChild(wrapper);
@@ -6848,7 +7570,7 @@ function renderTable(layout, container, opts) {
6848
7570
  }
6849
7571
 
6850
7572
  // src/table-mount.ts
6851
- function resolveDarkMode3(mode) {
7573
+ function resolveDarkMode4(mode) {
6852
7574
  if (mode === "force") return true;
6853
7575
  if (mode === "off" || mode === void 0) return false;
6854
7576
  if (typeof window !== "undefined" && window.matchMedia) {
@@ -6916,7 +7638,7 @@ function createTable(container, spec, options) {
6916
7638
  }
6917
7639
  function compile() {
6918
7640
  const state = getState();
6919
- const darkMode = resolveDarkMode3(options?.darkMode);
7641
+ const darkMode = resolveDarkMode4(options?.darkMode);
6920
7642
  const { width } = getContainerDimensions();
6921
7643
  const compileOpts = {
6922
7644
  width,
@@ -6977,7 +7699,7 @@ function createTable(container, spec, options) {
6977
7699
  if (isFirstRender) {
6978
7700
  isFirstRender = false;
6979
7701
  }
6980
- const isDark = resolveDarkMode3(options?.darkMode);
7702
+ const isDark = resolveDarkMode4(options?.darkMode);
6981
7703
  if (isDark) {
6982
7704
  container.classList.add("oc-dark");
6983
7705
  } else {
@@ -7132,7 +7854,7 @@ function createTable(container, spec, options) {
7132
7854
  throw new Error(`Unsupported export format: ${format}`);
7133
7855
  }
7134
7856
  const state = getState();
7135
- const darkMode = resolveDarkMode3(options?.darkMode);
7857
+ const darkMode = resolveDarkMode4(options?.darkMode);
7136
7858
  const { width } = getContainerDimensions();
7137
7859
  const fullLayout = compileTable(currentSpec, {
7138
7860
  width,
@@ -7213,6 +7935,7 @@ export {
7213
7935
  attachKeyboardNav,
7214
7936
  createChart,
7215
7937
  createGraph,
7938
+ createSankey,
7216
7939
  createSimulationWorker,
7217
7940
  createTable,
7218
7941
  createTextEditOverlay,