@zombie-mermaid/svg-renderer 3.1.0 → 3.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 +36 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +1328 -841
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/c4-mermaid-reference.test.ts +85 -0
- package/src/__tests__/c4-svg.test.ts +337 -0
- package/src/__tests__/class-diamond-markers-1232.test.ts +59 -0
- package/src/__tests__/er-cardinality-markers-1233.test.ts +35 -0
- package/src/__tests__/fixtures/c4-mermaid-reference.json +626 -0
- package/src/__tests__/xychart-smooth-curve.test.ts +42 -0
- package/src/c4/layout.ts +490 -0
- package/src/c4/metrics.ts +235 -0
- package/src/c4/renderer.ts +366 -0
- package/src/class/renderer.ts +7 -6
- package/src/er/renderer.ts +14 -1
- package/src/layout-engine/elk-graph-builder.ts +4 -0
- package/src/registry.ts +60 -0
- package/src/xychart/layout.ts +27 -11
- package/src/xychart/renderer.ts +33 -42
package/src/registry.ts
CHANGED
|
@@ -54,6 +54,9 @@ import {
|
|
|
54
54
|
parseErDiagram,
|
|
55
55
|
parseSequenceDiagram,
|
|
56
56
|
parseClassDiagram,
|
|
57
|
+
parseArchitecture,
|
|
58
|
+
architectureToGraph,
|
|
59
|
+
parseC4Diagram,
|
|
57
60
|
} from '@zombie-mermaid/mermaid-parser'
|
|
58
61
|
import type {
|
|
59
62
|
XYChart,
|
|
@@ -64,6 +67,9 @@ import type {
|
|
|
64
67
|
PositionedSequenceDiagram,
|
|
65
68
|
ClassDiagram,
|
|
66
69
|
PositionedClassDiagram,
|
|
70
|
+
ArchitectureDiagram,
|
|
71
|
+
C4Diagram,
|
|
72
|
+
PositionedC4Diagram,
|
|
67
73
|
} from '@zombie-mermaid/mermaid-parser'
|
|
68
74
|
import { layoutXYChart } from './xychart/layout.ts'
|
|
69
75
|
import { renderXYChartSvg } from './xychart/renderer.ts'
|
|
@@ -73,6 +79,8 @@ import { layoutSequenceDiagram } from './sequence/layout.ts'
|
|
|
73
79
|
import { renderSequenceSvg } from './sequence/renderer.ts'
|
|
74
80
|
import { layoutClassDiagramSync } from './class/layout.ts'
|
|
75
81
|
import { renderClassSvg } from './class/renderer.ts'
|
|
82
|
+
import { layoutC4DiagramSync } from './c4/layout.ts'
|
|
83
|
+
import { renderC4Svg } from './c4/renderer.ts'
|
|
76
84
|
import { layoutGraphSync } from './layout-engine.ts'
|
|
77
85
|
import { renderSvg as renderFlowchartSvg } from './renderer.ts'
|
|
78
86
|
import { parseMermaid } from '../../../src/parser.ts'
|
|
@@ -323,6 +331,55 @@ const flowchartModule: DiagramModule<MermaidGraph, PositionedFlowchart> = {
|
|
|
323
331
|
},
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
const c4Module: DiagramModule<C4Diagram, PositionedC4Diagram> = {
|
|
335
|
+
type: 'c4',
|
|
336
|
+
parse: parseC4Diagram,
|
|
337
|
+
// Mermaid's C4 renderer has no direction: shapes are placed in rows in
|
|
338
|
+
// declaration order, so `options.direction` does not apply to the SVG.
|
|
339
|
+
layoutForSvg(diagram, options) {
|
|
340
|
+
return layoutC4DiagramSync(diagram, options)
|
|
341
|
+
},
|
|
342
|
+
renderSvg(positioned, ctx) {
|
|
343
|
+
return renderC4Svg(
|
|
344
|
+
positioned,
|
|
345
|
+
ctx.colors,
|
|
346
|
+
ctx.font,
|
|
347
|
+
ctx.transparent,
|
|
348
|
+
ctx.fontSizes,
|
|
349
|
+
ctx.embedSource,
|
|
350
|
+
ctx.title,
|
|
351
|
+
ctx.decorative,
|
|
352
|
+
ctx.emit,
|
|
353
|
+
)
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Architecture diagrams are lowered to the flowchart model
|
|
359
|
+
* (`architectureToGraph` in `@zombie-mermaid/mermaid-parser`) and ride the
|
|
360
|
+
* flowchart layout + renderer, so this module is `flowchartModule` with a
|
|
361
|
+
* different `parse`. The lowered graph carries no `%%{init}%%` config, so
|
|
362
|
+
* `curve` is `options.curve` or 'linear'.
|
|
363
|
+
*/
|
|
364
|
+
const architectureModule: DiagramModule<
|
|
365
|
+
ArchitectureDiagram,
|
|
366
|
+
PositionedFlowchart
|
|
367
|
+
> = {
|
|
368
|
+
type: 'architecture',
|
|
369
|
+
parse: parseArchitecture,
|
|
370
|
+
layoutForSvg(diagram, options) {
|
|
371
|
+
const graph = withDirectionOverride(
|
|
372
|
+
architectureToGraph(diagram),
|
|
373
|
+
options.direction,
|
|
374
|
+
)
|
|
375
|
+
return {
|
|
376
|
+
graph: layoutGraphSync(graph, options),
|
|
377
|
+
curve: options.curve ?? 'linear',
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
renderSvg: flowchartModule.renderSvg,
|
|
381
|
+
}
|
|
382
|
+
|
|
326
383
|
/**
|
|
327
384
|
* The registry proper — every `DiagramType` is looked up here by the SVG
|
|
328
385
|
* front door (`renderMermaidSVG` in ./index.ts), which has no fallback
|
|
@@ -348,4 +405,7 @@ export const diagramRegistry: Record<DiagramType, AnyDiagramModule> = {
|
|
|
348
405
|
sequence: sequenceModule,
|
|
349
406
|
class: classModule,
|
|
350
407
|
flowchart: flowchartModule,
|
|
408
|
+
architecture: architectureModule,
|
|
409
|
+
|
|
410
|
+
c4: c4Module,
|
|
351
411
|
}
|
package/src/xychart/layout.ts
CHANGED
|
@@ -82,13 +82,10 @@ function layoutVertical(chart: XYChart): PositionedXYChart {
|
|
|
82
82
|
throw new Error('XY chart: y-axis range was not set by the parser')
|
|
83
83
|
}
|
|
84
84
|
const yTicks = niceTickValues(yRange.min, yRange.max)
|
|
85
|
+
const yLabels = formatTickLabels(yTicks)
|
|
85
86
|
const maxYLabelWidth = Math.max(
|
|
86
|
-
...
|
|
87
|
-
estimateTextWidth(
|
|
88
|
-
formatTickValue(v),
|
|
89
|
-
XY.axisLabelFontSize,
|
|
90
|
-
XY.axisLabelFontWeight,
|
|
91
|
-
),
|
|
87
|
+
...yLabels.map((label) =>
|
|
88
|
+
estimateTextWidth(label, XY.axisLabelFontSize, XY.axisLabelFontWeight),
|
|
92
89
|
),
|
|
93
90
|
XY.yLabelWidth,
|
|
94
91
|
)
|
|
@@ -127,8 +124,8 @@ function layoutVertical(chart: XYChart): PositionedXYChart {
|
|
|
127
124
|
const xTicks = buildXTicks(chart, xScale, top + plotH, bandWidth)
|
|
128
125
|
|
|
129
126
|
// Y-axis ticks
|
|
130
|
-
const yAxisTicks: AxisTick[] = yTicks.map((v) => ({
|
|
131
|
-
label:
|
|
127
|
+
const yAxisTicks: AxisTick[] = yTicks.map((v, i) => ({
|
|
128
|
+
label: yLabels[i]!,
|
|
132
129
|
x: left,
|
|
133
130
|
y: yScale(v),
|
|
134
131
|
tx: left - XY.tickLength,
|
|
@@ -251,6 +248,7 @@ function layoutHorizontal(chart: XYChart): PositionedXYChart {
|
|
|
251
248
|
throw new Error('XY chart: y-axis range was not set by the parser')
|
|
252
249
|
}
|
|
253
250
|
const valueTicks = niceTickValues(yRange.min, yRange.max)
|
|
251
|
+
const valueLabels = formatTickLabels(valueTicks)
|
|
254
252
|
|
|
255
253
|
// Compute category label widths for left margin
|
|
256
254
|
const dataCount = getDataCount(chart)
|
|
@@ -293,8 +291,8 @@ function layoutHorizontal(chart: XYChart): PositionedXYChart {
|
|
|
293
291
|
const catScale = (i: number) => top + (i + 0.5) * bandHeight
|
|
294
292
|
|
|
295
293
|
// X-axis (bottom): value ticks
|
|
296
|
-
const xTicks: AxisTick[] = valueTicks.map((v) => ({
|
|
297
|
-
label:
|
|
294
|
+
const xTicks: AxisTick[] = valueTicks.map((v, i) => ({
|
|
295
|
+
label: valueLabels[i]!,
|
|
298
296
|
x: valueScale(v),
|
|
299
297
|
y: top + plotH,
|
|
300
298
|
tx: valueScale(v),
|
|
@@ -606,7 +604,7 @@ function layoutLines(
|
|
|
606
604
|
}
|
|
607
605
|
|
|
608
606
|
/** Generate "nice" tick values for a numeric range */
|
|
609
|
-
function niceTickValues(min: number, max: number): number[] {
|
|
607
|
+
export function niceTickValues(min: number, max: number): number[] {
|
|
610
608
|
const range = max - min
|
|
611
609
|
if (range <= 0) return [min]
|
|
612
610
|
|
|
@@ -628,6 +626,24 @@ function niceTickValues(min: number, max: number): number[] {
|
|
|
628
626
|
return ticks
|
|
629
627
|
}
|
|
630
628
|
|
|
629
|
+
/**
|
|
630
|
+
* Format a set of evenly-spaced tick values with enough decimal places to
|
|
631
|
+
* keep every label distinct. Precision comes from the tick step, not the
|
|
632
|
+
* magnitude of the value: a 0.5 step around 100 needs one decimal, or
|
|
633
|
+
* 99.5 and 100.5 both collapse into neighbouring whole numbers.
|
|
634
|
+
*/
|
|
635
|
+
export function formatTickLabels(ticks: number[]): string[] {
|
|
636
|
+
const step = ticks.length > 1 ? Math.abs(ticks[1]! - ticks[0]!) : 0
|
|
637
|
+
// The -1e-9 keeps an exact power-of-ten step (0.1) from rounding up
|
|
638
|
+
const decimals =
|
|
639
|
+
step > 0 && step < 1 ? Math.ceil(-Math.log10(step) - 1e-9) : 0
|
|
640
|
+
return ticks.map((v) =>
|
|
641
|
+
Number.isInteger(v) && decimals === 0
|
|
642
|
+
? String(v)
|
|
643
|
+
: String(Number(v.toFixed(decimals))),
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
631
647
|
function formatTickValue(v: number): string {
|
|
632
648
|
if (Number.isInteger(v)) return String(v)
|
|
633
649
|
// Limit decimal places
|
package/src/xychart/renderer.ts
CHANGED
|
@@ -506,17 +506,21 @@ function roundedRightBarPath(
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
// ============================================================================
|
|
509
|
-
// Smooth line interpolation —
|
|
509
|
+
// Smooth line interpolation — monotone cubic (Fritsch-Carlson / PCHIP)
|
|
510
510
|
//
|
|
511
|
-
//
|
|
512
|
-
//
|
|
513
|
-
//
|
|
511
|
+
// A natural cubic spline is smooth but not shape-preserving: near a sharp
|
|
512
|
+
// spike it overshoots the data, drawing a peak above the maximum or a dip
|
|
513
|
+
// below the minimum (even below a zero baseline). Monotone cubic
|
|
514
|
+
// interpolation keeps each segment monotone between its two points and puts a
|
|
515
|
+
// flat tangent at every local extremum, so the curve never leaves the range of
|
|
516
|
+
// the data. y is treated as a function of x, so the curve never goes backwards.
|
|
514
517
|
//
|
|
515
|
-
//
|
|
516
|
-
// then convert each cubic segment to SVG cubic Bezier commands.
|
|
518
|
+
// Each cubic segment is converted to an SVG cubic Bezier command.
|
|
517
519
|
// ============================================================================
|
|
518
520
|
|
|
519
|
-
function smoothCurvePath(
|
|
521
|
+
export function smoothCurvePath(
|
|
522
|
+
points: Array<{ x: number; y: number }>,
|
|
523
|
+
): string {
|
|
520
524
|
if (points.length === 0) return ''
|
|
521
525
|
if (points.length === 1) return `M${r(points[0]!.x)},${r(points[0]!.y)}`
|
|
522
526
|
if (points.length === 2) {
|
|
@@ -526,12 +530,10 @@ function smoothCurvePath(points: Array<{ x: number; y: number }>): string {
|
|
|
526
530
|
const n = points.length
|
|
527
531
|
|
|
528
532
|
// From here on, n >= 3 (the n <= 2 cases returned above). Every index used
|
|
529
|
-
// below
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
// this codebase and are left as-is (justified, not asserting past a real
|
|
534
|
-
// gap in type info).
|
|
533
|
+
// below is bound by a `< n - 1` or `< n` loop condition, or is a fixed
|
|
534
|
+
// offset from n itself, into arrays sized exactly `n` or `n - 1`.
|
|
535
|
+
// Non-null assertions on those accesses are the same bounds-checked idiom as
|
|
536
|
+
// loop-index array access elsewhere in this codebase.
|
|
535
537
|
|
|
536
538
|
// 1. Interval widths and secant slopes
|
|
537
539
|
const h: number[] = []
|
|
@@ -541,37 +543,26 @@ function smoothCurvePath(points: Array<{ x: number; y: number }>): string {
|
|
|
541
543
|
delta.push(h[i]! === 0 ? 0 : (points[i + 1]!.y - points[i]!.y) / h[i]!)
|
|
542
544
|
}
|
|
543
545
|
|
|
544
|
-
// 2.
|
|
545
|
-
|
|
546
|
-
if (n > 2) {
|
|
547
|
-
// Forward elimination
|
|
548
|
-
const cp = new Array<number>(n).fill(0) // modified upper diagonal
|
|
549
|
-
const dp = new Array<number>(n).fill(0) // modified right-hand side
|
|
550
|
-
for (let i = 1; i < n - 1; i++) {
|
|
551
|
-
const diag = 2 * (h[i - 1]! + h[i]!)
|
|
552
|
-
const rhs = 3 * (delta[i]! - delta[i - 1]!)
|
|
553
|
-
if (i === 1) {
|
|
554
|
-
cp[i] = h[i]! / diag
|
|
555
|
-
dp[i] = rhs / diag
|
|
556
|
-
} else {
|
|
557
|
-
const w = diag - h[i - 1]! * cp[i - 1]!
|
|
558
|
-
cp[i] = h[i]! / w
|
|
559
|
-
dp[i] = (rhs - h[i - 1]! * dp[i - 1]!) / w
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// Back substitution
|
|
563
|
-
for (let i = n - 2; i >= 1; i--) {
|
|
564
|
-
c[i] = dp[i]! - cp[i]! * c[i + 1]!
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// 3. Compute first derivatives (slopes) at each knot
|
|
546
|
+
// 2. Tangent at each knot (Fritsch-Carlson). Interior: weighted harmonic
|
|
547
|
+
// mean of the adjacent secants, or 0 at a local extremum / flat run.
|
|
569
548
|
const slopes = new Array<number>(n).fill(0)
|
|
570
|
-
for (let i =
|
|
571
|
-
|
|
549
|
+
for (let i = 1; i < n - 1; i++) {
|
|
550
|
+
const d0 = delta[i - 1]!
|
|
551
|
+
const d1 = delta[i]!
|
|
552
|
+
if (d0 * d1 <= 0) continue
|
|
553
|
+
const w1 = 2 * h[i]! + h[i - 1]!
|
|
554
|
+
const w2 = h[i]! + 2 * h[i - 1]!
|
|
555
|
+
slopes[i] = (w1 + w2) / (w1 / d0 + w2 / d1)
|
|
556
|
+
}
|
|
557
|
+
// Ends: one-sided three-point estimate, clamped so it can't overshoot.
|
|
558
|
+
const endSlope = (h0: number, h1: number, d0: number, d1: number): number => {
|
|
559
|
+
let m = ((2 * h0 + h1) * d0 - h0 * d1) / (h0 + h1)
|
|
560
|
+
if (m * d0 <= 0) m = 0
|
|
561
|
+
else if (d0 * d1 <= 0 && Math.abs(m) > 3 * Math.abs(d0)) m = 3 * d0
|
|
562
|
+
return m
|
|
572
563
|
}
|
|
573
|
-
|
|
574
|
-
slopes[n - 1] =
|
|
564
|
+
slopes[0] = endSlope(h[0]!, h[1]!, delta[0]!, delta[1]!)
|
|
565
|
+
slopes[n - 1] = endSlope(h[n - 2]!, h[n - 3]!, delta[n - 2]!, delta[n - 3]!)
|
|
575
566
|
|
|
576
567
|
// 4. Convert to cubic Bezier — control points strictly between endpoints in x
|
|
577
568
|
let path = `M${r(points[0]!.x)},${r(points[0]!.y)}`
|