footprint-explainable-ui 0.24.0 → 0.25.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/flowchart.js CHANGED
@@ -490,7 +490,7 @@ var StageNode = memo(function StageNode2({
490
490
  background: bg,
491
491
  border: `${isHero ? "2.5px" : isMuted ? "1px" : "2px"} ${isLazyUnresolved ? "dashed" : "solid"} ${borderColor}`,
492
492
  borderRadius: theme.radius,
493
- padding: description ? `${Math.round(8 * sizeScale)}px ${Math.round(16 * sizeScale)}px` : `${Math.round(10 * sizeScale)}px ${Math.round(20 * sizeScale)}px`,
493
+ padding: description ? `${Math.round(6 * sizeScale)}px ${Math.round(12 * sizeScale)}px` : `${Math.round(7 * sizeScale)}px ${Math.round(14 * sizeScale)}px`,
494
494
  display: "flex",
495
495
  flexDirection: "column",
496
496
  alignItems: "center",
@@ -1464,6 +1464,14 @@ function staggeredBendY(sourceBottom, targetTop, others, minGapFromTarget = 8) {
1464
1464
  if (lowestSkippedBottom === -Infinity) return null;
1465
1465
  return Math.min((lowestSkippedBottom + targetTop) / 2, targetTop - minGapFromTarget);
1466
1466
  }
1467
+ function forkFanBendY(sourceBottom, childTops, minGapFromTarget = 8) {
1468
+ if (childTops.length < 2) return null;
1469
+ const nearestTop = Math.min(...childTops);
1470
+ return Math.min((sourceBottom + nearestTop) / 2, nearestTop - minGapFromTarget);
1471
+ }
1472
+ function resolveStepBendY(forkBend, staggeredBend) {
1473
+ return staggeredBend ?? forkBend;
1474
+ }
1467
1475
 
1468
1476
  // src/components/SmartStepEdge/SmartStepEdge.tsx
1469
1477
  import { jsx as jsx7 } from "react/jsx-runtime";
@@ -1486,6 +1494,16 @@ function SmartStepEdge({
1486
1494
  if (!src || !tgt) return null;
1487
1495
  const sourceBottom = src.internals.positionAbsolute.y + (src.measured.height ?? 0);
1488
1496
  const targetTop = tgt.internals.positionAbsolute.y;
1497
+ const childTops = [];
1498
+ for (const e of s.edges) {
1499
+ if (e.source !== source) continue;
1500
+ if (e.data?.kind === "loop") continue;
1501
+ const c = s.nodeLookup.get(e.target);
1502
+ if (c && c.type !== GROUP_CONTAINER_NODE_TYPE) {
1503
+ childTops.push(c.internals.positionAbsolute.y);
1504
+ }
1505
+ }
1506
+ const fan = forkFanBendY(sourceBottom, childTops);
1489
1507
  const others = [];
1490
1508
  for (const n of s.nodeLookup.values()) {
1491
1509
  if (n.id === source || n.id === target) continue;
@@ -1493,7 +1511,8 @@ function SmartStepEdge({
1493
1511
  const top = n.internals.positionAbsolute.y;
1494
1512
  others.push({ top, bottom: top + (n.measured.height ?? 0) });
1495
1513
  }
1496
- return staggeredBendY(sourceBottom, targetTop, others);
1514
+ const staggered = staggeredBendY(sourceBottom, targetTop, others);
1515
+ return resolveStepBendY(fan, staggered);
1497
1516
  });
1498
1517
  const [path] = getSmoothStepPath({
1499
1518
  sourceX,
@@ -1864,6 +1883,146 @@ import {
1864
1883
  MarkerType as MarkerType2
1865
1884
  } from "@xyflow/react";
1866
1885
 
1886
+ // src/components/FlowchartView/_internal/snapLinearSuccessors.ts
1887
+ function snapLinearSuccessors(graph, options = {}) {
1888
+ if (graph.nodes.length === 0) return graph;
1889
+ const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
1890
+ const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
1891
+ const byId = /* @__PURE__ */ new Map();
1892
+ const width = /* @__PURE__ */ new Map();
1893
+ for (const n of graph.nodes) {
1894
+ byId.set(n.id, n);
1895
+ width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
1896
+ }
1897
+ const preds = /* @__PURE__ */ new Map();
1898
+ const outDegree = /* @__PURE__ */ new Map();
1899
+ const seenEdge = /* @__PURE__ */ new Set();
1900
+ for (const e of graph.edges) {
1901
+ if (e.data?.kind === "loop") continue;
1902
+ if (!byId.has(e.source) || !byId.has(e.target)) continue;
1903
+ const key = `${e.source}\0${e.target}`;
1904
+ if (seenEdge.has(key)) continue;
1905
+ seenEdge.add(key);
1906
+ const list = preds.get(e.target);
1907
+ if (list) list.push(e.source);
1908
+ else preds.set(e.target, [e.source]);
1909
+ outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
1910
+ }
1911
+ const workingX = /* @__PURE__ */ new Map();
1912
+ for (const n of graph.nodes) workingX.set(n.id, n.position.x);
1913
+ const centerX = (id) => workingX.get(id) + width.get(id) / 2;
1914
+ const order = [...graph.nodes].sort(
1915
+ (a, b) => a.position.y - b.position.y || a.position.x - b.position.x || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
1916
+ );
1917
+ for (const n of order) {
1918
+ const p = preds.get(n.id);
1919
+ if (!p || p.length !== 1) continue;
1920
+ const pid = p[0];
1921
+ if ((outDegree.get(pid) ?? 0) !== 1) continue;
1922
+ const P = byId.get(pid);
1923
+ if ((n.parentId ?? void 0) !== (P.parentId ?? void 0)) continue;
1924
+ workingX.set(n.id, centerX(pid) - width.get(n.id) / 2);
1925
+ }
1926
+ const nodes = graph.nodes.map((n) => {
1927
+ const nx = workingX.get(n.id);
1928
+ return nx === n.position.x ? n : { ...n, position: { x: nx, y: n.position.y } };
1929
+ });
1930
+ return { nodes, edges: graph.edges };
1931
+ }
1932
+ function createSnappedDagreLayout(base, options = {}) {
1933
+ return (graph) => snapLinearSuccessors(base(graph), options);
1934
+ }
1935
+
1936
+ // src/components/FlowchartView/_internal/centerForkParents.ts
1937
+ function centerForkParents(graph, options = {}) {
1938
+ if (graph.nodes.length === 0) return graph;
1939
+ const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
1940
+ const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
1941
+ const byId = /* @__PURE__ */ new Map();
1942
+ const width = /* @__PURE__ */ new Map();
1943
+ for (const n of graph.nodes) {
1944
+ byId.set(n.id, n);
1945
+ width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
1946
+ }
1947
+ const childrenOf = /* @__PURE__ */ new Map();
1948
+ const predsOf = /* @__PURE__ */ new Map();
1949
+ const outDegree = /* @__PURE__ */ new Map();
1950
+ const inDegree = /* @__PURE__ */ new Map();
1951
+ const seen = /* @__PURE__ */ new Set();
1952
+ for (const e of graph.edges) {
1953
+ if (e.data?.kind === "loop") continue;
1954
+ if (!byId.has(e.source) || !byId.has(e.target)) continue;
1955
+ const key = `${e.source} ${e.target}`;
1956
+ if (seen.has(key)) continue;
1957
+ seen.add(key);
1958
+ const cl = childrenOf.get(e.source);
1959
+ if (cl) cl.push(e.target);
1960
+ else childrenOf.set(e.source, [e.target]);
1961
+ const pl = predsOf.get(e.target);
1962
+ if (pl) pl.push(e.source);
1963
+ else predsOf.set(e.target, [e.source]);
1964
+ outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
1965
+ inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
1966
+ }
1967
+ const workingX = /* @__PURE__ */ new Map();
1968
+ for (const n of graph.nodes) workingX.set(n.id, n.position.x);
1969
+ const centerX = (id) => workingX.get(id) + width.get(id) / 2;
1970
+ const nodeSep = options.nodeSep ?? 60;
1971
+ const clampX = (id, desiredX) => {
1972
+ const w = width.get(id);
1973
+ const x0 = workingX.get(id);
1974
+ const self = byId.get(id);
1975
+ let minX = -Infinity;
1976
+ let maxX = Infinity;
1977
+ for (const m of graph.nodes) {
1978
+ if (m.id === id || m.parentId !== self.parentId) continue;
1979
+ if (Math.abs(m.position.y - self.position.y) > 1) continue;
1980
+ const mLeft = workingX.get(m.id);
1981
+ const mRight = mLeft + width.get(m.id);
1982
+ if (mRight <= x0) minX = Math.max(minX, mRight + nodeSep);
1983
+ else if (mLeft >= x0 + w) maxX = Math.min(maxX, mLeft - nodeSep - w);
1984
+ }
1985
+ return minX <= maxX ? Math.max(minX, Math.min(maxX, desiredX)) : x0;
1986
+ };
1987
+ const order = [...graph.nodes].sort(
1988
+ (a, b) => b.position.y - a.position.y || a.position.x - b.position.x || a.id.localeCompare(b.id)
1989
+ );
1990
+ for (const n of order) {
1991
+ if ((outDegree.get(n.id) ?? 0) < 2) continue;
1992
+ if ((inDegree.get(n.id) ?? 0) > 1) continue;
1993
+ const kids = (childrenOf.get(n.id) ?? []).filter(
1994
+ (k) => byId.get(k)?.parentId === n.parentId
1995
+ // same compound only
1996
+ );
1997
+ if (kids.length < 2) continue;
1998
+ const centers = kids.map(centerX);
1999
+ const wN = width.get(n.id);
2000
+ const span = (Math.min(...centers) + Math.max(...centers)) / 2;
2001
+ workingX.set(n.id, clampX(n.id, span - wN / 2));
2002
+ let curId = n.id;
2003
+ const walked = /* @__PURE__ */ new Set([curId]);
2004
+ for (; ; ) {
2005
+ const ps = predsOf.get(curId);
2006
+ if (!ps || ps.length !== 1) break;
2007
+ const p = ps[0];
2008
+ if (walked.has(p)) break;
2009
+ if ((outDegree.get(p) ?? 0) !== 1) break;
2010
+ if ((inDegree.get(p) ?? 0) > 1) break;
2011
+ if (byId.get(p)?.parentId !== byId.get(curId)?.parentId) break;
2012
+ workingX.set(p, clampX(p, centerX(curId) - width.get(p) / 2));
2013
+ walked.add(p);
2014
+ curId = p;
2015
+ }
2016
+ }
2017
+ const nodes = graph.nodes.map(
2018
+ (n) => workingX.get(n.id) === n.position.x ? n : { ...n, position: { x: workingX.get(n.id), y: n.position.y } }
2019
+ );
2020
+ return { nodes, edges: graph.edges };
2021
+ }
2022
+ function withForkCentering(base, options = {}) {
2023
+ return (graph) => centerForkParents(base(graph), options);
2024
+ }
2025
+
1867
2026
  // src/components/FlowchartView/_internal/devWarn.ts
1868
2027
  function isDevModeEnv() {
1869
2028
  const proc = globalThis.process;
@@ -2231,6 +2390,49 @@ function GroupContainerNode({ data }) {
2231
2390
  );
2232
2391
  }
2233
2392
 
2393
+ // src/components/FlowchartView/_internal/MeasuredNodeSizes.tsx
2394
+ import { useEffect as useEffect6 } from "react";
2395
+ import { useNodesInitialized, useStore as useStore3 } from "@xyflow/react";
2396
+
2397
+ // src/components/FlowchartView/_internal/measuredFootprints.ts
2398
+ function extractMeasuredFootprints(entries) {
2399
+ const sizes = /* @__PURE__ */ new Map();
2400
+ for (const [id, node] of entries) {
2401
+ const width = node.measured?.width;
2402
+ const height = node.measured?.height;
2403
+ if (typeof width === "number" && typeof height === "number" && width > 0 && height > 0) {
2404
+ sizes.set(id, { width: Math.round(width), height: Math.round(height) });
2405
+ }
2406
+ }
2407
+ return sizes;
2408
+ }
2409
+ function sameFootprints(a, b) {
2410
+ if (a === b) return true;
2411
+ if (a.size !== b.size) return false;
2412
+ for (const [id, s] of a) {
2413
+ const t = b.get(id);
2414
+ if (!t || t.width !== s.width || t.height !== s.height) return false;
2415
+ }
2416
+ return true;
2417
+ }
2418
+
2419
+ // src/components/FlowchartView/_internal/MeasuredNodeSizes.tsx
2420
+ function MeasuredNodeSizes({
2421
+ onSizes,
2422
+ includeHiddenNodes = false
2423
+ }) {
2424
+ const initialized = useNodesInitialized({ includeHiddenNodes });
2425
+ const sizes = useStore3(
2426
+ (s) => extractMeasuredFootprints(s.nodeLookup),
2427
+ sameFootprints
2428
+ );
2429
+ useEffect6(() => {
2430
+ if (!initialized || sizes.size === 0) return;
2431
+ onSizes(sizes);
2432
+ }, [initialized, sizes, onSizes]);
2433
+ return null;
2434
+ }
2435
+
2234
2436
  // src/components/FlowchartView/TracedFlow.tsx
2235
2437
  import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
2236
2438
  var DEFAULT_COLORS = {
@@ -2403,8 +2605,19 @@ function TracedFlow({
2403
2605
  () => buildSubflowBreadcrumb(graph, drill.currentSubflowId),
2404
2606
  [graph, drill.currentSubflowId]
2405
2607
  );
2608
+ const [measuredSizes, setMeasuredSizes] = useState4(null);
2406
2609
  const positioned = useMemo5(() => {
2407
- const realBase = layout === "passthrough" ? (g) => g : layout;
2610
+ const nodeSize = measuredSizes ? (n) => measuredSizes.get(n.id) : void 0;
2611
+ const sizeOpts = nodeSize ? { nodeSize } : {};
2612
+ const dagreBase = withForkCentering(
2613
+ createSnappedDagreLayout(
2614
+ createDagreTraceLayout({ ...sizeOpts, rankSep: 52, nodeSep: 36 }),
2615
+ sizeOpts
2616
+ ),
2617
+ { ...sizeOpts, nodeSep: 36 }
2618
+ // same nodeSep → clamp preserves dagre's reserved gap
2619
+ );
2620
+ const realBase = layout === "passthrough" ? (g) => g : layoutProp === void 0 ? dagreBase : layout;
2408
2621
  if (groupedSet.size > 0) {
2409
2622
  const grouped = applyGroupLayout(filteredGraph, {
2410
2623
  groupedSubflowIds: [...groupedSet],
@@ -2415,8 +2628,8 @@ function TracedFlow({
2415
2628
  if (mainChartBox) {
2416
2629
  return wrapInMainChartBox(filteredGraph, { baseLayout: realBase, ...mainChartBox });
2417
2630
  }
2418
- return layout === "passthrough" ? filteredGraph : layout(filteredGraph);
2419
- }, [filteredGraph, layout, groupedSet, mainChartBox]);
2631
+ return realBase(filteredGraph);
2632
+ }, [filteredGraph, layout, layoutProp, groupedSet, mainChartBox, measuredSizes]);
2420
2633
  const slice = useMemo5(() => {
2421
2634
  const empty = {
2422
2635
  doneStageIds: /* @__PURE__ */ new Set(),
@@ -2460,7 +2673,11 @@ function TracedFlow({
2460
2673
  );
2461
2674
  const wrapperRef = useRef5(null);
2462
2675
  const [rfInstance, setRfInstance] = useState4(null);
2463
- useChartAutoRefit(wrapperRef, rfInstance, { refitKey: drill.currentSubflowId });
2676
+ useChartAutoRefit(wrapperRef, rfInstance, {
2677
+ // Re-fit on drill AND after the measured-size re-layout settles.
2678
+ refitKey: `${drill.currentSubflowId ?? ""}:${measuredSizes ? "measured" : "estimated"}`,
2679
+ padding: 0.18
2680
+ });
2464
2681
  return /* @__PURE__ */ jsxs8(
2465
2682
  "div",
2466
2683
  {
@@ -2492,8 +2709,11 @@ function TracedFlow({
2492
2709
  onNodeClick: handleNodeClick,
2493
2710
  onInit: setRfInstance,
2494
2711
  fitView: true,
2712
+ fitViewOptions: { padding: 0.18 },
2713
+ minZoom: 0.1,
2495
2714
  proOptions: { hideAttribution: true },
2496
2715
  children: [
2716
+ /* @__PURE__ */ jsx11(MeasuredNodeSizes, { onSizes: setMeasuredSizes }),
2497
2717
  /* @__PURE__ */ jsx11(Background2, { variant: BackgroundVariant2.Dots, gap: 20, size: 1 }),
2498
2718
  children
2499
2719
  ]
@@ -3508,56 +3728,6 @@ function SlotPillNode({ data }) {
3508
3728
  );
3509
3729
  }
3510
3730
 
3511
- // src/components/FlowchartView/_internal/snapLinearSuccessors.ts
3512
- function snapLinearSuccessors(graph, options = {}) {
3513
- if (graph.nodes.length === 0) return graph;
3514
- const fallbackW = options.nodeWidth ?? DEFAULT_NODE_W2;
3515
- const fallbackH = options.nodeHeight ?? DEFAULT_NODE_H2;
3516
- const byId = /* @__PURE__ */ new Map();
3517
- const width = /* @__PURE__ */ new Map();
3518
- for (const n of graph.nodes) {
3519
- byId.set(n.id, n);
3520
- width.set(n.id, sizeOf(n, fallbackW, fallbackH, options.nodeSize).width);
3521
- }
3522
- const preds = /* @__PURE__ */ new Map();
3523
- const outDegree = /* @__PURE__ */ new Map();
3524
- const seenEdge = /* @__PURE__ */ new Set();
3525
- for (const e of graph.edges) {
3526
- if (e.data?.kind === "loop") continue;
3527
- if (!byId.has(e.source) || !byId.has(e.target)) continue;
3528
- const key = `${e.source}\0${e.target}`;
3529
- if (seenEdge.has(key)) continue;
3530
- seenEdge.add(key);
3531
- const list = preds.get(e.target);
3532
- if (list) list.push(e.source);
3533
- else preds.set(e.target, [e.source]);
3534
- outDegree.set(e.source, (outDegree.get(e.source) ?? 0) + 1);
3535
- }
3536
- const workingX = /* @__PURE__ */ new Map();
3537
- for (const n of graph.nodes) workingX.set(n.id, n.position.x);
3538
- const centerX = (id) => workingX.get(id) + width.get(id) / 2;
3539
- const order = [...graph.nodes].sort(
3540
- (a, b) => a.position.y - b.position.y || a.position.x - b.position.x || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
3541
- );
3542
- for (const n of order) {
3543
- const p = preds.get(n.id);
3544
- if (!p || p.length !== 1) continue;
3545
- const pid = p[0];
3546
- if ((outDegree.get(pid) ?? 0) !== 1) continue;
3547
- const P = byId.get(pid);
3548
- if ((n.parentId ?? void 0) !== (P.parentId ?? void 0)) continue;
3549
- workingX.set(n.id, centerX(pid) - width.get(n.id) / 2);
3550
- }
3551
- const nodes = graph.nodes.map((n) => {
3552
- const nx = workingX.get(n.id);
3553
- return nx === n.position.x ? n : { ...n, position: { x: nx, y: n.position.y } };
3554
- });
3555
- return { nodes, edges: graph.edges };
3556
- }
3557
- function createSnappedDagreLayout(base, options = {}) {
3558
- return (graph) => snapLinearSuccessors(base(graph), options);
3559
- }
3560
-
3561
3731
  // src/components/FlowchartView/_internal/traceGroupLayout.ts
3562
3732
  function buildAdjacency(graph, fallbackW, fallbackH, nodeSize) {
3563
3733
  const preds = /* @__PURE__ */ new Map();