@zombie-mermaid/svg-renderer 4.1.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.
@@ -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
+ }
package/src/registry.ts CHANGED
@@ -57,6 +57,7 @@ import {
57
57
  parseArchitecture,
58
58
  architectureToGraph,
59
59
  parseC4Diagram,
60
+ parsePieChart,
60
61
  } from '@zombie-mermaid/mermaid-parser'
61
62
  import type {
62
63
  XYChart,
@@ -70,6 +71,8 @@ import type {
70
71
  ArchitectureDiagram,
71
72
  C4Diagram,
72
73
  PositionedC4Diagram,
74
+ PieChart,
75
+ PositionedPieChart,
73
76
  } from '@zombie-mermaid/mermaid-parser'
74
77
  import { layoutXYChart } from './xychart/layout.ts'
75
78
  import { renderXYChartSvg } from './xychart/renderer.ts'
@@ -81,6 +84,8 @@ import { layoutClassDiagramSync } from './class/layout.ts'
81
84
  import { renderClassSvg } from './class/renderer.ts'
82
85
  import { layoutC4DiagramSync } from './c4/layout.ts'
83
86
  import { renderC4Svg } from './c4/renderer.ts'
87
+ import { layoutPieChart } from './pie/layout.ts'
88
+ import { renderPieSvg } from './pie/renderer.ts'
84
89
  import { layoutFlowchartSync, layoutGraphSync } from './layout-engine.ts'
85
90
  import { renderSvg as renderFlowchartSvg } from './renderer.ts'
86
91
  import { parseMermaid } from '@zombie-mermaid/mermaid-parser'
@@ -380,6 +385,30 @@ const architectureModule: DiagramModule<
380
385
  renderSvg: flowchartModule.renderSvg,
381
386
  }
382
387
 
388
+ /**
389
+ * Pie charts parse from the raw `text` rather than `lines` (see
390
+ * `parsePieChart`'s doc: `splitStatements` would split `title A; B` and
391
+ * treat `'` as a quote), then lay out and render to Mermaid's geometry —
392
+ * see ./pie/layout.ts. No direction or interactivity applies.
393
+ */
394
+ const pieModule: DiagramModule<PieChart, PositionedPieChart> = {
395
+ type: 'pie',
396
+ parse: (_lines, text) => parsePieChart(text),
397
+ layoutForSvg: layoutPieChart,
398
+ renderSvg(positioned, ctx) {
399
+ return renderPieSvg(
400
+ positioned,
401
+ ctx.colors,
402
+ ctx.font,
403
+ ctx.transparent,
404
+ ctx.embedSource,
405
+ ctx.title,
406
+ ctx.decorative,
407
+ ctx.emit,
408
+ )
409
+ },
410
+ }
411
+
383
412
  /**
384
413
  * The registry proper — every `DiagramType` is looked up here by the SVG
385
414
  * front door (`renderMermaidSVG` in ./index.ts), which has no fallback
@@ -408,4 +437,5 @@ export const diagramRegistry: Record<DiagramType, AnyDiagramModule> = {
408
437
  architecture: architectureModule,
409
438
 
410
439
  c4: c4Module,
440
+ pie: pieModule,
411
441
  }