@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.
- package/README.md +15 -1
- package/dist/index.cjs +52 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -4
- package/dist/index.d.ts +23 -4
- package/dist/index.js +890 -850
- package/dist/index.js.map +1 -1
- package/package.json +14 -6
- package/src/c4/arial-widths.ts +0 -59
- package/src/c4/layout.ts +0 -680
- package/src/c4/metrics.ts +0 -261
- package/src/c4/renderer.ts +0 -366
- package/src/class/layout.ts +0 -525
- package/src/class/renderer.ts +0 -669
- package/src/edge-curves.ts +0 -205
- package/src/elk-instance.ts +0 -292
- package/src/er/layout.ts +0 -216
- package/src/er/renderer.ts +0 -516
- package/src/generated/mono-font-subset.ts +0 -32
- package/src/index.ts +0 -310
- package/src/layout-engine/back-edges.ts +0 -73
- package/src/layout-engine/compound-flat.ts +0 -916
- package/src/layout-engine/constants.ts +0 -19
- package/src/layout-engine/edge-bundling.ts +0 -458
- package/src/layout-engine/elk-adapter-utils.ts +0 -81
- package/src/layout-engine/elk-graph-builder.ts +0 -245
- package/src/layout-engine/from-elk.ts +0 -767
- package/src/layout-engine/inner-edges.ts +0 -329
- package/src/layout-engine/jog-straightening.ts +0 -52
- package/src/layout-engine/layer-alignment.ts +0 -174
- package/src/layout-engine/layout-hints.ts +0 -27
- package/src/layout-engine/to-elk.ts +0 -855
- package/src/layout-engine.ts +0 -115
- package/src/pie/layout.ts +0 -234
- package/src/pie/renderer.ts +0 -161
- package/src/registry.ts +0 -441
- package/src/renderer.ts +0 -1510
- package/src/resolve-colors.ts +0 -339
- package/src/sequence/layout.ts +0 -766
- package/src/sequence/renderer.ts +0 -687
- package/src/shape-clipping.ts +0 -197
- package/src/styles.ts +0 -145
- package/src/title-gaps.ts +0 -161
- package/src/xychart/layout.ts +0 -727
- package/src/xychart/renderer.ts +0 -676
package/src/layout-engine.ts
DELETED
|
@@ -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
|
-
}
|
package/src/pie/renderer.ts
DELETED
|
@@ -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
|
-
}
|