@zombie-mermaid/svg-renderer 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +59 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -1
- package/dist/index.d.ts +51 -1
- package/dist/index.js +1400 -1671
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/flowchart-parallel-edge-lanes-1308.test.ts +168 -0
- package/src/__tests__/pie-layout.test.ts +215 -0
- package/src/__tests__/pie-svg.test.ts +194 -0
- package/src/__tests__/svg-edge-to-own-cluster-1310.test.ts +32 -0
- package/src/class/renderer.ts +2 -2
- package/src/er/renderer.ts +2 -2
- package/src/generated/mono-font-subset.ts +32 -0
- package/src/index.ts +2 -0
- package/src/layout-engine/edge-bundling.ts +79 -0
- package/src/layout-engine/to-elk.ts +15 -5
- package/src/layout-engine.ts +5 -1
- package/src/pie/layout.ts +234 -0
- package/src/pie/renderer.ts +161 -0
- package/src/registry.ts +31 -1
- package/src/styles.ts +28 -1
- package/src/title-gaps.ts +17 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED FILE — do not edit by hand.
|
|
3
|
+
*
|
|
4
|
+
* Regenerate with `pnpm run build:mono-font`
|
|
5
|
+
* (scripts/build-mono-font-subset.ts), which subsets
|
|
6
|
+
* third_party/fonts/jetbrains-mono-nl/JetBrainsMonoNL-Regular.ttf down to
|
|
7
|
+
* Basic Latin + Latin-1 Supplement — the self-hosted, embeddable
|
|
8
|
+
* `@font-face` packages/svg-renderer/src/styles.ts's `buildSvgStyleBlock()` inlines
|
|
9
|
+
* into an SVG's own `<style>` for its `.mono` rule (class-diagram method
|
|
10
|
+
* signatures, ER-diagram attribute types), replacing a Google Fonts CDN
|
|
11
|
+
* `@import` this library's SVG output previously depended on by default
|
|
12
|
+
* (#1061). See that script's header comment for why this subset is
|
|
13
|
+
* deliberately narrower than the site's own (demo/components/generated/
|
|
14
|
+
* mono-font-subset.ts).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The self-hosted family name every consumer's font stack should list
|
|
18
|
+
* first — matches this subset's own `name` table entry. */
|
|
19
|
+
export const MONO_FONT_FAMILY = 'JetBrains Mono NL'
|
|
20
|
+
|
|
21
|
+
/** The `@font-face` rule embedding the subset as a base64 `woff2` data
|
|
22
|
+
* URI — no separate network request, no page-depth-relative path to get
|
|
23
|
+
* wrong (see the script header comment for why data-URI over a static
|
|
24
|
+
* asset file). */
|
|
25
|
+
export const MONO_FONT_FACE_CSS = `@font-face {
|
|
26
|
+
font-family: 'JetBrains Mono NL';
|
|
27
|
+
font-style: normal;
|
|
28
|
+
font-weight: 400;
|
|
29
|
+
font-display: swap;
|
|
30
|
+
src: url(data:font/woff2;base64,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) format('woff2');
|
|
31
|
+
unicode-range: U+0020-007E, U+00A0-00FF;
|
|
32
|
+
}`
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,8 @@ export * from './sequence/layout.ts'
|
|
|
62
62
|
export * from './sequence/renderer.ts'
|
|
63
63
|
export * from './xychart/layout.ts'
|
|
64
64
|
export * from './xychart/renderer.ts'
|
|
65
|
+
export * from './pie/layout.ts'
|
|
66
|
+
export * from './pie/renderer.ts'
|
|
65
67
|
|
|
66
68
|
import { decodeXML } from 'entities'
|
|
67
69
|
import type {
|
|
@@ -123,6 +123,45 @@ function adjustJunctionForGroups(
|
|
|
123
123
|
return crossingGroup.y - GAP // TD
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
/*
|
|
127
|
+
* How far an edge end may sit from a node's side and still count as attached
|
|
128
|
+
* to that side, in px (ELK routes to the box edge; alignment can shift it a
|
|
129
|
+
* little).
|
|
130
|
+
*/
|
|
131
|
+
const SIDE_TOLERANCE = 2
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* True when an edge outside the bundle also attaches to `hub` on the side the
|
|
135
|
+
* bundle would use. Bundling collapses the group onto the side's midpoint, so
|
|
136
|
+
* an outside edge ending there (a back edge returning to the node, say) would
|
|
137
|
+
* land on the trunk and read as part of it. ELK's own lanes keep them apart,
|
|
138
|
+
* so that group is left as routed.
|
|
139
|
+
*
|
|
140
|
+
* `side` is the coordinate of the hub's side along the flow axis and
|
|
141
|
+
* `horizontal` says which axis that is (x for a left-to-right graph).
|
|
142
|
+
*/
|
|
143
|
+
function hasOutsideEdgeOnSide(
|
|
144
|
+
edges: PositionedEdge[],
|
|
145
|
+
bundleEdges: Set<PositionedEdge>,
|
|
146
|
+
hubId: string,
|
|
147
|
+
side: number,
|
|
148
|
+
horizontal: boolean,
|
|
149
|
+
): boolean {
|
|
150
|
+
for (const e of edges) {
|
|
151
|
+
if (bundleEdges.has(e) || e.source === e.target) continue
|
|
152
|
+
const end =
|
|
153
|
+
e.source === hubId
|
|
154
|
+
? e.points[0]
|
|
155
|
+
: e.target === hubId
|
|
156
|
+
? e.points[e.points.length - 1]
|
|
157
|
+
: undefined
|
|
158
|
+
if (!end) continue
|
|
159
|
+
if (Math.abs((horizontal ? end.x : end.y) - side) <= SIDE_TOLERANCE)
|
|
160
|
+
return true
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
126
165
|
/**
|
|
127
166
|
* Bundle fan-out and fan-in edge paths so they share a common trunk segment.
|
|
128
167
|
*
|
|
@@ -193,6 +232,16 @@ export function bundleEdgePaths(
|
|
|
193
232
|
if (isHorizontal) {
|
|
194
233
|
const exitX = isLR ? source.x + source.width : source.x
|
|
195
234
|
const exitY = srcCY
|
|
235
|
+
if (
|
|
236
|
+
hasOutsideEdgeOnSide(
|
|
237
|
+
edges,
|
|
238
|
+
new Set(targets.map((t) => t.edge)),
|
|
239
|
+
sourceId,
|
|
240
|
+
exitX,
|
|
241
|
+
true,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
continue
|
|
196
245
|
|
|
197
246
|
const nearestX = isLR
|
|
198
247
|
? Math.min(...targets.map((t) => t.node.x))
|
|
@@ -229,6 +278,16 @@ export function bundleEdgePaths(
|
|
|
229
278
|
} else {
|
|
230
279
|
const exitX = srcCX
|
|
231
280
|
const exitY = isBT ? source.y : source.y + source.height
|
|
281
|
+
if (
|
|
282
|
+
hasOutsideEdgeOnSide(
|
|
283
|
+
edges,
|
|
284
|
+
new Set(targets.map((t) => t.edge)),
|
|
285
|
+
sourceId,
|
|
286
|
+
exitY,
|
|
287
|
+
false,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
232
291
|
|
|
233
292
|
const nearestY = isBT
|
|
234
293
|
? Math.max(...targets.map((t) => t.node.y + t.node.height))
|
|
@@ -307,6 +366,16 @@ export function bundleEdgePaths(
|
|
|
307
366
|
if (isHorizontal) {
|
|
308
367
|
const entryX = isLR ? target.x : target.x + target.width
|
|
309
368
|
const entryY = tgtCY
|
|
369
|
+
if (
|
|
370
|
+
hasOutsideEdgeOnSide(
|
|
371
|
+
edges,
|
|
372
|
+
new Set(sources.map((s) => s.edge)),
|
|
373
|
+
targetId,
|
|
374
|
+
entryX,
|
|
375
|
+
true,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
continue
|
|
310
379
|
|
|
311
380
|
const farthestX = isLR
|
|
312
381
|
? Math.max(...sources.map((s) => s.node.x + s.node.width))
|
|
@@ -342,6 +411,16 @@ export function bundleEdgePaths(
|
|
|
342
411
|
} else {
|
|
343
412
|
const entryX = tgtCX
|
|
344
413
|
const entryY = isBT ? target.y + target.height : target.y
|
|
414
|
+
if (
|
|
415
|
+
hasOutsideEdgeOnSide(
|
|
416
|
+
edges,
|
|
417
|
+
new Set(sources.map((s) => s.edge)),
|
|
418
|
+
targetId,
|
|
419
|
+
entryY,
|
|
420
|
+
false,
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
continue
|
|
345
424
|
|
|
346
425
|
const farthestY = isBT
|
|
347
426
|
? Math.min(...sources.map((s) => s.node.y))
|
|
@@ -48,6 +48,18 @@ const STATE_MARKER_LAYER_CONSTRAINT: Partial<Record<string, string>> = {
|
|
|
48
48
|
* Root-graph ELK options applied to state diagrams only (flowcharts keep
|
|
49
49
|
* their tuned defaults).
|
|
50
50
|
*
|
|
51
|
+
* Flowcharts deliberately do not get `MODEL_ORDER` cycle breaking (#1307).
|
|
52
|
+
* They already break cycles the way mermaid.js does, by reversing the
|
|
53
|
+
* depth-first back edges before ELK runs (`edgesReversedForLayout`), so ELK
|
|
54
|
+
* has no cycle left to break; adding `MODEL_ORDER` on top only moved nodes
|
|
55
|
+
* away from mermaid.js. Measured against mermaid 11.17.2 (relative placement
|
|
56
|
+
* of every node pair, `scripts/layout-oracle.ts` style): applied to every
|
|
57
|
+
* flowchart it changed 5 of the 26 gallery samples and 49 of 104 doc/test
|
|
58
|
+
* flowcharts, closer for 0 and 2, further for 4 and 41; applied only to
|
|
59
|
+
* graphs with a subgraph direction override (the one case where the
|
|
60
|
+
* pre-reversal is skipped) it changed 1 and 7, closer for 0 and 2, further
|
|
61
|
+
* for 1 and 5.
|
|
62
|
+
*
|
|
51
63
|
* - `cycleBreaking: MODEL_ORDER` reverses the edge that points *against*
|
|
52
64
|
* declaration order instead of whichever edge ELK's greedy heuristic picks.
|
|
53
65
|
* The greedy default reversed the main chain of `[*] --> Idle`,
|
|
@@ -570,11 +582,9 @@ export function mermaidToElk(
|
|
|
570
582
|
// Deliberately scoped to *only* the sibling-subgraph reversal, not a
|
|
571
583
|
// wholesale "all clusters before all leaves" reordering (which mermaid's
|
|
572
584
|
// own mechanism also does, globally): moving the whole subgraphs group
|
|
573
|
-
// ahead of top-level leaf nodes
|
|
574
|
-
// as a feedback edge
|
|
575
|
-
//
|
|
576
|
-
// entire rank structure rather than just left-right sibling position —
|
|
577
|
-
// a much bigger, unreviewed blast radius than the reported bug needs.
|
|
585
|
+
// ahead of top-level leaf nodes changes which edge in a cycle ELK treats
|
|
586
|
+
// as a feedback edge. Cycles in a flowchart are broken before ELK sees
|
|
587
|
+
// them (`edgesReversedForLayout`), so this ordering no longer decides that.
|
|
578
588
|
const stateGraph = isStateGraph(graph)
|
|
579
589
|
const leafCount = rootChildren.length
|
|
580
590
|
// State diagrams read in source order, so composites keep forward
|
package/src/layout-engine.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
PositionedGraph,
|
|
31
31
|
FlowchartRenderOptions,
|
|
32
32
|
} from '@zombie-mermaid/core'
|
|
33
|
+
import { withoutOwnClusterEdges } from '@zombie-mermaid/core'
|
|
33
34
|
import { elkLayoutSync } from './elk-instance.ts'
|
|
34
35
|
import { resolveFontSizes } from './styles.ts'
|
|
35
36
|
import { DEFAULTS } from './layout-engine/constants.ts'
|
|
@@ -80,9 +81,12 @@ function layoutWithHints(
|
|
|
80
81
|
* share the engine (state, architecture) keep using `layoutGraphSync`.
|
|
81
82
|
*/
|
|
82
83
|
export function layoutFlowchartSync(
|
|
83
|
-
|
|
84
|
+
rawGraph: MermaidGraph,
|
|
84
85
|
options: FlowchartRenderOptions = {},
|
|
85
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)
|
|
86
90
|
if (canLayOutFlat(graph)) {
|
|
87
91
|
const fontSizes = resolveFontSizes(options.fontSizes)
|
|
88
92
|
const flat = layoutCompoundFlat(
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
}
|