@ktrysmt/beautiful-mermaid 1.4.2 → 1.4.4

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # beautiful-mermaid
3
+ # @ktrysmt/beautiful-mermaid
4
4
 
5
5
  **Render Mermaid diagrams as beautiful SVGs or ASCII art**
6
6
 
@@ -8,32 +8,23 @@ Ultra-fast, fully themeable, zero DOM dependencies. Built for the AI era.
8
8
 
9
9
  ![beautiful-mermaid sequence diagram example](hero.png)
10
10
 
11
- [![npm version](https://img.shields.io/npm/v/beautiful-mermaid.svg)](https://www.npmjs.com/package/beautiful-mermaid)
11
+ [![npm version](https://img.shields.io/npm/v/@ktrysmt/beautiful-mermaid.svg)](https://www.npmjs.com/package/@ktrysmt/beautiful-mermaid)
12
12
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
13
13
 
14
- [**Live Demo & Samples**](https://agents.craft.do/mermaid)
15
-
16
- **[→ Use it live in Craft Agents](https://agents.craft.do)**
17
-
18
14
  </div>
19
15
 
20
16
  ---
21
17
 
22
- ## Why We Built This
23
-
24
- Diagrams are essential for AI-assisted programming. When you're working with an AI coding assistant, being able to visualize data flows, state machines, and system architecture—directly in your terminal or chat interface—makes complex concepts instantly graspable.
25
-
26
- [Mermaid](https://mermaid.js.org/) is the de facto standard for text-based diagrams. It's brilliant. But the default renderer has problems:
27
-
28
- - **Aesthetics** — Might be personal preference, but wished they looked more professional
29
- - **Complex theming** — Customizing colors requires wrestling with CSS classes
30
- - **No terminal output** — Can't render to ASCII for CLI tools
31
- - **Heavy dependencies** — Pulls in a lot of code for simple diagrams
18
+ ## About This Fork
32
19
 
33
- We built `beautiful-mermaid` at [Craft](https://craft.do) to power diagrams in [Craft Agents](https://agents.craft.do). It's fast, beautiful, and works everywhere—from rich UIs to plain terminals.
20
+ This is a fork of [beautiful-mermaid](https://github.com/lukilabs/beautiful-mermaid), originally built by the team at [Craft](https://craft.do). This fork is independently maintained by [@ktrysmt](https://github.com/ktrysmt) with additional bug fixes and improvements including:
34
21
 
22
+ - CJK/Unicode label support for subgraphs
23
+ - Subgraph layout fixes (vertical stacking, containment, label width)
24
+ - Edge routing improvements
25
+ - Scoped npm package (`@ktrysmt/beautiful-mermaid`)
35
26
 
36
- The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff. We ported it from Go to TypeScript and extended it. Thank you Alexander for the excellent foundation! (And inspiration that this was possible.)
27
+ The ASCII rendering engine is based on [mermaid-ascii](https://github.com/AlexanderGrooff/mermaid-ascii) by Alexander Grooff, ported from Go to TypeScript. Thank you Alexander for the excellent foundation!
37
28
 
38
29
  ## Features
39
30
 
@@ -50,11 +41,11 @@ The ASCII rendering engine is based on [mermaid-ascii](https://github.com/Alexan
50
41
  ## Installation
51
42
 
52
43
  ```bash
53
- npm install beautiful-mermaid
44
+ npm install @ktrysmt/beautiful-mermaid
54
45
  # or
55
- bun add beautiful-mermaid
46
+ bun add @ktrysmt/beautiful-mermaid
56
47
  # or
57
- pnpm add beautiful-mermaid
48
+ pnpm add @ktrysmt/beautiful-mermaid
58
49
  ```
59
50
 
60
51
  ## Quick Start
@@ -62,7 +53,7 @@ pnpm add beautiful-mermaid
62
53
  ### SVG Output
63
54
 
64
55
  ```typescript
65
- import { renderMermaidSVG } from 'beautiful-mermaid'
56
+ import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
66
57
 
67
58
  const svg = renderMermaidSVG(`
68
59
  graph TD
@@ -79,7 +70,7 @@ Need async? Use `renderMermaidSVGAsync()` — same output, returns a `Promise<st
79
70
  ### ASCII Output
80
71
 
81
72
  ```typescript
82
- import { renderMermaidASCII } from 'beautiful-mermaid'
73
+ import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
83
74
 
84
75
  const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
85
76
  ```
@@ -99,7 +90,7 @@ const ascii = renderMermaidASCII(`graph LR; A --> B --> C`)
99
90
  Because rendering is synchronous, you can use `useMemo()` for zero-flash diagram rendering:
100
91
 
101
92
  ```tsx
102
- import { renderMermaidSVG } from 'beautiful-mermaid'
93
+ import { renderMermaidSVG } from '@ktrysmt/beautiful-mermaid'
103
94
 
104
95
  function MermaidDiagram({ code }: { code: string }) {
105
96
  const { svg, error } = React.useMemo(() => {
@@ -224,7 +215,7 @@ const svg = renderMermaidSVG(diagram, {
224
215
  | `one-dark` | Dark | `#282c34` | `#c678dd` |
225
216
 
226
217
  ```typescript
227
- import { renderMermaidSVG, THEMES } from 'beautiful-mermaid'
218
+ import { renderMermaidSVG, THEMES } from '@ktrysmt/beautiful-mermaid'
228
219
 
229
220
  const svg = renderMermaidSVG(diagram, THEMES['tokyo-night'])
230
221
  ```
@@ -259,7 +250,7 @@ Use **any VS Code theme** directly via Shiki integration. This gives you access
259
250
 
260
251
  ```typescript
261
252
  import { getSingletonHighlighter } from 'shiki'
262
- import { renderMermaidSVG, fromShikiTheme } from 'beautiful-mermaid'
253
+ import { renderMermaidSVG, fromShikiTheme } from '@ktrysmt/beautiful-mermaid'
263
254
 
264
255
  // Load any theme from Shiki's registry
265
256
  const highlighter = await getSingletonHighlighter({
@@ -437,7 +428,7 @@ The chart renderer follows a clean, minimal design philosophy inspired by Apple
437
428
  For terminal environments, CLI tools, or anywhere you need plain text, render to ASCII or Unicode box-drawing characters:
438
429
 
439
430
  ```typescript
440
- import { renderMermaidASCII } from 'beautiful-mermaid'
431
+ import { renderMermaidASCII } from '@ktrysmt/beautiful-mermaid'
441
432
 
442
433
  // Unicode mode (default) — prettier box drawing
443
434
  const unicode = renderMermaidASCII(`graph LR; A --> B`)
@@ -580,6 +571,6 @@ MIT — see [LICENSE](LICENSE) for details.
580
571
 
581
572
  <div align="center">
582
573
 
583
- Built with care by the team at [Craft](https://craft.do)
574
+ Originally built by the team at [Craft](https://craft.do). Fork maintained by [@ktrysmt](https://github.com/ktrysmt).
584
575
 
585
576
  </div>
package/dist/index.js CHANGED
@@ -263,8 +263,11 @@ function measureMultilineText(text, fontSize, fontWeight) {
263
263
 
264
264
  // src/multiline-utils.ts
265
265
  function normalizeBrTags(label) {
266
- const unquoted = label.startsWith('"') && label.endsWith('"') ? label.slice(1, -1) : label;
267
- return unquoted.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>");
266
+ let result = label.replace(/<br\s*\/?>/gi, "\n").replace(/\\n/g, "\n").trim();
267
+ if (result.startsWith('"') && result.endsWith('"')) {
268
+ result = result.slice(1, -1);
269
+ }
270
+ return result.replace(/<\/?(?:sub|sup|small|mark)\s*>/gi, "").replace(/\*\*(.+?)\*\*/g, "<b>$1</b>").replace(/(?<!\*)\*([^\s*](?:[^*]*[^\s*])?)\*(?!\*)/g, "<i>$1</i>").replace(/~~(.+?)~~/g, "<s>$1</s>").trim();
268
271
  }
269
272
  function escapeXml(text) {
270
273
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -336,8 +339,43 @@ ${textEl}`;
336
339
  }
337
340
 
338
341
  // src/parser.ts
342
+ function joinMultilineDefinitions(lines) {
343
+ const result = [];
344
+ let buffer = "";
345
+ let bracketDepth = 0;
346
+ let parenDepth = 0;
347
+ let inQuote = false;
348
+ for (const line of lines) {
349
+ if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
350
+ buffer = line;
351
+ } else {
352
+ if (/<br\s*\/?>\s*$/i.test(buffer)) {
353
+ buffer += line;
354
+ } else {
355
+ buffer += "<br>" + line;
356
+ }
357
+ }
358
+ for (const ch of line) {
359
+ if (ch === '"') {
360
+ inQuote = !inQuote;
361
+ } else if (!inQuote) {
362
+ if (ch === "[") bracketDepth++;
363
+ else if (ch === "]") bracketDepth = Math.max(0, bracketDepth - 1);
364
+ else if (ch === "(") parenDepth++;
365
+ else if (ch === ")") parenDepth = Math.max(0, parenDepth - 1);
366
+ }
367
+ }
368
+ if (bracketDepth === 0 && parenDepth === 0 && !inQuote) {
369
+ result.push(buffer);
370
+ buffer = "";
371
+ }
372
+ }
373
+ if (buffer) result.push(buffer);
374
+ return result;
375
+ }
339
376
  function parseMermaid(text) {
340
- const lines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
377
+ const rawLines = text.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("%%"));
378
+ const lines = joinMultilineDefinitions(rawLines);
341
379
  if (lines.length === 0) {
342
380
  throw new Error("Empty mermaid diagram");
343
381
  }
@@ -1691,7 +1729,7 @@ function isFreeInGrid(grid, c) {
1691
1729
  return !grid.has(gridKey(c));
1692
1730
  }
1693
1731
  var MAX_ITERATIONS = 5e4;
1694
- function getPath(grid, from, to) {
1732
+ function getPath(grid, from, to, congestion) {
1695
1733
  const pq = new MinHeap();
1696
1734
  pq.push({ coord: from, priority: 0 });
1697
1735
  const costSoFar = /* @__PURE__ */ new Map();
@@ -1719,7 +1757,8 @@ function getPath(grid, from, to) {
1719
1757
  if (!isFreeInGrid(grid, next) && !gridCoordEquals(next, to)) {
1720
1758
  continue;
1721
1759
  }
1722
- const newCost = currentCost + 1;
1760
+ const congestionPenalty = congestion?.get(gridKey(next)) ?? 0;
1761
+ const newCost = currentCost + 1 + congestionPenalty * 3;
1723
1762
  const nextKey = gridKey(next);
1724
1763
  const existingCost = costSoFar.get(nextKey);
1725
1764
  if (existingCost === void 0 || newCost < existingCost) {
@@ -1863,17 +1902,17 @@ function determineStartAndEndDir(edge, graphDirection) {
1863
1902
  }
1864
1903
  return [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir];
1865
1904
  }
1866
- function determinePath(graph, edge) {
1905
+ function determinePath(graph, edge, congestion) {
1867
1906
  const sourceSg = getNodeSubgraph(graph, edge.from);
1868
1907
  const targetSg = getNodeSubgraph(graph, edge.to);
1869
1908
  const effectiveDir = sourceSg && sourceSg === targetSg && sourceSg.direction ? sourceSg.direction : graph.config.graphDirection;
1870
1909
  const [preferredDir, preferredOppositeDir, alternativeDir, alternativeOppositeDir] = determineStartAndEndDir(edge, effectiveDir);
1871
1910
  const prefFrom = gridCoordDirection(edge.from.gridCoord, preferredDir);
1872
1911
  const prefTo = gridCoordDirection(edge.to.gridCoord, preferredOppositeDir);
1873
- let preferredPath = getPath(graph.grid, prefFrom, prefTo);
1912
+ let preferredPath = getPath(graph.grid, prefFrom, prefTo, congestion);
1874
1913
  const altFrom = gridCoordDirection(edge.from.gridCoord, alternativeDir);
1875
1914
  const altTo = gridCoordDirection(edge.to.gridCoord, alternativeOppositeDir);
1876
- let alternativePath = getPath(graph.grid, altFrom, altTo);
1915
+ let alternativePath = getPath(graph.grid, altFrom, altTo, congestion);
1877
1916
  if (preferredPath !== null && alternativePath !== null) {
1878
1917
  preferredPath = mergePath(preferredPath);
1879
1918
  alternativePath = mergePath(alternativePath);
@@ -1904,11 +1943,41 @@ function determinePath(graph, edge) {
1904
1943
  edge.endDir = preferredOppositeDir;
1905
1944
  edge.path = [prefFrom, prefTo];
1906
1945
  }
1946
+ function pathToCells(path) {
1947
+ const cells = [];
1948
+ for (let i = 0; i < path.length - 1; i++) {
1949
+ const p1 = path[i];
1950
+ const p2 = path[i + 1];
1951
+ if (p1.x === p2.x) {
1952
+ const minY = Math.min(p1.y, p2.y);
1953
+ const maxY = Math.max(p1.y, p2.y);
1954
+ for (let y = minY; y <= maxY; y++) {
1955
+ cells.push({ x: p1.x, y });
1956
+ }
1957
+ } else {
1958
+ const minX = Math.min(p1.x, p2.x);
1959
+ const maxX = Math.max(p1.x, p2.x);
1960
+ for (let x = minX; x <= maxX; x++) {
1961
+ cells.push({ x, y: p1.y });
1962
+ }
1963
+ }
1964
+ }
1965
+ return cells;
1966
+ }
1967
+ function segmentMidpointKey(line) {
1968
+ const isVertical = line[0].x === line[1].x;
1969
+ if (isVertical) {
1970
+ const mid = Math.floor((line[0].y + line[1].y) / 2);
1971
+ return `v:${line[0].x}:${mid}`;
1972
+ } else {
1973
+ const mid = Math.floor((line[0].x + line[1].x) / 2);
1974
+ return `h:${line[0].y}:${mid}`;
1975
+ }
1976
+ }
1907
1977
  function determineLabelLine(graph, edge) {
1908
1978
  if (edge.text.length === 0) return;
1909
1979
  const lenLabel = maxLineWidth(edge.text);
1910
1980
  const pathLen = edge.path.length;
1911
- const isVerticalFlow = graph.config.graphDirection === "TD";
1912
1981
  const segments = [];
1913
1982
  for (let i = 1; i < pathLen; i++) {
1914
1983
  const p1 = edge.path[i - 1];
@@ -1918,21 +1987,26 @@ function determineLabelLine(graph, edge) {
1918
1987
  const isVertical = p1.x === p2.x;
1919
1988
  segments.push({ line, width, index: i, isVertical });
1920
1989
  }
1921
- const suitableSegments = segments.filter((s) => s.width >= lenLabel && s.index > 1);
1990
+ const used = graph.usedLabelMidpoints ?? /* @__PURE__ */ new Set();
1991
+ const middleIdx = Math.floor(pathLen / 2);
1992
+ const scoreSeg = (s) => {
1993
+ const key = segmentMidpointKey(s.line);
1994
+ let score = 0;
1995
+ if (!used.has(key)) score += 1e3;
1996
+ if (s.index > 1) score += 100;
1997
+ score -= Math.abs(s.index - middleIdx);
1998
+ return score;
1999
+ };
2000
+ const candidates = segments.filter((s) => s.width >= lenLabel);
1922
2001
  let largestLine;
1923
- if (suitableSegments.length > 0) {
1924
- suitableSegments.sort((a, b) => b.index - a.index);
1925
- largestLine = suitableSegments[0].line;
2002
+ if (candidates.length > 0) {
2003
+ candidates.sort((a, b) => scoreSeg(b) - scoreSeg(a));
2004
+ largestLine = candidates[0].line;
1926
2005
  } else {
1927
- const fallbackSegments = segments.filter((s) => s.width >= lenLabel);
1928
- if (fallbackSegments.length > 0) {
1929
- fallbackSegments.sort((a, b) => b.index - a.index);
1930
- largestLine = fallbackSegments[0].line;
1931
- } else {
1932
- segments.sort((a, b) => b.width - a.width);
1933
- largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
1934
- }
2006
+ segments.sort((a, b) => b.width - a.width);
2007
+ largestLine = segments[0]?.line ?? [edge.path[0], edge.path[1]];
1935
2008
  }
2009
+ used.add(segmentMidpointKey(largestLine));
1936
2010
  const minX = Math.min(largestLine[0].x, largestLine[1].x);
1937
2011
  const maxX = Math.max(largestLine[0].x, largestLine[1].x);
1938
2012
  const middleX = minX + Math.floor((maxX - minX) / 2);
@@ -2853,8 +2927,9 @@ function drawArrow(graph, edge) {
2853
2927
  return [empty, empty, empty, empty, empty, empty];
2854
2928
  }
2855
2929
  const labelCanvas = drawArrowLabel(graph, edge);
2856
- const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style);
2857
- const boxStartCanvas = drawBoxStart(graph, edge.path, linesDrawn[0], edge.from.shape);
2930
+ const endCoordOverride = computeEndCoordOverride(graph, edge);
2931
+ const [pathCanvas, linesDrawn, lineDirs] = drawPath(graph, edge.path, edge.style, void 0, endCoordOverride);
2932
+ const boxStartCanvas = drawBoxStart(graph, edge.path, linesDrawn[0], edge.from);
2858
2933
  let arrowHeadEndCanvas;
2859
2934
  if (edge.hasArrowEnd) {
2860
2935
  arrowHeadEndCanvas = drawArrowHead(
@@ -2883,6 +2958,38 @@ function drawArrow(graph, edge) {
2883
2958
  const cornersCanvas = drawCorners(graph, edge.path);
2884
2959
  return [pathCanvas, boxStartCanvas, arrowHeadEndCanvas, arrowHeadStartCanvas, cornersCanvas, labelCanvas];
2885
2960
  }
2961
+ function computeEndCoordOverride(graph, edge) {
2962
+ if (edge.path.length < 2) return void 0;
2963
+ const lastPathCoord = edge.path[edge.path.length - 1];
2964
+ const prevPathCoord = edge.path[edge.path.length - 2];
2965
+ const dir = determineDirection(prevPathCoord, lastPathCoord);
2966
+ const defaultDC = gridToDrawingCoord(graph, lastPathCoord);
2967
+ const targetDC = edge.to.drawingCoord;
2968
+ const targetGC = edge.to.gridCoord;
2969
+ if (!targetDC || !targetGC) return void 0;
2970
+ let boxW = 0;
2971
+ for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(targetGC.x + i) ?? 0;
2972
+ let boxH = 0;
2973
+ for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(targetGC.y + i) ?? 0;
2974
+ if (dirEquals(dir, Left)) {
2975
+ const borderX = targetDC.x + boxW;
2976
+ if (borderX === defaultDC.x) return void 0;
2977
+ return { x: borderX, y: defaultDC.y };
2978
+ } else if (dirEquals(dir, Right)) {
2979
+ const borderX = targetDC.x;
2980
+ if (borderX === defaultDC.x) return void 0;
2981
+ return { x: borderX, y: defaultDC.y };
2982
+ } else if (dirEquals(dir, Up)) {
2983
+ const borderY = targetDC.y + boxH;
2984
+ if (borderY === defaultDC.y) return void 0;
2985
+ return { x: defaultDC.x, y: borderY };
2986
+ } else if (dirEquals(dir, Down)) {
2987
+ const borderY = targetDC.y;
2988
+ if (borderY === defaultDC.y) return void 0;
2989
+ return { x: defaultDC.x, y: borderY };
2990
+ }
2991
+ return void 0;
2992
+ }
2886
2993
  function reverseDirection(dir) {
2887
2994
  if (dirEquals(dir, Up)) return Down;
2888
2995
  if (dirEquals(dir, Down)) return Up;
@@ -2894,15 +3001,16 @@ function reverseDirection(dir) {
2894
3001
  if (dirEquals(dir, LowerRight)) return UpperLeft;
2895
3002
  return Middle;
2896
3003
  }
2897
- function drawPath(graph, path, style = "solid") {
3004
+ function drawPath(graph, path, style = "solid", startCoordOverride, endCoordOverride) {
2898
3005
  const canvas = copyCanvas(graph.canvas);
2899
3006
  let previousCoord = path[0];
2900
3007
  const linesDrawn = [];
2901
3008
  const lineDirs = [];
3009
+ const lastIdx = path.length - 1;
2902
3010
  for (let i = 1; i < path.length; i++) {
2903
3011
  const nextCoord = path[i];
2904
- const prevDC = gridToDrawingCoord(graph, previousCoord);
2905
- const nextDC = gridToDrawingCoord(graph, nextCoord);
3012
+ const prevDC = i === 1 && startCoordOverride ? startCoordOverride : gridToDrawingCoord(graph, previousCoord);
3013
+ const nextDC = i === lastIdx && endCoordOverride ? endCoordOverride : gridToDrawingCoord(graph, nextCoord);
2906
3014
  if (drawingCoordEquals(prevDC, nextDC)) {
2907
3015
  previousCoord = nextCoord;
2908
3016
  continue;
@@ -2916,18 +3024,45 @@ function drawPath(graph, path, style = "solid") {
2916
3024
  }
2917
3025
  return [canvas, linesDrawn, lineDirs];
2918
3026
  }
2919
- function drawBoxStart(graph, path, firstLine, sourceShape) {
3027
+ function drawBoxStart(graph, path, firstLine, sourceNode) {
2920
3028
  const canvas = copyCanvas(graph.canvas);
2921
3029
  if (graph.config.useAscii) return canvas;
2922
- if (sourceShape === "state-start" || sourceShape === "state-end") {
3030
+ if (sourceNode.shape === "state-start" || sourceNode.shape === "state-end") {
2923
3031
  return canvas;
2924
3032
  }
2925
3033
  const from = firstLine[0];
2926
3034
  const dir = determineDirection(path[0], path[1]);
2927
- if (dirEquals(dir, Up)) canvas[from.x][from.y + 1] = "\u2534";
2928
- else if (dirEquals(dir, Down)) canvas[from.x][from.y - 1] = "\u252C";
2929
- else if (dirEquals(dir, Left)) canvas[from.x + 1][from.y] = "\u2524";
2930
- else if (dirEquals(dir, Right)) canvas[from.x - 1][from.y] = "\u251C";
3035
+ const dc = sourceNode.drawingCoord;
3036
+ const gc = sourceNode.gridCoord;
3037
+ if (dirEquals(dir, Right)) {
3038
+ let boxW = 0;
3039
+ for (let i = 0; i < 2; i++) boxW += graph.columnWidth.get(gc.x + i) ?? 0;
3040
+ const borderX = dc.x + boxW;
3041
+ canvas[borderX][from.y] = "\u251C";
3042
+ for (let x = borderX + 1; x < from.x; x++) {
3043
+ canvas[x][from.y] = "\u2500";
3044
+ }
3045
+ } else if (dirEquals(dir, Left)) {
3046
+ const borderX = dc.x;
3047
+ canvas[borderX][from.y] = "\u2524";
3048
+ for (let x = from.x + 1; x < borderX; x++) {
3049
+ canvas[x][from.y] = "\u2500";
3050
+ }
3051
+ } else if (dirEquals(dir, Down)) {
3052
+ let boxH = 0;
3053
+ for (let i = 0; i < 2; i++) boxH += graph.rowHeight.get(gc.y + i) ?? 0;
3054
+ const borderY = dc.y + boxH;
3055
+ canvas[from.x][borderY] = "\u252C";
3056
+ for (let y = borderY + 1; y < from.y; y++) {
3057
+ canvas[from.x][y] = "\u2502";
3058
+ }
3059
+ } else if (dirEquals(dir, Up)) {
3060
+ const borderY = dc.y;
3061
+ canvas[from.x][borderY] = "\u2534";
3062
+ for (let y = from.y + 1; y < borderY; y++) {
3063
+ canvas[from.x][y] = "\u2502";
3064
+ }
3065
+ }
2931
3066
  return canvas;
2932
3067
  }
2933
3068
  function drawArrowHead(graph, lastLine, fallbackDir) {
@@ -3815,7 +3950,14 @@ function createMapping(graph) {
3815
3950
  const parentSg = getNodeSubgraph(graph, node);
3816
3951
  const childSg = getNodeSubgraph(graph, child);
3817
3952
  const edgeDir = parentSg && parentSg === childSg && parentSg.direction ? parentSg.direction : graph.config.graphDirection;
3818
- let childLevel = edgeDir === "LR" ? gc.x + 4 : gc.y + 4;
3953
+ let maxPredLevel = edgeDir === "LR" ? gc.x : gc.y;
3954
+ for (const e of graph.edges) {
3955
+ if (e.to.name === child.name && e.from.gridCoord !== null) {
3956
+ const predLevel = edgeDir === "LR" ? e.from.gridCoord.x : e.from.gridCoord.y;
3957
+ if (predLevel > maxPredLevel) maxPredLevel = predLevel;
3958
+ }
3959
+ }
3960
+ let childLevel = maxPredLevel + 4;
3819
3961
  if (childSg && parentSg !== childSg && deferredRoots.size > 0) {
3820
3962
  let placedDeferred = false;
3821
3963
  for (const dr of [...deferredRoots]) {
@@ -3880,17 +4022,44 @@ function createMapping(graph) {
3880
4022
  for (const node of graph.nodes) {
3881
4023
  setColumnWidth(graph, node);
3882
4024
  }
4025
+ const labeledInDegree = /* @__PURE__ */ new Map();
4026
+ for (const edge of graph.edges) {
4027
+ if (edge.text.length > 0) {
4028
+ labeledInDegree.set(edge.to.name, (labeledInDegree.get(edge.to.name) ?? 0) + 1);
4029
+ }
4030
+ }
4031
+ for (const node of graph.nodes) {
4032
+ const deg = labeledInDegree.get(node.name) ?? 0;
4033
+ if (deg >= 3 && node.gridCoord) {
4034
+ const approachRow = node.gridCoord.y - 1;
4035
+ if (approachRow >= 0) {
4036
+ const extra = Math.min((deg - 2) * 2, 10);
4037
+ const current = graph.rowHeight.get(approachRow) ?? 0;
4038
+ graph.rowHeight.set(approachRow, Math.max(current, current + extra));
4039
+ }
4040
+ }
4041
+ }
3883
4042
  graph.bundles = analyzeEdgeBundles(graph);
3884
4043
  processBundles(graph);
4044
+ graph.usedLabelMidpoints = /* @__PURE__ */ new Set();
4045
+ const congestion = /* @__PURE__ */ new Map();
3885
4046
  for (const edge of graph.edges) {
3886
4047
  if (edge.bundle && edge.path.length > 0) {
3887
4048
  increaseGridSizeForPath(graph, edge.path);
3888
4049
  determineLabelLine(graph, edge);
4050
+ for (const cell of pathToCells(edge.path)) {
4051
+ const k = gridKey(cell);
4052
+ congestion.set(k, (congestion.get(k) ?? 0) + 1);
4053
+ }
3889
4054
  continue;
3890
4055
  }
3891
- determinePath(graph, edge);
4056
+ determinePath(graph, edge, congestion);
3892
4057
  increaseGridSizeForPath(graph, edge.path);
3893
4058
  determineLabelLine(graph, edge);
4059
+ for (const cell of pathToCells(edge.path)) {
4060
+ const k = gridKey(cell);
4061
+ congestion.set(k, (congestion.get(k) ?? 0) + 1);
4062
+ }
3894
4063
  }
3895
4064
  for (const node of graph.nodes) {
3896
4065
  node.drawingCoord = gridToDrawingCoord(graph, node.gridCoord);