@zombie-mermaid/svg-renderer 4.2.3 → 5.0.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.
Files changed (45) hide show
  1. package/README.md +15 -1
  2. package/dist/index.cjs +52 -52
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +23 -4
  5. package/dist/index.d.ts +23 -4
  6. package/dist/index.js +890 -850
  7. package/dist/index.js.map +1 -1
  8. package/package.json +14 -6
  9. package/src/c4/arial-widths.ts +0 -59
  10. package/src/c4/layout.ts +0 -680
  11. package/src/c4/metrics.ts +0 -261
  12. package/src/c4/renderer.ts +0 -366
  13. package/src/class/layout.ts +0 -525
  14. package/src/class/renderer.ts +0 -669
  15. package/src/edge-curves.ts +0 -205
  16. package/src/elk-instance.ts +0 -292
  17. package/src/er/layout.ts +0 -216
  18. package/src/er/renderer.ts +0 -516
  19. package/src/generated/mono-font-subset.ts +0 -32
  20. package/src/index.ts +0 -310
  21. package/src/layout-engine/back-edges.ts +0 -73
  22. package/src/layout-engine/compound-flat.ts +0 -916
  23. package/src/layout-engine/constants.ts +0 -19
  24. package/src/layout-engine/edge-bundling.ts +0 -458
  25. package/src/layout-engine/elk-adapter-utils.ts +0 -81
  26. package/src/layout-engine/elk-graph-builder.ts +0 -245
  27. package/src/layout-engine/from-elk.ts +0 -767
  28. package/src/layout-engine/inner-edges.ts +0 -329
  29. package/src/layout-engine/jog-straightening.ts +0 -52
  30. package/src/layout-engine/layer-alignment.ts +0 -174
  31. package/src/layout-engine/layout-hints.ts +0 -27
  32. package/src/layout-engine/to-elk.ts +0 -855
  33. package/src/layout-engine.ts +0 -115
  34. package/src/pie/layout.ts +0 -234
  35. package/src/pie/renderer.ts +0 -161
  36. package/src/registry.ts +0 -441
  37. package/src/renderer.ts +0 -1510
  38. package/src/resolve-colors.ts +0 -339
  39. package/src/sequence/layout.ts +0 -766
  40. package/src/sequence/renderer.ts +0 -687
  41. package/src/shape-clipping.ts +0 -197
  42. package/src/styles.ts +0 -145
  43. package/src/title-gaps.ts +0 -161
  44. package/src/xychart/layout.ts +0 -727
  45. package/src/xychart/renderer.ts +0 -676
@@ -1,115 +0,0 @@
1
- /**
2
- * Layout engine for zombie-mermaid (ELK.js based).
3
- *
4
- * Converts MermaidGraph to ELK's JSON format, runs layout, and converts
5
- * the result back to PositionedGraph. This is the core layout engine used
6
- * by all graph-based diagram types (flowcharts, state, ER, class).
7
- *
8
- * ELK (Eclipse Layout Kernel) features:
9
- * - Native orthogonal edge routing (no post-processing needed)
10
- * - Proper handling of compound nodes (subgraphs)
11
- * - Support for disconnected graphs
12
- * - Direction overrides per subgraph
13
- * - Sophisticated algorithms for complex graphs
14
- *
15
- * Uses elk.bundled.js (pure synchronous JS, no WASM/Workers).
16
- * Safe for Electron, Node, and browser environments.
17
- *
18
- * This file is the public entry point (layoutGraphSync, convertToElkFormat).
19
- * The implementation is split across src/layout-engine/ by pipeline stage:
20
- * - to-elk.ts MermaidGraph → ELK JSON input
21
- * - from-elk.ts ELK result → PositionedGraph, plus shape clipping
22
- * - layer-alignment.ts post-process: snap same-layer nodes to a uniform position
23
- * - edge-bundling.ts post-process: merge fan-out/fan-in edges into shared trunks
24
- * See packages/svg-renderer/src/layout-engine/constants.ts for the shared DEFAULTS used across stages.
25
- */
26
-
27
- import type { ElkNode } from 'elkjs'
28
- import type {
29
- MermaidGraph,
30
- PositionedGraph,
31
- FlowchartRenderOptions,
32
- } from '@zombie-mermaid/core'
33
- import { withoutOwnClusterEdges } from '@zombie-mermaid/core'
34
- import { elkLayoutSync } from './elk-instance.ts'
35
- import { resolveFontSizes } from './styles.ts'
36
- import { DEFAULTS } from './layout-engine/constants.ts'
37
- import { mermaidToElk } from './layout-engine/to-elk.ts'
38
- import { elkToPositioned } from './layout-engine/from-elk.ts'
39
- import {
40
- canLayOutFlat,
41
- layoutCompoundFlat,
42
- } from './layout-engine/compound-flat.ts'
43
- import type { LayoutHints } from './layout-engine/layout-hints.ts'
44
-
45
- // ============================================================================
46
- // Public API
47
- // ============================================================================
48
-
49
- /**
50
- * Lay out a parsed MermaidGraph using ELK.js (synchronous).
51
- * Returns a fully positioned graph ready for rendering.
52
- */
53
- export function layoutGraphSync(
54
- graph: MermaidGraph,
55
- options: FlowchartRenderOptions = {},
56
- ): PositionedGraph {
57
- return layoutWithHints(graph, options)
58
- }
59
-
60
- /** One layout run, with the hints the compound layout gives it. */
61
- function layoutWithHints(
62
- graph: MermaidGraph,
63
- options: FlowchartRenderOptions,
64
- hints?: LayoutHints,
65
- ): PositionedGraph {
66
- const opts = {
67
- ...DEFAULTS,
68
- ...options,
69
- fontSizes: resolveFontSizes(options.fontSizes),
70
- }
71
- const elkGraph = mermaidToElk(graph, opts, hints)
72
- const result = elkLayoutSync(elkGraph, options.layoutCache)
73
- return elkToPositioned(result, graph, opts.mergeEdges)
74
- }
75
-
76
- /**
77
- * Lay out a flowchart, with its subgraphs arranged the way mermaid.js arranges
78
- * them (see `layout-engine/compound-flat.ts`). Anything that layout doesn't
79
- * handle, or can't make fit, is laid out by `layoutGraphSync`, so this is a
80
- * drop-in for it wherever the graph is a flowchart. Other diagram types that
81
- * share the engine (state, architecture) keep using `layoutGraphSync`.
82
- */
83
- export function layoutFlowchartSync(
84
- rawGraph: MermaidGraph,
85
- options: FlowchartRenderOptions = {},
86
- ): PositionedGraph {
87
- // Real mermaid.js draws no line for an edge between a node and a subgraph
88
- // containing it (#1310), so it is not laid out or rendered here either.
89
- const graph = withoutOwnClusterEdges(rawGraph)
90
- if (canLayOutFlat(graph)) {
91
- const fontSizes = resolveFontSizes(options.fontSizes)
92
- const flat = layoutCompoundFlat(
93
- graph,
94
- (g, hints) => layoutWithHints(g, options, hints),
95
- fontSizes,
96
- )
97
- if (flat) return flat
98
- }
99
- return layoutGraphSync(graph, options)
100
- }
101
-
102
- /**
103
- * Convert MermaidGraph to ELK format (for benchmarking conversion overhead).
104
- */
105
- export function convertToElkFormat(
106
- graph: MermaidGraph,
107
- options: FlowchartRenderOptions = {},
108
- ): ElkNode {
109
- const opts = {
110
- ...DEFAULTS,
111
- ...options,
112
- fontSizes: resolveFontSizes(options.fontSizes),
113
- }
114
- return mermaidToElk(graph, opts)
115
- }
package/src/pie/layout.ts DELETED
@@ -1,234 +0,0 @@
1
- // ============================================================================
2
- // Pie chart layout
3
- //
4
- // Turns a parsed PieChart into absolute SVG geometry, following Mermaid's own
5
- // renderer (packages/mermaid/src/diagrams/pie/pieRenderer.ts, default config:
6
- // textPosition 0.75, no donut hole, legend on the right):
7
- //
8
- // - a 450 x 450 frame with a 40px margin, so the pie radius is 185 and its
9
- // centre sits at (225, 225); a 2px outline circle of radius 186 around it
10
- // - slices in source order, clockwise from 12 o'clock (d3.pie with
11
- // sort(null)); slices under 1% of the total are left out and the rest
12
- // share the full circle between them
13
- // - each drawn slice labelled with `toFixed(0)` of its share of the
14
- // *whole* total (omitted slices included), at 0.75 of the radius
15
- // - the title centred over the pie with its baseline 25px from the top
16
- // - a legend row for every slice, omitted ones included: an 18px swatch
17
- // and the label (or `label [value]` with showData), rows 22px apart,
18
- // vertically centred on the pie, starting 216px right of its centre
19
- // - width = pie + margin + swatch + gap + longest legend text, widened to
20
- // fit a title wider than that
21
- //
22
- // Two places differ from Mermaid on purpose, both where Mermaid's output is
23
- // broken rather than a style choice:
24
- // - A legend taller than the 450px frame (more than ~20 rows) grows the
25
- // viewBox instead of being clipped at the top and bottom.
26
- // - A chart with no slices keeps the pie's own width. Mermaid's width
27
- // formula takes Math.max() of an empty list there (-Infinity), which
28
- // leaves a viewBox that crops the circle in half.
29
- //
30
- // Lengths are kept from Mermaid as-is. Text widths come from this library's
31
- // own metrics for its own font, not Mermaid's "trebuchet ms".
32
- // ============================================================================
33
-
34
- import type {
35
- PieChart,
36
- PositionedPieChart,
37
- PositionedPieLegendItem,
38
- PositionedPieSlice,
39
- } from '@zombie-mermaid/mermaid-parser'
40
- import { estimateTextWidth } from '../styles.ts'
41
-
42
- /** Mermaid pieRenderer.ts constants (default config and default theme). */
43
- export const PIE = {
44
- /** Frame height; the pie's own width equals it. */
45
- height: 450,
46
- margin: 40,
47
- legendRectSize: 18,
48
- legendSpacing: 4,
49
- /** `pie.textPosition` default: label distance as a fraction of the radius. */
50
- textPosition: 0.75,
51
- /** `pieOuterStrokeWidth`; the outline circle sits half of it outside the pie. */
52
- outerStrokeWidth: 2,
53
- /** `pieTitleTextSize` */
54
- titleFontSize: 25,
55
- /** `pieSectionTextSize` */
56
- sectionFontSize: 17,
57
- /** `pieLegendTextSize` */
58
- legendFontSize: 17,
59
- fontWeight: 400,
60
- /** Mermaid's `pie1`..`pie12` scale wraps after this many colours. */
61
- paletteSize: 12,
62
- /** Slices below this percentage of the total are not drawn. */
63
- minPercent: 1,
64
- } as const
65
-
66
- const TAU = Math.PI * 2
67
-
68
- /** Round to two decimals for compact path data. */
69
- function r(n: number): string {
70
- return String(Math.round(n * 100) / 100)
71
- }
72
-
73
- /** Point on a circle, angle in radians clockwise from 12 o'clock (d3's convention). */
74
- function polar(
75
- cx: number,
76
- cy: number,
77
- radius: number,
78
- angle: number,
79
- ): { x: number; y: number } {
80
- return { x: cx + radius * Math.sin(angle), y: cy - radius * Math.cos(angle) }
81
- }
82
-
83
- /** Wedge path, or a full circle when the slice covers the whole pie. */
84
- function slicePath(
85
- cx: number,
86
- cy: number,
87
- radius: number,
88
- start: number,
89
- end: number,
90
- ): string {
91
- if (end - start >= TAU - 1e-9) {
92
- return (
93
- `M${r(cx)},${r(cy - radius)}` +
94
- `A${r(radius)},${r(radius)},0,1,1,${r(cx)},${r(cy + radius)}` +
95
- `A${r(radius)},${r(radius)},0,1,1,${r(cx)},${r(cy - radius)}Z`
96
- )
97
- }
98
- const p0 = polar(cx, cy, radius, start)
99
- const p1 = polar(cx, cy, radius, end)
100
- const large = end - start > Math.PI ? 1 : 0
101
- return (
102
- `M${r(p0.x)},${r(p0.y)}` +
103
- `A${r(radius)},${r(radius)},0,${large},1,${r(p1.x)},${r(p1.y)}` +
104
- `L${r(cx)},${r(cy)}Z`
105
- )
106
- }
107
-
108
- /** Legend row text, as Mermaid builds it: `String(value)`, no number formatting. */
109
- function legendText(label: string, value: number, showData: boolean): string {
110
- return showData ? `${label} [${String(value)}]` : label
111
- }
112
-
113
- /** Lay out a parsed pie chart. Options are accepted for registry parity; none apply. */
114
- export function layoutPieChart(
115
- chart: PieChart,
116
- _options: unknown = {},
117
- ): PositionedPieChart {
118
- const height = PIE.height
119
- const pieWidth = height
120
- const radius = Math.min(pieWidth, height) / 2 - PIE.margin
121
- // Everything below is first computed in Mermaid's frame (pie centre at
122
- // (225, 225)), then shifted so the viewBox starts at (0, 0).
123
- const cx0 = pieWidth / 2
124
- const cy0 = height / 2
125
-
126
- const total = chart.slices.reduce((sum, s) => sum + s.value, 0)
127
-
128
- // createPieArcs(): keep slices at or above 1% of the total. A zero total
129
- // gives NaN here, which drops every slice, as in Mermaid. (Mermaid filters
130
- // a second time on `toFixed(0) !== '0'`, which nothing at or above 1% can
131
- // fail, so that step is not repeated.)
132
- const drawn = chart.slices
133
- .map((slice, index) => ({ slice, colorIndex: index % PIE.paletteSize }))
134
- .filter(({ slice }) => (slice.value / total) * 100 >= PIE.minPercent)
135
- // d3.pie() spreads the full circle over the slices it is given.
136
- const drawnTotal = drawn.reduce((sum, d) => sum + d.slice.value, 0)
137
-
138
- // Legend geometry (Mermaid's 'right' position).
139
- const rowHeight = PIE.legendRectSize + PIE.legendSpacing
140
- const rowCount = chart.slices.length
141
- const legendOffset = (rowHeight * rowCount) / 2
142
- const legendX0 = cx0 + 12 * PIE.legendRectSize
143
- const legendTexts = chart.slices.map((s) =>
144
- legendText(s.label, s.value, chart.showData),
145
- )
146
- const longestText = Math.max(
147
- 0,
148
- ...legendTexts.map((t) =>
149
- estimateTextWidth(t, PIE.legendFontSize, PIE.fontWeight),
150
- ),
151
- )
152
-
153
- const chartAndLegendWidth =
154
- rowCount > 0
155
- ? pieWidth +
156
- PIE.margin +
157
- PIE.legendRectSize +
158
- PIE.legendSpacing +
159
- longestText
160
- : pieWidth + PIE.margin
161
-
162
- const titleText = chart.title ?? ''
163
- const titleWidth =
164
- titleText.length > 0
165
- ? estimateTextWidth(titleText, PIE.titleFontSize, PIE.fontWeight)
166
- : 0
167
- const titleLeft = cx0 - titleWidth / 2
168
- const titleRight = cx0 + titleWidth / 2
169
-
170
- const minX = Math.min(0, titleLeft)
171
- const maxX = Math.max(chartAndLegendWidth, titleRight)
172
- const legendTop = cy0 - legendOffset
173
- const legendBottom =
174
- cy0 + (rowCount - 1) * rowHeight - legendOffset + PIE.legendRectSize
175
- const minY = rowCount > 0 ? Math.min(0, legendTop) : 0
176
- const maxY = rowCount > 0 ? Math.max(height, legendBottom) : height
177
-
178
- const dx = -minX
179
- const dy = -minY
180
- const cx = cx0 + dx
181
- const cy = cy0 + dy
182
-
183
- const slices: PositionedPieSlice[] = []
184
- let angle = 0
185
- const labelRadius = radius * PIE.textPosition
186
- for (const { slice, colorIndex } of drawn) {
187
- const start = angle
188
- const end = angle + (slice.value / drawnTotal) * TAU
189
- angle = end
190
- const label = polar(cx, cy, labelRadius, (start + end) / 2)
191
- slices.push({
192
- label: slice.label,
193
- value: slice.value,
194
- colorIndex,
195
- startAngle: start,
196
- endAngle: end,
197
- path: slicePath(cx, cy, radius, start, end),
198
- percentText: `${((slice.value / total) * 100).toFixed(0)}%`,
199
- labelX: label.x,
200
- labelY: label.y,
201
- })
202
- }
203
-
204
- const legend: PositionedPieLegendItem[] = legendTexts.map((text, index) => {
205
- const x = legendX0 + dx
206
- const y = cy0 + index * rowHeight - legendOffset + dy
207
- return {
208
- text,
209
- colorIndex: index % PIE.paletteSize,
210
- x,
211
- y,
212
- size: PIE.legendRectSize,
213
- textX: x + PIE.legendRectSize + PIE.legendSpacing,
214
- textY: y + PIE.legendRectSize - PIE.legendSpacing,
215
- }
216
- })
217
-
218
- return {
219
- width: Math.ceil(maxX - minX),
220
- height: Math.ceil(maxY - minY),
221
- cx,
222
- cy,
223
- radius,
224
- outerRadius: radius + PIE.outerStrokeWidth / 2,
225
- title:
226
- titleText.length > 0
227
- ? { text: titleText, x: cx, y: cy - (height - 50) / 2 }
228
- : undefined,
229
- slices,
230
- legend,
231
- accTitle: chart.accTitle,
232
- accDescr: chart.accDescr,
233
- }
234
- }
@@ -1,161 +0,0 @@
1
- // ============================================================================
2
- // Pie chart SVG renderer
3
- //
4
- // Draws a PositionedPieChart (see ./layout.ts) in Mermaid's own structure and
5
- // paint order: outline circle, slices, percentage labels, title, legend.
6
- //
7
- // Styling follows Mermaid's pieStyles.ts and default theme, mapped onto this
8
- // library's CSS custom properties so every theme (and live theme switching
9
- // through `--bg`/`--fg`/`--accent`) applies:
10
- //
11
- // Mermaid here
12
- // pie1..pie12 --pie-color-0..11: the theme accent, then
13
- // getSeriesColor() shades of it (the same
14
- // palette xychart series use)
15
- // slice opacity 0.7 opacity: 0.7 (on fill and stroke, as there)
16
- // pieStrokeColor black, 2px var(--fg), 2px
17
- // pieOuterStrokeColor black, 2px var(--fg), 2px
18
- // title 25px, legend/labels 17px same sizes, regular weight
19
- // text colours (black / #333) var(--_text)
20
- //
21
- // The legend swatch is filled and stroked with the slice colour at full
22
- // opacity, as in Mermaid. There are no tooltips or hover effects: Mermaid
23
- // draws none by default (`highlightSlice` is off), so `interactivity` does
24
- // not change the output.
25
- // ============================================================================
26
-
27
- import type { PositionedPieChart } from '@zombie-mermaid/mermaid-parser'
28
- import type { DiagramColors, SvgEmitOptions } from '@zombie-mermaid/core'
29
- import {
30
- svgOpenTag,
31
- buildStyleBlock,
32
- styleOpenTag,
33
- escapeXml,
34
- escapeAttr,
35
- } from '@zombie-mermaid/core'
36
- import {
37
- getSeriesColor,
38
- CHART_ACCENT_FALLBACK,
39
- } from '@zombie-mermaid/mermaid-parser'
40
- import { withDataSrc } from '../renderer.ts'
41
- import { PIE } from './layout.ts'
42
-
43
- function r(n: number): string {
44
- return String(Math.round(n * 100) / 100)
45
- }
46
-
47
- /** CSS value for palette slot `index`: slot 0 follows `--accent` live. */
48
- export function pieColorValue(
49
- index: number,
50
- accent: string | undefined,
51
- bg: string,
52
- ): string {
53
- if (index === 0) return `var(--accent, ${CHART_ACCENT_FALLBACK})`
54
- return getSeriesColor(index, accent ?? CHART_ACCENT_FALLBACK, bg)
55
- }
56
-
57
- function pieStyles(
58
- chart: PositionedPieChart,
59
- colors: DiagramColors,
60
- nonce?: string,
61
- ): string {
62
- const used = new Set<number>()
63
- for (const s of chart.slices) used.add(s.colorIndex)
64
- for (const l of chart.legend) used.add(l.colorIndex)
65
- const indices = [...used].sort((a, b) => a - b)
66
-
67
- const vars = indices.map(
68
- (i) =>
69
- ` --pie-color-${i}: ${pieColorValue(i, colors.accent, colors.bg)};`,
70
- )
71
- const rules = indices.map(
72
- (i) =>
73
- ` .pie-color-${i} { fill: var(--pie-color-${i}); }\n` +
74
- ` rect.pie-color-${i} { stroke: var(--pie-color-${i}); }`,
75
- )
76
- const varsBlock = vars.length > 0 ? `\n svg {\n${vars.join('\n')}\n }` : ''
77
-
78
- return `${styleOpenTag(nonce)}
79
- .pie-outer { fill: none; stroke: var(--fg); stroke-width: ${PIE.outerStrokeWidth}px; }
80
- .pie-slice { stroke: var(--fg); stroke-width: 2px; opacity: 0.7; }
81
- .pie-slice-label { fill: var(--_text); font-size: ${PIE.sectionFontSize}px; font-weight: ${PIE.fontWeight}; }
82
- .pie-title { fill: var(--_text); font-size: ${PIE.titleFontSize}px; font-weight: ${PIE.fontWeight}; }
83
- .pie-legend-text { fill: var(--_text); font-size: ${PIE.legendFontSize}px; font-weight: ${PIE.fontWeight}; }${varsBlock}
84
- ${rules.join('\n')}
85
- </style>`
86
- }
87
-
88
- /**
89
- * Render a positioned pie chart as an SVG string.
90
- *
91
- * The accessible name is `title` (from `options.title`) when given,
92
- * otherwise the chart's own `accTitle`; `accDescr` becomes the root
93
- * `<desc>`. Both are dropped when `decorative` is set — see svgOpenTag() in
94
- * packages/core/src/theme.ts.
95
- */
96
- export function renderPieSvg(
97
- chart: PositionedPieChart,
98
- colors: DiagramColors,
99
- font: string = 'Inter',
100
- transparent: boolean = false,
101
- embedSource?: string,
102
- title?: string,
103
- decorative?: boolean,
104
- emit: SvgEmitOptions = {},
105
- ): string {
106
- const parts: string[] = []
107
-
108
- parts.push(
109
- withDataSrc(
110
- svgOpenTag(
111
- chart.width,
112
- chart.height,
113
- colors,
114
- transparent,
115
- title ?? chart.accTitle,
116
- decorative,
117
- undefined,
118
- emit.styleAttribute,
119
- chart.accDescr,
120
- ),
121
- embedSource,
122
- ),
123
- )
124
- parts.push(buildStyleBlock(font, false, emit.nonce))
125
- parts.push(pieStyles(chart, colors, emit.nonce))
126
-
127
- parts.push(
128
- `<circle cx="${r(chart.cx)}" cy="${r(chart.cy)}" r="${r(chart.outerRadius)}" class="pie-outer"/>`,
129
- )
130
-
131
- for (const s of chart.slices) {
132
- parts.push(
133
- `<path d="${s.path}" class="pie-slice pie-color-${s.colorIndex}"` +
134
- ` data-label="${escapeAttr(s.label)}" data-value="${s.value}"/>`,
135
- )
136
- }
137
-
138
- for (const s of chart.slices) {
139
- parts.push(
140
- `<text x="${r(s.labelX)}" y="${r(s.labelY)}" text-anchor="middle" class="pie-slice-label">${escapeXml(s.percentText)}</text>`,
141
- )
142
- }
143
-
144
- if (chart.title) {
145
- parts.push(
146
- `<text x="${r(chart.title.x)}" y="${r(chart.title.y)}" text-anchor="middle" class="pie-title">${escapeXml(chart.title.text)}</text>`,
147
- )
148
- }
149
-
150
- for (const item of chart.legend) {
151
- parts.push(
152
- `<g class="pie-legend">` +
153
- `<rect x="${r(item.x)}" y="${r(item.y)}" width="${item.size}" height="${item.size}" class="pie-legend-swatch pie-color-${item.colorIndex}"/>` +
154
- `<text x="${r(item.textX)}" y="${r(item.textY)}" class="pie-legend-text">${escapeXml(item.text)}</text>` +
155
- `</g>`,
156
- )
157
- }
158
-
159
- parts.push('</svg>')
160
- return parts.join('\n')
161
- }