diff --git a/.changeset/bright-elk-centering.md b/.changeset/bright-elk-centering.md new file mode 100644 index 00000000000..af24ee3b0fb --- /dev/null +++ b/.changeset/bright-elk-centering.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +Use balanced Brandes-Koepf placement for the default ELK preset to improve alignment of branches and composite-state entries. Preserve named non-default presets and explicit placement/alignment options, including `NONE`. Some diagrams may become wider or taller; `depthFirst` retains the previous default layout. diff --git a/.changeset/elk-layout-presets.md b/.changeset/elk-layout-presets.md index 55af18a01c8..cad3da5410b 100644 --- a/.changeset/elk-layout-presets.md +++ b/.changeset/elk-layout-presets.md @@ -6,10 +6,10 @@ feat: `elk.preset` picks a named combination of the options that decide where no Three options settle node positions, and they sit in different phases of the layout: which layer a node lands in, where it goes within that layer, and which edges get reversed to make the graph acyclic. Choosing them well means knowing all three interact; `preset` names the combinations worth using. -- `default` — network simplex layering and placement with depth-first cycle breaking at the top level; subgraph contents are placed with Brandes-Koepf. Depth-first breaking gives shorter back edges on graphs that loop. +- `default` — network simplex layering, balanced Brandes-Koepf placement at the top level and inside subgraphs, and depth-first cycle breaking. Balanced placement centres branches and composite-state entries; depth-first breaking gives shorter back edges on graphs that loop. - `legacy` — reproduces what earlier versions actually rendered: Brandes-Koepf placement with ELK's own greedy cycle breaking. -- `modelOrder` — as `default`, but breaks cycles by greedy model order, which disturbs declaration order least at the cost of longer back edges. -- `depthFirst` — a name for what `default` already is, so a diagram can say depth-first rather than rely on the default staying put. +- `modelOrder` — as `depthFirst`, but breaks cycles by greedy model order, which disturbs declaration order least at the cost of longer back edges. +- `depthFirst` — the previous default: network simplex layering and top-level placement, Brandes-Koepf placement inside subgraphs, `NONE` alignment, and depth-first cycle breaking. ```yaml --- @@ -20,10 +20,10 @@ config: --- ``` -Setting `layeringStrategy`, `nodePlacementStrategy` or `cycleBreakingStrategy` explicitly overrides the preset for that one option and leaves the rest in place, so a preset is a starting point rather than a lock. +Setting `layeringStrategy`, `nodePlacementStrategy`, `nodePlacementAlignment` or `cycleBreakingStrategy` explicitly overrides the preset for that one option and leaves the rest in place, so a preset is a starting point rather than a lock. -**Node placement changes from `BRANDES_KOEPF` to `NETWORK_SIMPLEX`, so existing ELK diagrams will lay out differently.** `preset: legacy` restores the previous behaviour, and is the single switch for it — this is the net change against the last release, measured from what shipped rather than from any intermediate state. +**Node placement keeps `BRANDES_KOEPF`, but its alignment changes from `NONE` to `BALANCED` and cycle breaking from `GREEDY` to `DEPTH_FIRST`, so existing ELK diagrams will lay out differently.** `preset: legacy` restores the previous behaviour, and is the single switch for it — this is the net change against the last release, measured from what shipped rather than from any intermediate state. -Subgraph contents keep `BRANDES_KOEPF`, which is deliberately not the root's strategy: network simplex inside a frame produced routes that left the subgraph on its bounding-box corner. The two sides are tuned independently, so `nodePlacementStrategy` set explicitly still applies to both. +Subgraph contents use `BRANDES_KOEPF` under every preset. For `modelOrder` and `depthFirst` that is deliberately not the root's strategy: network simplex inside a frame produced routes that left the subgraph on its bounding-box corner. The two sides are tuned independently, so `nodePlacementStrategy` set explicitly still applies to both. Note that `legacy` uses `GREEDY` cycle breaking rather than the `GREEDY_MODEL_ORDER` the schema previously advertised. That default was declared in the schema but never listed in the shipped defaults, so it reached ELK as undefined and ELK's own default applied — `legacy` reproduces what was rendered, not what was documented. diff --git a/docs/config/setup/defaultConfig/variables/configKeys.md b/docs/config/setup/defaultConfig/variables/configKeys.md index 632794937c9..57ad371e98f 100644 --- a/docs/config/setup/defaultConfig/variables/configKeys.md +++ b/docs/config/setup/defaultConfig/variables/configKeys.md @@ -12,4 +12,4 @@ > `const` **configKeys**: `Set`<`string`> -Defined in: [packages/mermaid/src/defaultConfig.ts:357](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L357) +Defined in: [packages/mermaid/src/defaultConfig.ts:356](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/defaultConfig.ts#L356) diff --git a/docs/config/setup/mermaid/interfaces/MermaidConfig.md b/docs/config/setup/mermaid/interfaces/MermaidConfig.md index 7db03bf3c61..7b6ed2f2568 100644 --- a/docs/config/setup/mermaid/interfaces/MermaidConfig.md +++ b/docs/config/setup/mermaid/interfaces/MermaidConfig.md @@ -18,7 +18,7 @@ Defined in: [packages/mermaid/src/config.type.ts:66](https://github.com/mermaid- > `optional` **agentflow**: `AgentflowDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:325](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L325) +Defined in: [packages/mermaid/src/config.type.ts:326](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L326) --- @@ -26,7 +26,7 @@ Defined in: [packages/mermaid/src/config.type.ts:325](https://github.com/mermaid > `optional` **altFontFamily**: `string` -Defined in: [packages/mermaid/src/config.type.ts:263](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L263) +Defined in: [packages/mermaid/src/config.type.ts:264](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L264) --- @@ -34,7 +34,7 @@ Defined in: [packages/mermaid/src/config.type.ts:263](https://github.com/mermaid > `optional` **architecture**: `ArchitectureDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:337](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L337) +Defined in: [packages/mermaid/src/config.type.ts:338](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L338) --- @@ -42,7 +42,7 @@ Defined in: [packages/mermaid/src/config.type.ts:337](https://github.com/mermaid > `optional` **arrowMarkerAbsolute**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:282](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L282) +Defined in: [packages/mermaid/src/config.type.ts:283](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L283) Controls whether or arrow markers in html code are absolute paths or anchors. This matters if you are using base tag settings. @@ -53,7 +53,7 @@ This matters if you are using base tag settings. > `optional` **block**: `BlockDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:345](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L345) +Defined in: [packages/mermaid/src/config.type.ts:346](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L346) --- @@ -61,7 +61,7 @@ Defined in: [packages/mermaid/src/config.type.ts:345](https://github.com/mermaid > `optional` **c4**: `C4DiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:342](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L342) +Defined in: [packages/mermaid/src/config.type.ts:343](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L343) --- @@ -69,7 +69,7 @@ Defined in: [packages/mermaid/src/config.type.ts:342](https://github.com/mermaid > `optional` **class**: `ClassDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:330](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L330) +Defined in: [packages/mermaid/src/config.type.ts:331](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L331) --- @@ -77,7 +77,7 @@ Defined in: [packages/mermaid/src/config.type.ts:330](https://github.com/mermaid > `optional` **cynefin**: `CynefinDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:352](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L352) +Defined in: [packages/mermaid/src/config.type.ts:353](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L353) --- @@ -85,7 +85,7 @@ Defined in: [packages/mermaid/src/config.type.ts:352](https://github.com/mermaid > `optional` **darkMode**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:247](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L247) +Defined in: [packages/mermaid/src/config.type.ts:248](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L248) --- @@ -93,7 +93,7 @@ Defined in: [packages/mermaid/src/config.type.ts:247](https://github.com/mermaid > `optional` **deterministicIds**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:315](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L315) +Defined in: [packages/mermaid/src/config.type.ts:316](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L316) This option controls if the generated ids of nodes in the SVG are generated randomly or based on a seed. @@ -109,7 +109,7 @@ should not change unless content is changed. > `optional` **deterministicIDSeed**: `string` -Defined in: [packages/mermaid/src/config.type.ts:322](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L322) +Defined in: [packages/mermaid/src/config.type.ts:323](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L323) This option is the optional seed for deterministic ids. If set to `undefined` but deterministicIds is `true`, a simple number iterator is used. @@ -121,7 +121,7 @@ You can set this attribute to base the seed on a static string. > `optional` **dompurifyConfig**: `Config` -Defined in: [packages/mermaid/src/config.type.ts:354](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L354) +Defined in: [packages/mermaid/src/config.type.ts:355](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L355) --- @@ -205,7 +205,9 @@ Elk specific option that allows edges to share path where it convenient. It can > `optional` **nodePlacementAlignment**: `"NONE"` | `"LEFTUP"` | `"LEFTDOWN"` | `"RIGHTUP"` | `"RIGHTDOWN"` | `"BALANCED"` Elk specific option affecting Brandes-Koepf node placement alignment. -NONE picks the alignment with the smallest height. +BALANCED combines the four directional alignments; NONE picks the +smallest result. Defaults to BALANCED for the default preset and NONE +for named non-default presets. #### nodePlacementStrategy? @@ -217,31 +219,30 @@ Elk specific option affecting how nodes are placed. > `optional` **preset**: `"legacy"` | `"default"` | `"modelOrder"` | `"depthFirst"` -Named combination of the three options that decide where nodes end up: -layering strategy, node placement strategy and cycle breaking strategy. -They belong to different phases of the layout, so a preset is simply a -named triple rather than a mode with behaviour of its own. +Named combination of layering, node placement, placement alignment +and cycle breaking options. Explicit options override the preset for +that option; the remaining preset values still apply. -`default` — network simplex layering and placement with depth-first -cycle breaking at the top level, and Brandes-Koepf placement inside -subgraphs. Depth-first breaking gives shorter back edges on graphs -that loop. +`default` — network simplex layering, balanced Brandes-Koepf placement +at the top level and inside subgraphs, and depth-first cycle breaking. +Balanced placement favors centered branches and composite-state entries, +sometimes at the cost of a wider or taller drawing. It does not +guarantee that every edge attaches to the center of its target. -`legacy` — what shipped before presets existed: Brandes-Koepf placement, -which straightens long edges at the cost of that alignment, with ELK's -own greedy cycle breaking. Reproduces the rendering of earlier -versions rather than the defaults their schema advertised. +`legacy` — Brandes-Koepf placement with NONE alignment and ELK's own +greedy cycle breaking. Reproduces the rendering before presets existed. -`modelOrder` — as `default`, but breaks cycles by greedy model order, -which disturbs declaration order least at the cost of longer back -edges. This is the combination `default` named previously. +`modelOrder` — network simplex layering and top-level placement, +Brandes-Koepf placement inside subgraphs, NONE alignment, and greedy +model-order cycle breaking, which favors declaration order. -`depthFirst` — a name for what `default` already is, for diagrams that -would rather say depth-first than rely on the default. +`depthFirst` — the previous default: network simplex layering and +top-level placement, Brandes-Koepf placement inside subgraphs, NONE +alignment, and depth-first cycle breaking. -Setting `layeringStrategy`, `nodePlacementStrategy` or -`cycleBreakingStrategy` explicitly overrides the preset for that one -option; the rest of the preset still applies. +Setting `layeringStrategy`, `nodePlacementStrategy`, +`nodePlacementAlignment` or `cycleBreakingStrategy` explicitly overrides +the preset for that option, including an explicit NONE alignment. #### straightenEdges? @@ -268,7 +269,7 @@ port, or would introduce a crossing. > `optional` **er**: `ErDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:332](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L332) +Defined in: [packages/mermaid/src/config.type.ts:333](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L333) --- @@ -276,7 +277,7 @@ Defined in: [packages/mermaid/src/config.type.ts:332](https://github.com/mermaid > `optional` **eventmodeling**: `EventModelingDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:346](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L346) +Defined in: [packages/mermaid/src/config.type.ts:347](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L347) --- @@ -284,7 +285,7 @@ Defined in: [packages/mermaid/src/config.type.ts:346](https://github.com/mermaid > `optional` **flowchart**: `FlowchartDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:323](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L323) +Defined in: [packages/mermaid/src/config.type.ts:324](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L324) --- @@ -292,7 +293,7 @@ Defined in: [packages/mermaid/src/config.type.ts:323](https://github.com/mermaid > `optional` **fontFamily**: `string` -Defined in: [packages/mermaid/src/config.type.ts:262](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L262) +Defined in: [packages/mermaid/src/config.type.ts:263](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L263) Specifies the font to be used in the rendered diagrams. Can be any possible CSS `font-family`. @@ -304,7 +305,7 @@ See > `optional` **fontSize**: `number` -Defined in: [packages/mermaid/src/config.type.ts:356](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L356) +Defined in: [packages/mermaid/src/config.type.ts:357](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L357) --- @@ -312,7 +313,7 @@ Defined in: [packages/mermaid/src/config.type.ts:356](https://github.com/mermaid > `optional` **forceLegacyMathML**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:304](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L304) +Defined in: [packages/mermaid/src/config.type.ts:305](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L305) This option forces Mermaid to rely on KaTeX's own stylesheet for rendering MathML. Due to differences between OS fonts and browser's MathML implementation, this option is recommended if consistent rendering is important. @@ -324,7 +325,7 @@ If set to true, ignores legacyMathML. > `optional` **gantt**: `GanttDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:327](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L327) +Defined in: [packages/mermaid/src/config.type.ts:328](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L328) --- @@ -332,7 +333,7 @@ Defined in: [packages/mermaid/src/config.type.ts:327](https://github.com/mermaid > `optional` **gitGraph**: `GitGraphDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:341](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L341) +Defined in: [packages/mermaid/src/config.type.ts:342](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L342) --- @@ -350,7 +351,7 @@ Defines the seed to be used when using handDrawn look. This is important for the > `optional` **htmlLabels**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:255](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L255) +Defined in: [packages/mermaid/src/config.type.ts:256](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L256) Flag for setting whether or not a html tag should be used for rendering labels on nodes and edges. **Note:** Diagram-specific `htmlLabels` settings (e.g., `flowchart.htmlLabels`) are deprecated. @@ -363,7 +364,7 @@ over any diagram-specific settings. > `optional` **ishikawa**: `IshikawaDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:339](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L339) +Defined in: [packages/mermaid/src/config.type.ts:340](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L340) --- @@ -371,7 +372,7 @@ Defined in: [packages/mermaid/src/config.type.ts:339](https://github.com/mermaid > `optional` **journey**: `JourneyDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:328](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L328) +Defined in: [packages/mermaid/src/config.type.ts:329](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L329) --- @@ -379,7 +380,7 @@ Defined in: [packages/mermaid/src/config.type.ts:328](https://github.com/mermaid > `optional` **kanban**: `KanbanDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:340](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L340) +Defined in: [packages/mermaid/src/config.type.ts:341](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L341) --- @@ -400,7 +401,7 @@ ELK to stay small and falls back to `dagre`. > `optional` **legacyMathML**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:297](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L297) +Defined in: [packages/mermaid/src/config.type.ts:298](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L298) This option specifies if Mermaid can expect the dependent to include KaTeX stylesheets for browsers without their own MathML implementation. If this option is disabled and MathML is not supported, the math @@ -413,7 +414,7 @@ fall back to legacy rendering for KaTeX. > `optional` **logLevel**: `0` | `2` | `1` | `"trace"` | `"debug"` | `"info"` | `"warn"` | `"error"` | `"fatal"` | `3` | `4` | `5` -Defined in: [packages/mermaid/src/config.type.ts:268](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L268) +Defined in: [packages/mermaid/src/config.type.ts:269](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L269) This option decides the amount of logging to be used by mermaid. @@ -433,7 +434,7 @@ Defines which main look to use for the diagram. > `optional` **markdownAutoWrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:357](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L357) +Defined in: [packages/mermaid/src/config.type.ts:358](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L358) --- @@ -461,7 +462,7 @@ The maximum allowed size of the users text diagram > `optional` **mindmap**: `MindmapDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:338](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L338) +Defined in: [packages/mermaid/src/config.type.ts:339](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L339) --- @@ -469,7 +470,7 @@ Defined in: [packages/mermaid/src/config.type.ts:338](https://github.com/mermaid > `optional` **packet**: `PacketDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:344](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L344) +Defined in: [packages/mermaid/src/config.type.ts:345](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L345) --- @@ -477,7 +478,7 @@ Defined in: [packages/mermaid/src/config.type.ts:344](https://github.com/mermaid > `optional` **pie**: `PieDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:333](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L333) +Defined in: [packages/mermaid/src/config.type.ts:334](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L334) --- @@ -485,7 +486,7 @@ Defined in: [packages/mermaid/src/config.type.ts:333](https://github.com/mermaid > `optional` **quadrantChart**: `QuadrantChartConfig` -Defined in: [packages/mermaid/src/config.type.ts:334](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L334) +Defined in: [packages/mermaid/src/config.type.ts:335](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L335) --- @@ -493,7 +494,7 @@ Defined in: [packages/mermaid/src/config.type.ts:334](https://github.com/mermaid > `optional` **radar**: `RadarDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:348](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L348) +Defined in: [packages/mermaid/src/config.type.ts:349](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L349) --- @@ -501,7 +502,7 @@ Defined in: [packages/mermaid/src/config.type.ts:348](https://github.com/mermaid > `optional` **railroad**: `RailroadDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:353](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L353) +Defined in: [packages/mermaid/src/config.type.ts:354](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L354) --- @@ -509,7 +510,7 @@ Defined in: [packages/mermaid/src/config.type.ts:353](https://github.com/mermaid > `optional` **requirement**: `RequirementDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:336](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L336) +Defined in: [packages/mermaid/src/config.type.ts:337](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L337) --- @@ -517,7 +518,7 @@ Defined in: [packages/mermaid/src/config.type.ts:336](https://github.com/mermaid > `optional` **sankey**: `SankeyDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:343](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L343) +Defined in: [packages/mermaid/src/config.type.ts:344](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L344) --- @@ -525,7 +526,7 @@ Defined in: [packages/mermaid/src/config.type.ts:343](https://github.com/mermaid > `optional` **secure**: `string`\[] -Defined in: [packages/mermaid/src/config.type.ts:289](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L289) +Defined in: [packages/mermaid/src/config.type.ts:290](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L290) This option controls which `currentConfig` keys are considered secure and can only be changed via call to `mermaid.initialize`. @@ -537,7 +538,7 @@ This prevents malicious graph directives from overriding a site's default securi > `optional` **securityLevel**: `"strict"` | `"loose"` | `"antiscript"` | `"sandbox"` -Defined in: [packages/mermaid/src/config.type.ts:272](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L272) +Defined in: [packages/mermaid/src/config.type.ts:273](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L273) Level of trust for parsed diagram @@ -547,7 +548,7 @@ Level of trust for parsed diagram > `optional` **sequence**: `SequenceDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:326](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L326) +Defined in: [packages/mermaid/src/config.type.ts:327](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L327) --- @@ -555,7 +556,7 @@ Defined in: [packages/mermaid/src/config.type.ts:326](https://github.com/mermaid > `optional` **startOnLoad**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:276](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L276) +Defined in: [packages/mermaid/src/config.type.ts:277](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L277) Dictates whether mermaid starts on Page load @@ -565,7 +566,7 @@ Dictates whether mermaid starts on Page load > `optional` **state**: `StateDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:331](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L331) +Defined in: [packages/mermaid/src/config.type.ts:332](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L332) --- @@ -573,7 +574,7 @@ Defined in: [packages/mermaid/src/config.type.ts:331](https://github.com/mermaid > `optional` **suppressErrorRendering**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:363](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L363) +Defined in: [packages/mermaid/src/config.type.ts:364](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L364) Suppresses inserting 'Syntax error' diagram in the DOM. This is useful when you want to control how to handle syntax errors in your application. @@ -584,7 +585,7 @@ This is useful when you want to control how to handle syntax errors in your appl > `optional` **swimlane**: `SwimlaneDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:324](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L324) +Defined in: [packages/mermaid/src/config.type.ts:325](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L325) --- @@ -619,7 +620,7 @@ Defined in: [packages/mermaid/src/config.type.ts:85](https://github.com/mermaid- > `optional` **timeline**: `TimelineDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:329](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L329) +Defined in: [packages/mermaid/src/config.type.ts:330](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L330) --- @@ -627,7 +628,7 @@ Defined in: [packages/mermaid/src/config.type.ts:329](https://github.com/mermaid > `optional` **treeView**: `TreeViewDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:347](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L347) +Defined in: [packages/mermaid/src/config.type.ts:348](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L348) --- @@ -635,7 +636,7 @@ Defined in: [packages/mermaid/src/config.type.ts:347](https://github.com/mermaid > `optional` **usecase**: `UsecaseDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:349](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L349) +Defined in: [packages/mermaid/src/config.type.ts:350](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L350) --- @@ -643,7 +644,7 @@ Defined in: [packages/mermaid/src/config.type.ts:349](https://github.com/mermaid > `optional` **venn**: `VennDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:350](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L350) +Defined in: [packages/mermaid/src/config.type.ts:351](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L351) --- @@ -651,7 +652,7 @@ Defined in: [packages/mermaid/src/config.type.ts:350](https://github.com/mermaid > `optional` **wardley-beta**: `WardleyDiagramConfig` -Defined in: [packages/mermaid/src/config.type.ts:351](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L351) +Defined in: [packages/mermaid/src/config.type.ts:352](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L352) --- @@ -659,7 +660,7 @@ Defined in: [packages/mermaid/src/config.type.ts:351](https://github.com/mermaid > `optional` **wrap**: `boolean` -Defined in: [packages/mermaid/src/config.type.ts:355](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L355) +Defined in: [packages/mermaid/src/config.type.ts:356](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L356) --- @@ -667,4 +668,4 @@ Defined in: [packages/mermaid/src/config.type.ts:355](https://github.com/mermaid > `optional` **xyChart**: `XYChartConfig` -Defined in: [packages/mermaid/src/config.type.ts:335](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L335) +Defined in: [packages/mermaid/src/config.type.ts:336](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.type.ts#L336) diff --git a/docs/intro/syntax-reference.md b/docs/intro/syntax-reference.md index 32877d30624..72d0b5f6909 100644 --- a/docs/intro/syntax-reference.md +++ b/docs/intro/syntax-reference.md @@ -228,12 +228,13 @@ When using the ELK layout, you can further refine the diagram’s configuration, - LINEAR_SEGMENTS - BRANDES_KOEPF (default) - To configure Brandes-Koepf node placement alignment, use nodePlacementAlignment with the following options: - - NONE (default) + - NONE - LEFTUP - LEFTDOWN - RIGHTUP - RIGHTDOWN - BALANCED +- When nodePlacementAlignment is not set, the alignment comes from the `elk.preset` option: BALANCED for the `default` preset, and NONE for the named non-default presets `legacy`, `modelOrder` and `depthFirst`. Use `preset: depthFirst` to get the former default layout, or set nodePlacementAlignment explicitly to override the preset. **Example configuration:** diff --git a/e2e/diagrams/state-diagram-v2/elk/elk-composite-entry-default-centering.mmd b/e2e/diagrams/state-diagram-v2/elk/elk-composite-entry-default-centering.mmd new file mode 100644 index 00000000000..2223f24f8ce --- /dev/null +++ b/e2e/diagrams/state-diagram-v2/elk/elk-composite-entry-default-centering.mmd @@ -0,0 +1,27 @@ +--- +config: + fontFamily: courier + layout: elk +--- +stateDiagram-v2 + [*]-->TV + + state TV { + [*] --> Off: Off to start with + On --> Off : Turn off + Off --> On : Turn on + } + + TV--> Console + + state Console { + [*] --> Off2: Off to start with + On2--> Off2 : Turn off + Off2 --> On2 : Turn on + On2-->Playing + + state Playing { + Alive --> Dead + Dead-->Alive + } + } diff --git a/packages/mermaid/src/config.spec.ts b/packages/mermaid/src/config.spec.ts index 4c3a210cff3..9cfc9b4685f 100644 --- a/packages/mermaid/src/config.spec.ts +++ b/packages/mermaid/src/config.spec.ts @@ -60,10 +60,11 @@ describe('when working with site config', () => { expect(config.swimlane?.automaticLaneOrdering).toBe(false); }); - it('should default ELK node placement alignment to NONE', () => { + it('leaves ELK placement and alignment to the selected preset', () => { const config = configApi.getConfig(); - expect(config.elk?.nodePlacementAlignment).toBe('NONE'); + expect(config.elk?.nodePlacementStrategy).toBeUndefined(); + expect(config.elk?.nodePlacementAlignment).toBeUndefined(); }); it('should not force class diagram padding', () => { diff --git a/packages/mermaid/src/config.type.ts b/packages/mermaid/src/config.type.ts index fd2367e1048..a91fdf1be60 100644 --- a/packages/mermaid/src/config.type.ts +++ b/packages/mermaid/src/config.type.ts @@ -124,36 +124,37 @@ export interface MermaidConfig { nodePlacementStrategy?: 'SIMPLE' | 'NETWORK_SIMPLEX' | 'LINEAR_SEGMENTS' | 'BRANDES_KOEPF'; /** * Elk specific option affecting Brandes-Koepf node placement alignment. - * NONE picks the alignment with the smallest height. + * BALANCED combines the four directional alignments; NONE picks the + * smallest result. Defaults to BALANCED for the default preset and NONE + * for named non-default presets. * */ nodePlacementAlignment?: 'NONE' | 'LEFTUP' | 'LEFTDOWN' | 'RIGHTUP' | 'RIGHTDOWN' | 'BALANCED'; /** - * Named combination of the three options that decide where nodes end up: - * layering strategy, node placement strategy and cycle breaking strategy. - * They belong to different phases of the layout, so a preset is simply a - * named triple rather than a mode with behaviour of its own. + * Named combination of layering, node placement, placement alignment + * and cycle breaking options. Explicit options override the preset for + * that option; the remaining preset values still apply. * - * `default` — network simplex layering and placement with depth-first - * cycle breaking at the top level, and Brandes-Koepf placement inside - * subgraphs. Depth-first breaking gives shorter back edges on graphs - * that loop. + * `default` — network simplex layering, balanced Brandes-Koepf placement + * at the top level and inside subgraphs, and depth-first cycle breaking. + * Balanced placement favors centered branches and composite-state entries, + * sometimes at the cost of a wider or taller drawing. It does not + * guarantee that every edge attaches to the center of its target. * - * `legacy` — what shipped before presets existed: Brandes-Koepf placement, - * which straightens long edges at the cost of that alignment, with ELK's - * own greedy cycle breaking. Reproduces the rendering of earlier - * versions rather than the defaults their schema advertised. + * `legacy` — Brandes-Koepf placement with NONE alignment and ELK's own + * greedy cycle breaking. Reproduces the rendering before presets existed. * - * `modelOrder` — as `default`, but breaks cycles by greedy model order, - * which disturbs declaration order least at the cost of longer back - * edges. This is the combination `default` named previously. + * `modelOrder` — network simplex layering and top-level placement, + * Brandes-Koepf placement inside subgraphs, NONE alignment, and greedy + * model-order cycle breaking, which favors declaration order. * - * `depthFirst` — a name for what `default` already is, for diagrams that - * would rather say depth-first than rely on the default. + * `depthFirst` — the previous default: network simplex layering and + * top-level placement, Brandes-Koepf placement inside subgraphs, NONE + * alignment, and depth-first cycle breaking. * - * Setting `layeringStrategy`, `nodePlacementStrategy` or - * `cycleBreakingStrategy` explicitly overrides the preset for that one - * option; the rest of the preset still applies. + * Setting `layeringStrategy`, `nodePlacementStrategy`, + * `nodePlacementAlignment` or `cycleBreakingStrategy` explicitly overrides + * the preset for that option, including an explicit NONE alignment. * */ preset?: 'default' | 'legacy' | 'modelOrder' | 'depthFirst'; diff --git a/packages/mermaid/src/defaultConfig.ts b/packages/mermaid/src/defaultConfig.ts index 332191258a8..93443525ddb 100644 --- a/packages/mermaid/src/defaultConfig.ts +++ b/packages/mermaid/src/defaultConfig.ts @@ -33,8 +33,7 @@ const config: RequiredDeep = { layeringStrategy: undefined, cycleBreakingStrategy: undefined, layeringLayerBound: 4, - // Brandes-Koepf specific; inert unless nodePlacementStrategy is set back to it. - nodePlacementAlignment: 'NONE', + nodePlacementAlignment: undefined, forceNodeModelOrder: false, considerModelOrder: 'NODES_AND_EDGES', diff --git a/packages/mermaid/src/docs/intro/syntax-reference.md b/packages/mermaid/src/docs/intro/syntax-reference.md index 553823b75f2..66bd435d72c 100644 --- a/packages/mermaid/src/docs/intro/syntax-reference.md +++ b/packages/mermaid/src/docs/intro/syntax-reference.md @@ -169,12 +169,13 @@ When using the ELK layout, you can further refine the diagram’s configuration, - LINEAR_SEGMENTS - BRANDES_KOEPF (default) - To configure Brandes-Koepf node placement alignment, use nodePlacementAlignment with the following options: - - NONE (default) + - NONE - LEFTUP - LEFTDOWN - RIGHTUP - RIGHTDOWN - BALANCED +- When nodePlacementAlignment is not set, the alignment comes from the `elk.preset` option: BALANCED for the `default` preset, and NONE for the named non-default presets `legacy`, `modelOrder` and `depthFirst`. Use `preset: depthFirst` to get the former default layout, or set nodePlacementAlignment explicitly to override the preset. **Example configuration:** diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/defaultPreset.spec.ts b/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/defaultPreset.spec.ts new file mode 100644 index 00000000000..fdd6355d17f --- /dev/null +++ b/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/defaultPreset.spec.ts @@ -0,0 +1,330 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import mermaidAPI from '../../../../mermaidAPI.js'; +import { getConfig } from '../../../../config.js'; +import type { MermaidConfig } from '../../../../config.type.js'; +import { buildElkGraphFromLayoutData, runElkLayoutCore } from '../render.js'; + +const log = { + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined, +}; +const context = { + algorithm: 'elk.layered', + common: { lineBreakRegex: //gi }, + getConfig, + interpolateToCurve: (curve: unknown) => curve, + log, +} as any; + +afterEach(() => mermaidAPI.initialize({})); + +const graphFor = (elk?: MermaidConfig['elk']) => { + mermaidAPI.initialize(elk ? { elk } : {}); + return buildElkGraphFromLayoutData( + { + direction: 'TB', + config: getConfig(), + edges: [], + nodes: [ + { id: 'group', isGroup: true, label: 'Group', padding: 8 }, + { id: 'child', parentId: 'group', label: 'Child', width: 100, height: 40 }, + ], + } as any, + context + ); +}; + +describe('ELK preset configuration through initialize', () => { + it.each([undefined, 'default'] as const)('centers omitted/default preset (%s)', (preset) => { + const { elkGraph, nodeDb } = graphFor(preset ? { preset } : undefined); + for (const options of [elkGraph.layoutOptions, nodeDb.group.layoutOptions]) { + expect(options?.['elk.layered.nodePlacement.strategy']).toBe('BRANDES_KOEPF'); + expect(options?.['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('BALANCED'); + } + }); + + it.each([ + ['legacy', 'BRANDES_KOEPF', 'GREEDY'], + ['modelOrder', 'NETWORK_SIMPLEX', 'GREEDY_MODEL_ORDER'], + ['depthFirst', 'NETWORK_SIMPLEX', 'DEPTH_FIRST'], + ] as const)('preserves the %s preset', (preset, placement, cycleBreaking) => { + const { elkGraph, nodeDb } = graphFor({ preset }); + expect(elkGraph.layoutOptions['elk.layered.nodePlacement.strategy']).toBe(placement); + expect(nodeDb.group.layoutOptions?.['elk.layered.nodePlacement.strategy']).toBe( + 'BRANDES_KOEPF' + ); + expect(elkGraph.layoutOptions['elk.layered.layering.strategy']).toBe('NETWORK_SIMPLEX'); + for (const options of [elkGraph.layoutOptions, nodeDb.group.layoutOptions]) { + expect(options?.['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('NONE'); + expect(options?.['elk.layered.cycleBreaking.strategy']).toBe(cycleBreaking); + } + }); + + it.each(['default', 'legacy', 'modelOrder', 'depthFirst'] as const)( + 'honors explicit placement and NONE alignment with %s', + (preset) => { + const { elkGraph, nodeDb } = graphFor({ + preset, + nodePlacementStrategy: 'SIMPLE', + nodePlacementAlignment: 'NONE', + }); + for (const options of [elkGraph.layoutOptions, nodeDb.group.layoutOptions]) { + expect(options?.['elk.layered.nodePlacement.strategy']).toBe('SIMPLE'); + expect(options?.['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('NONE'); + } + } + ); + + it('allows alignment to be overridden without replacing default placement', () => { + const { elkGraph, nodeDb } = graphFor({ nodePlacementAlignment: 'LEFTUP' }); + for (const options of [elkGraph.layoutOptions, nodeDb.group.layoutOptions]) { + expect(options?.['elk.layered.nodePlacement.strategy']).toBe('BRANDES_KOEPF'); + expect(options?.['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('LEFTUP'); + } + }); +}); + +// Measurements from the courier-font TV/Console fixture. Run the real ELK core; +// assert the visible attachment relationship rather than exact layout coordinates. +const compositeFixture = { + nodes: [ + { + id: 'root_start', + isGroup: false, + label: 'root_start', + shape: 'stateStart', + padding: 8, + width: 14, + height: 14, + }, + { + id: 'TV', + isGroup: true, + label: 'TV', + shape: 'roundedWithTitle', + dir: 'TB', + padding: 8, + labelBBox: { + width: 19.203125, + height: 24, + }, + }, + { + id: 'TV_start', + parentId: 'TV', + isGroup: false, + label: 'TV_start', + shape: 'stateStart', + padding: 8, + width: 14, + height: 14, + }, + { + id: 'Off', + parentId: 'TV', + isGroup: false, + label: 'Off', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + { + id: 'On', + parentId: 'TV', + isGroup: false, + label: 'On', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + { + id: 'Console', + isGroup: true, + label: 'Console', + shape: 'roundedWithTitle', + dir: 'TB', + padding: 8, + labelBBox: { + width: 67.21875, + height: 24, + }, + }, + { + id: 'Console_start', + parentId: 'Console', + isGroup: false, + label: 'Console_start', + shape: 'stateStart', + padding: 8, + width: 14, + height: 14, + }, + { + id: 'Off2', + parentId: 'Console', + isGroup: false, + label: 'Off2', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + { + id: 'On2', + parentId: 'Console', + isGroup: false, + label: 'On2', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + { + id: 'Playing', + parentId: 'Console', + isGroup: true, + label: 'Playing', + shape: 'roundedWithTitle', + dir: 'TB', + padding: 8, + labelBBox: { + width: 67.21875, + height: 24, + }, + }, + { + id: 'Alive', + parentId: 'Playing', + isGroup: false, + label: 'Alive', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + { + id: 'Dead', + parentId: 'Playing', + isGroup: false, + label: 'Dead', + shape: 'roundedRect', + padding: 8, + width: 136, + height: 40, + }, + ], + edges: [ + { + id: 'edge0', + start: 'root_start', + end: 'TV', + label: '', + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge1', + start: 'TV_start', + end: 'Off', + label: 'Off to start with', + width: 163.234375, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge2', + start: 'On', + end: 'Off', + label: 'Turn off', + width: 76.8125, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge3', + start: 'Off', + end: 'On', + label: 'Turn on', + width: 67.21875, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge4', + start: 'TV', + end: 'Console', + label: '', + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge5', + start: 'Console_start', + end: 'Off2', + label: 'Off to start with', + width: 163.234375, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge6', + start: 'On2', + end: 'Off2', + label: 'Turn off', + width: 76.8125, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge7', + start: 'Off2', + end: 'On2', + label: 'Turn on', + width: 67.21875, + height: 24, + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge8', + start: 'On2', + end: 'Playing', + label: '', + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge9', + start: 'Alive', + end: 'Dead', + label: '', + arrowTypeEnd: 'arrow_barb', + }, + { + id: 'edge10', + start: 'Dead', + end: 'Alive', + label: '', + arrowTypeEnd: 'arrow_barb', + }, + ], +}; + +it('centers the initial transition to TV with defaults and keeps the entry straight', async () => { + mermaidAPI.initialize({}); + const data = { + ...structuredClone(compositeFixture), + direction: 'TB', + config: getConfig(), + } as any; + await runElkLayoutCore(data, { helpers: context, options: { algorithm: 'elk.layered' } } as any); + const tv = data.nodes.find((node: any) => node.id === 'TV'); + const start = data.nodes.find((node: any) => node.id === 'root_start'); + const points = data.edges.find((edge: any) => edge.start === 'root_start').points; + // Frame trimming moves the painted midpoint slightly after ELK routing. + expect(Math.abs(points.at(-1).x - tv.x)).toBeLessThan(2); + expect(start.x).toBeCloseTo(points[0].x, 3); + for (const point of points) { + expect(point.x).toBeCloseTo(points[0].x, 3); + } +}); diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/render.spec.ts b/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/render.spec.ts index 9f88e933973..e137d98a23c 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/render.spec.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/elk/__tests__/render.spec.ts @@ -119,9 +119,9 @@ describe('buildSubgraphLayoutOptions', () => { expect(opts['elk.layered.nodePlacement.strategy']).toBe('BRANDES_KOEPF'); }); - it('defaults nodePlacementAlignment to NONE', () => { + it('defaults nodePlacementAlignment to BALANCED', () => { const opts = buildSubgraphLayoutOptions({}, { mergeEdges: true }, 'layered'); - expect(opts['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('NONE'); + expect(opts['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('BALANCED'); }); it('passes through nodePlacementAlignment from config', () => { @@ -133,11 +133,9 @@ describe('buildSubgraphLayoutOptions', () => { const opts = buildSubgraphLayoutOptions({}, undefined, 'layered'); expect(opts['elk.layered.mergeEdges']).toBeUndefined(); // With no config at all the `default` preset supplies the placement - // strategy. Containers are BRANDES_KOEPF while the root is NETWORK_SIMPLEX: - // network simplex inside a frame produced routes that left a subgraph on - // its bounding-box corner, so containers keep the strategy that does not. + // strategy and alignment for both the root and its containers. expect(opts['elk.layered.nodePlacement.strategy']).toBe('BRANDES_KOEPF'); - expect(opts['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('NONE'); + expect(opts['elk.layered.nodePlacement.bk.fixedAlignment']).toBe('BALANCED'); }); it('lets an explicit strategy beat the preset', () => { @@ -196,9 +194,8 @@ describe('buildSubgraphLayoutOptions', () => { // reach containers too — leaving them on the new strategy would make it a // half-restore that still lays subgraph contents out differently. expect(placement('legacy')).toBe('BRANDES_KOEPF'); - // `default` and `depthFirst` place the ROOT with NETWORK_SIMPLEX but keep - // containers on BRANDES_KOEPF — the two sides are tuned separately on - // purpose, so a change to one must not be assumed to carry to the other. + // Both presets keep container placement on BRANDES_KOEPF even though + // their root placement now differs. expect(placement('depthFirst')).toBe('BRANDES_KOEPF'); expect(placement('default')).toBe('BRANDES_KOEPF'); }); @@ -455,7 +452,7 @@ describe('buildElkGraphFromLayoutData', () => { expect(state.elkGraph.layoutOptions['elk.direction']).toBe('RIGHT'); expect(state.elkGraph.layoutOptions['elk.layered.nodePlacement.bk.fixedAlignment']).toBe( - 'NONE' + 'BALANCED' ); expect(state.elkGraph.children).toHaveLength(2); @@ -1352,16 +1349,15 @@ describe('resolveElkPreset', () => { expect(resolveElkPreset('legacy').cycleBreaking).toBe('GREEDY'); }); - it('keeps depthFirst as a name for what default already is', () => { - // Not a distinct combination — a label, so a diagram can say depth-first - // rather than depend on the default staying put. - expect(resolveElkPreset('depthFirst')).toEqual(resolveElkPreset('default')); - }); - - it('differs from default in cycle breaking alone for modelOrder', () => { - const { cycleBreaking: _a, ...restDefault } = resolveElkPreset('default'); - const { cycleBreaking: _b, ...restModelOrder } = resolveElkPreset('modelOrder'); - expect(restModelOrder).toEqual(restDefault); + it('preserves depthFirst and modelOrder placement independently of default', () => { + for (const name of ['depthFirst', 'modelOrder']) { + expect(resolveElkPreset(name)).toMatchObject({ + layering: 'NETWORK_SIMPLEX', + placement: 'NETWORK_SIMPLEX', + containerPlacement: 'BRANDES_KOEPF', + alignment: 'NONE', + }); + } }); it('falls back to default for an unknown name, including __proto__', () => { diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/elk/elkOptionCatalogue.ts b/packages/mermaid/src/rendering-util/layout-algorithms/elk/elkOptionCatalogue.ts index 93e58ea1b86..63caa61a564 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/elk/elkOptionCatalogue.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/elk/elkOptionCatalogue.ts @@ -79,16 +79,16 @@ export const PLACEMENT_OPTIONS: Record = { // 'elk.layered.considerModelOrder.strategy': 'PREFER_NODES', // order nodes, let edges move // ─── Node placement — the coordinate within the layer ─── // Wired to `elk.nodePlacementStrategy`; uncomment to override that config. - // 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', // our default: balanced - // 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', // ELK default: straight long edges + // 'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX', // balanced; root under modelOrder/depthFirst + // 'elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', // ELK default and ours: straight long edges // 'elk.layered.nodePlacement.strategy': 'LINEAR_SEGMENTS', // keeps chains aligned // 'elk.layered.nodePlacement.strategy': 'SIMPLE', // cheapest, least tidy // Shifts nodes to straighten edges rather than centre them in the layer. // 'elk.layered.nodePlacement.favorStraightEdges': true, // 'elk.layered.nodePlacement.favorStraightEdges': false, // Brandes-Koepf only: which of its four candidate alignments to keep. - // 'elk.layered.nodePlacement.bk.fixedAlignment': 'NONE', // pick the shortest result - // 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', // average all four + // 'elk.layered.nodePlacement.bk.fixedAlignment': 'NONE', // pick the shortest result; named presets + // 'elk.layered.nodePlacement.bk.fixedAlignment': 'BALANCED', // average all four; default preset // 'elk.layered.nodePlacement.bk.fixedAlignment': 'LEFTUP', // 'elk.layered.nodePlacement.bk.fixedAlignment': 'RIGHTUP', // 'elk.layered.nodePlacement.bk.fixedAlignment': 'LEFTDOWN', diff --git a/packages/mermaid/src/rendering-util/layout-algorithms/elk/render.ts b/packages/mermaid/src/rendering-util/layout-algorithms/elk/render.ts index 3a182686984..c3935a904a3 100644 --- a/packages/mermaid/src/rendering-util/layout-algorithms/elk/render.ts +++ b/packages/mermaid/src/rendering-util/layout-algorithms/elk/render.ts @@ -168,7 +168,6 @@ const ARROW_MAP: Record = { arrow_circle: ['none', 'arrow_circle'], double_arrow_circle: ['arrow_circle', 'arrow_circle'], }; -const DEFAULT_NODE_PLACEMENT_ALIGNMENT = 'NONE'; /** * Margin reserved at the ends of each side of a node, so that a port cannot be @@ -390,12 +389,9 @@ export function buildSubgraphLayoutOptions( 'elk.layered.mergeEdges': elkConfig?.mergeEdges, 'elk.layered.nodePlacement.bk.fixedAlignment': - elkConfig?.nodePlacementAlignment ?? DEFAULT_NODE_PLACEMENT_ALIGNMENT, - // Containers place their own children, and the preset says how: by default - // NETWORK_SIMPLEX, which balances a node against all of its neighbours and - // so keeps a group's nodes aligned with each other instead of drifting, - // while the root uses LINEAR_SEGMENTS. `legacy` keeps both on the strategy - // that shipped before, so it still reproduces the old rendering. + elkConfig?.nodePlacementAlignment ?? preset.alignment, + // The preset resolves child placement separately from root placement. + // Named presets retain their previous strategies; explicit options win. // // ONE key, fully qualified. ELK reads `nodePlacement.strategy` and // `elk.layered.nodePlacement.strategy` as the same option, so listing both @@ -807,76 +803,51 @@ function getElkLayoutContext( * was dead this way, and it took a bisect against the raw ELK option to notice. */ /** - * Named combinations of the three options that decide where nodes end up. - * - * Layering picks the column, node placement the coordinate within it, and cycle - * breaking which edges are reversed and therefore which ones detour. They run in - * different phases and do not interact, so a preset is a named triple rather - * than a mode of its own. - * - * An explicit `elk.layeringStrategy` / `nodePlacementStrategy` / - * `cycleBreakingStrategy` beats the preset for that one option — which is why - * `defaultConfig` leaves all three undefined rather than giving them values. + * Presets supply layout choices only when the caller leaves them unspecified. + * Root and container placement are independent, while both use the preset's + * Brandes-Koepf alignment. Named non-default presets retain their earlier layout. */ const ELK_PRESETS: Record< string, - { layering: string; placement: string; containerPlacement: string; cycleBreaking: string } + { + layering: string; + placement: string; + containerPlacement: string; + alignment: string; + cycleBreaking: string; + } > = { - /** - * Network simplex at the root, Brandes-Koepf inside frames, cycles broken - * depth first. - * - * Depth-first cycle breaking gives shorter back edges on graphs that have - * many of them, which is most flowcharts that loop at all. `modelOrder` is - * the same triple with the greedy-model-order breaking this used to carry. - * - * Containers deliberately do NOT follow the root's placement. Network simplex - * inside a frame produced routes that left a subgraph on its bounding-box - * corner, so the two sides are tuned separately: changing one is not a reason - * to change the other, and `legacy` keeps both on the strategy that shipped - * before. - */ + // Balanced Brandes-Koepf centers simple branches and composite-state entries. + // Layering and cycle breaking retain the release defaults. default: { layering: 'NETWORK_SIMPLEX', - placement: 'NETWORK_SIMPLEX', + placement: 'BRANDES_KOEPF', containerPlacement: 'BRANDES_KOEPF', + alignment: 'BALANCED', cycleBreaking: 'DEPTH_FIRST', }, - /** - * What shipped before presets: straighter long edges, less alignment. - * - * `GREEDY`, not `GREEDY_MODEL_ORDER`, is deliberate. The schema advertised - * the latter, but `defaultConfig` never listed `cycleBreakingStrategy`, so it - * reached ELK as undefined and ELK's own default applied. This preset - * reproduces what `develop` actually renders, not what its schema claimed. - * Layering is ELK's default too — `develop` does not wire the option at all. - */ + // Reproduce the layout before presets, including ELK's own greedy cycle + // breaking rather than the greedy-model-order value advertised by the schema. legacy: { layering: 'NETWORK_SIMPLEX', placement: 'BRANDES_KOEPF', containerPlacement: 'BRANDES_KOEPF', + alignment: 'NONE', cycleBreaking: 'GREEDY', }, - /** - * As `default`, but breaks cycles by greedy model order — which reverses the - * edges that disturb declaration order least, at the cost of longer back - * edges. This is the triple `default` named before depth-first took over. - */ modelOrder: { layering: 'NETWORK_SIMPLEX', placement: 'NETWORK_SIMPLEX', containerPlacement: 'BRANDES_KOEPF', + alignment: 'NONE', cycleBreaking: 'GREEDY_MODEL_ORDER', }, - /** - * Kept as a name for what `default` now is, so diagrams that asked for - * depth-first breaking by name keep saying what they mean. Identical to - * `default` on purpose — not a distinct combination. - */ + // Preserve the previous default recipe for callers selecting it by name. depthFirst: { layering: 'NETWORK_SIMPLEX', placement: 'NETWORK_SIMPLEX', containerPlacement: 'BRANDES_KOEPF', + alignment: 'NONE', cycleBreaking: 'DEPTH_FIRST', }, }; @@ -910,7 +881,7 @@ function createRootElkGraph( 'elk.layered.nodePlacement.strategy': data4Layout.config.elk?.nodePlacementStrategy ?? preset.placement, 'elk.layered.nodePlacement.bk.fixedAlignment': - data4Layout.config.elk?.nodePlacementAlignment ?? DEFAULT_NODE_PLACEMENT_ALIGNMENT, + data4Layout.config.elk?.nodePlacementAlignment ?? preset.alignment, 'elk.layered.mergeEdges': data4Layout.config.elk?.mergeEdges, 'elk.direction': 'DOWN', 'spacing.baseValue': 40, diff --git a/packages/mermaid/src/schemas/config.schema.yaml b/packages/mermaid/src/schemas/config.schema.yaml index 96bf2eb96d3..e3b3b57a03c 100644 --- a/packages/mermaid/src/schemas/config.schema.yaml +++ b/packages/mermaid/src/schemas/config.schema.yaml @@ -115,7 +115,9 @@ properties: nodePlacementAlignment: description: | Elk specific option affecting Brandes-Koepf node placement alignment. - NONE picks the alignment with the smallest height. + BALANCED combines the four directional alignments; NONE picks the + smallest result. Defaults to BALANCED for the default preset and NONE + for named non-default presets. type: string enum: - NONE @@ -124,34 +126,33 @@ properties: - RIGHTUP - RIGHTDOWN - BALANCED - default: NONE + # Default comes from `elk.preset` unless set explicitly. preset: description: | - Named combination of the three options that decide where nodes end up: - layering strategy, node placement strategy and cycle breaking strategy. - They belong to different phases of the layout, so a preset is simply a - named triple rather than a mode with behaviour of its own. + Named combination of layering, node placement, placement alignment + and cycle breaking options. Explicit options override the preset for + that option; the remaining preset values still apply. - `default` — network simplex layering and placement with depth-first - cycle breaking at the top level, and Brandes-Koepf placement inside - subgraphs. Depth-first breaking gives shorter back edges on graphs - that loop. + `default` — network simplex layering, balanced Brandes-Koepf placement + at the top level and inside subgraphs, and depth-first cycle breaking. + Balanced placement favors centered branches and composite-state entries, + sometimes at the cost of a wider or taller drawing. It does not + guarantee that every edge attaches to the center of its target. - `legacy` — what shipped before presets existed: Brandes-Koepf placement, - which straightens long edges at the cost of that alignment, with ELK's - own greedy cycle breaking. Reproduces the rendering of earlier - versions rather than the defaults their schema advertised. + `legacy` — Brandes-Koepf placement with NONE alignment and ELK's own + greedy cycle breaking. Reproduces the rendering before presets existed. - `modelOrder` — as `default`, but breaks cycles by greedy model order, - which disturbs declaration order least at the cost of longer back - edges. This is the combination `default` named previously. + `modelOrder` — network simplex layering and top-level placement, + Brandes-Koepf placement inside subgraphs, NONE alignment, and greedy + model-order cycle breaking, which favors declaration order. - `depthFirst` — a name for what `default` already is, for diagrams that - would rather say depth-first than rely on the default. + `depthFirst` — the previous default: network simplex layering and + top-level placement, Brandes-Koepf placement inside subgraphs, NONE + alignment, and depth-first cycle breaking. - Setting `layeringStrategy`, `nodePlacementStrategy` or - `cycleBreakingStrategy` explicitly overrides the preset for that one - option; the rest of the preset still applies. + Setting `layeringStrategy`, `nodePlacementStrategy`, + `nodePlacementAlignment` or `cycleBreakingStrategy` explicitly overrides + the preset for that option, including an explicit NONE alignment. type: string enum: - default