@zombie-mermaid/svg-renderer 2.2.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.
@@ -0,0 +1,493 @@
1
+ import type {
2
+ PositionedErDiagram,
3
+ PositionedErEntity,
4
+ PositionedErRelationship,
5
+ ErAttribute,
6
+ Cardinality,
7
+ } from '@zombie-mermaid/mermaid-parser'
8
+ import type { DiagramColors, SvgEmitOptions } from '@zombie-mermaid/core'
9
+ import {
10
+ svgOpenTag,
11
+ buildStyleBlock,
12
+ renderMultilineText,
13
+ escapeXml as escapeXmlUtil,
14
+ escapeAttr,
15
+ measureMultilineText,
16
+ } from '@zombie-mermaid/core'
17
+ import { withDataSrc } from '../renderer.ts'
18
+ import {
19
+ FONT_SIZES,
20
+ FONT_WEIGHTS,
21
+ STROKE_WIDTHS,
22
+ estimateTextWidth,
23
+ TEXT_BASELINE_SHIFT,
24
+ } from '../styles.ts'
25
+ import type { FontSizes } from '../styles.ts'
26
+
27
+ // ============================================================================
28
+ // ER diagram SVG renderer
29
+ //
30
+ // Renders positioned ER diagrams to SVG.
31
+ // All colors use CSS custom properties (var(--_xxx)) from the theme system.
32
+ //
33
+ // Render order:
34
+ // 1. Relationship lines (behind boxes)
35
+ // 2. Entity boxes (header + attribute rows)
36
+ // 3. Cardinality markers (crow's foot notation)
37
+ // 4. Relationship labels
38
+ // ============================================================================
39
+
40
+ /** Font sizes specific to ER diagrams */
41
+ const ER_FONT = {
42
+ attrSize: 11,
43
+ attrWeight: 400,
44
+ keySize: 9,
45
+ keyWeight: 600,
46
+ } as const
47
+
48
+ /**
49
+ * Render a positioned ER diagram as an SVG string.
50
+ *
51
+ * @param colors - DiagramColors with bg/fg and optional enrichment variables.
52
+ * @param transparent - If true, renders with transparent background.
53
+ * @param embedSource - Original diagram source to stamp onto the root `<svg>`
54
+ * as `data-src` (from `options.embedSource`). Omitted
55
+ * when the option is off.
56
+ * @param title - Accessible name (from `options.title`). See svgOpenTag() in
57
+ * packages/core/src/theme.ts.
58
+ * @param decorative - Marks the SVG decorative (from `options.decorative`).
59
+ * @param emit - Strict-CSP controls (from `options.nonce` /
60
+ * `options.styleAttribute`, see #216). Default: no nonce,
61
+ * root `style` attribute on.
62
+ */
63
+ export function renderErSvg(
64
+ diagram: PositionedErDiagram,
65
+ colors: DiagramColors,
66
+ font: string = 'Inter',
67
+ transparent: boolean = false,
68
+ fontSizes: FontSizes = FONT_SIZES,
69
+ embedSource?: string,
70
+ title?: string,
71
+ decorative?: boolean,
72
+ emit: SvgEmitOptions = {},
73
+ ): string {
74
+ const parts: string[] = []
75
+
76
+ // SVG root with CSS variables + style block (with mono font) + defs
77
+ parts.push(
78
+ withDataSrc(
79
+ svgOpenTag(
80
+ diagram.width,
81
+ diagram.height,
82
+ colors,
83
+ transparent,
84
+ title,
85
+ decorative,
86
+ undefined,
87
+ emit.styleAttribute,
88
+ ),
89
+ embedSource,
90
+ ),
91
+ )
92
+ parts.push(buildStyleBlock(font, true, emit.nonce))
93
+ parts.push('<defs>')
94
+ parts.push('</defs>') // No marker defs — we draw crow's foot inline
95
+
96
+ // 1. Relationship lines
97
+ for (const rel of diagram.relationships) {
98
+ parts.push(renderRelationshipLine(rel))
99
+ }
100
+
101
+ // 2. Entity boxes
102
+ for (const entity of diagram.entities) {
103
+ parts.push(renderEntityBox(entity, fontSizes))
104
+ }
105
+
106
+ // 3. Cardinality markers at relationship endpoints
107
+ for (const rel of diagram.relationships) {
108
+ parts.push(renderCardinality(rel))
109
+ }
110
+
111
+ // 4. Relationship labels
112
+ for (const rel of diagram.relationships) {
113
+ parts.push(renderRelationshipLabel(rel, fontSizes))
114
+ }
115
+
116
+ parts.push('</svg>')
117
+ return parts.join('\n')
118
+ }
119
+
120
+ // ============================================================================
121
+ // Entity box rendering
122
+ // ============================================================================
123
+
124
+ /**
125
+ * Render an entity box with header and attribute rows.
126
+ * Wrapped in <g class="entity"> with semantic data attributes.
127
+ */
128
+ function renderEntityBox(
129
+ entity: PositionedErEntity,
130
+ fontSizes: FontSizes,
131
+ ): string {
132
+ const {
133
+ id,
134
+ x,
135
+ y,
136
+ width,
137
+ height,
138
+ headerHeight,
139
+ rowHeight,
140
+ label,
141
+ attributes,
142
+ } = entity
143
+ const parts: string[] = []
144
+
145
+ // Semantic wrapper with entity metadata
146
+ parts.push(
147
+ `<g class="entity" data-id="${escapeAttr(id)}" data-label="${escapeAttr(label)}">`,
148
+ )
149
+
150
+ // Outer rectangle
151
+ parts.push(
152
+ ` <rect x="${x}" y="${y}" width="${width}" height="${height}" ` +
153
+ `rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
154
+ )
155
+
156
+ // Header background
157
+ parts.push(
158
+ ` <rect x="${x}" y="${y}" width="${width}" height="${headerHeight}" ` +
159
+ `rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.outerBox}" />`,
160
+ )
161
+
162
+ // Entity name (supports multi-line via <br> tags)
163
+ parts.push(
164
+ ' ' +
165
+ renderMultilineText(
166
+ label,
167
+ x + width / 2,
168
+ y + headerHeight / 2,
169
+ fontSizes.nodeLabel,
170
+ `text-anchor="middle" font-size="${fontSizes.nodeLabel}" font-weight="700" fill="var(--_text)"`,
171
+ ),
172
+ )
173
+
174
+ // Divider
175
+ const attrTop = y + headerHeight
176
+ parts.push(
177
+ ` <line x1="${x}" y1="${attrTop}" x2="${x + width}" y2="${attrTop}" ` +
178
+ `stroke="var(--_node-stroke)" stroke-width="${STROKE_WIDTHS.innerBox}" />`,
179
+ )
180
+
181
+ // Attribute rows
182
+ for (let i = 0; i < attributes.length; i++) {
183
+ const attr = attributes[i]!
184
+ const rowY = attrTop + i * rowHeight + rowHeight / 2
185
+ parts.push(
186
+ ' ' + renderAttribute(attr, x, rowY, width).replace(/\n/g, '\n '),
187
+ )
188
+ }
189
+
190
+ // Empty row placeholder when no attributes
191
+ if (attributes.length === 0) {
192
+ parts.push(
193
+ ` <text x="${x + width / 2}" y="${attrTop + rowHeight / 2}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
194
+ `font-size="${ER_FONT.attrSize}" fill="var(--_text-faint)" font-style="italic">(no attributes)</text>`,
195
+ )
196
+ }
197
+
198
+ parts.push('</g>')
199
+ return parts.join('\n')
200
+ }
201
+
202
+ /**
203
+ * Render a single attribute row with monospace syntax highlighting.
204
+ * Layout: [PK badge] type name (left-aligned in mono, name right-aligned)
205
+ * Uses <tspan> elements for per-part coloring, matching the class diagram style.
206
+ *
207
+ * Key badge uses var(--_key-badge) for background tint.
208
+ * Comments are shown as tooltips via SVG <title> element.
209
+ */
210
+ function renderAttribute(
211
+ attr: ErAttribute,
212
+ boxX: number,
213
+ y: number,
214
+ boxWidth: number,
215
+ ): string {
216
+ const parts: string[] = []
217
+
218
+ // Wrap in a group if there's a comment (for tooltip support)
219
+ const hasComment = attr.comment && attr.comment.length > 0
220
+ if (attr.comment && attr.comment.length > 0) {
221
+ // Replace <br> with newlines for tooltip display
222
+ const tooltipText = attr.comment.replace(/<br\s*\/?>/gi, '\n')
223
+ parts.push(`<g><title>${escapeXml(tooltipText)}</title>`)
224
+ }
225
+
226
+ // Key badges on the left (keep proportional font — they're visual tags, not code)
227
+ let keyWidth = 0
228
+ if (attr.keys.length > 0) {
229
+ const keyText = attr.keys.join(',')
230
+ keyWidth =
231
+ estimateTextWidth(keyText, ER_FONT.keySize, ER_FONT.keyWeight) + 8
232
+ parts.push(
233
+ `<rect x="${boxX + 6}" y="${y - 7}" width="${keyWidth}" height="14" rx="2" ry="2" ` +
234
+ `fill="var(--_key-badge)" />`,
235
+ )
236
+ parts.push(
237
+ `<text x="${boxX + 6 + keyWidth / 2}" y="${y}" text-anchor="middle" dy="${TEXT_BASELINE_SHIFT}" ` +
238
+ `font-size="${ER_FONT.keySize}" font-weight="${ER_FONT.keyWeight}" fill="var(--_text-sec)">${attr.keys.join(',')}</text>`,
239
+ )
240
+ }
241
+
242
+ // Type (left-aligned after keys, monospace with syntax highlighting)
243
+ const typeX = boxX + 8 + (keyWidth > 0 ? keyWidth + 6 : 0)
244
+ parts.push(
245
+ `<text x="${typeX}" y="${y}" class="mono" dy="${TEXT_BASELINE_SHIFT}" ` +
246
+ `font-size="${ER_FONT.attrSize}" font-weight="${ER_FONT.attrWeight}">` +
247
+ `<tspan fill="var(--_text-muted)">${escapeXml(attr.type)}</tspan></text>`,
248
+ )
249
+
250
+ // Name (right-aligned, monospace with syntax highlighting)
251
+ const nameX = boxX + boxWidth - 8
252
+ parts.push(
253
+ `<text x="${nameX}" y="${y}" class="mono" text-anchor="end" dy="${TEXT_BASELINE_SHIFT}" ` +
254
+ `font-size="${ER_FONT.attrSize}" font-weight="${ER_FONT.attrWeight}">` +
255
+ `<tspan fill="var(--_text-sec)">${escapeXml(attr.name)}</tspan></text>`,
256
+ )
257
+
258
+ // Close the group if we opened one
259
+ if (hasComment) {
260
+ parts.push('</g>')
261
+ }
262
+
263
+ return parts.join('\n')
264
+ }
265
+
266
+ // ============================================================================
267
+ // Relationship rendering
268
+ // ============================================================================
269
+
270
+ /**
271
+ * Render a relationship line with semantic data attributes.
272
+ */
273
+ function renderRelationshipLine(rel: PositionedErRelationship): string {
274
+ if (rel.points.length < 2) return ''
275
+
276
+ const pathData = rel.points.map((p) => `${p.x},${p.y}`).join(' ')
277
+ const dashArray = !rel.identifying ? ' stroke-dasharray="6 4"' : ''
278
+
279
+ // Semantic data attributes for relationship inspection
280
+ const labelAttr = rel.label ? ` data-label="${escapeAttr(rel.label)}"` : ''
281
+ const dataAttrs = [
282
+ 'class="er-relationship"',
283
+ `data-entity1="${escapeAttr(rel.entity1)}"`,
284
+ `data-entity2="${escapeAttr(rel.entity2)}"`,
285
+ `data-cardinality1="${rel.cardinality1}"`,
286
+ `data-cardinality2="${rel.cardinality2}"`,
287
+ `data-identifying="${rel.identifying}"`,
288
+ ]
289
+
290
+ return (
291
+ `<polyline ${dataAttrs.join(' ')}${labelAttr} points="${pathData}" fill="none" stroke="var(--_line)" ` +
292
+ `stroke-width="${STROKE_WIDTHS.connector}"${dashArray} />`
293
+ )
294
+ }
295
+
296
+ /** Render a relationship label at the midpoint (supports multi-line) */
297
+ function renderRelationshipLabel(
298
+ rel: PositionedErRelationship,
299
+ fontSizes: FontSizes,
300
+ ): string {
301
+ if (!rel.label || rel.points.length < 2) return ''
302
+
303
+ const mid = midpoint(rel.points)
304
+ const metrics = measureMultilineText(
305
+ rel.label,
306
+ fontSizes.edgeLabel,
307
+ FONT_WEIGHTS.edgeLabel,
308
+ )
309
+
310
+ // Background pill for readability
311
+ const bgW = metrics.width + 8
312
+ const bgH = metrics.height + 6
313
+
314
+ return (
315
+ `<rect x="${mid.x - bgW / 2}" y="${mid.y - bgH / 2}" width="${bgW}" height="${bgH}" rx="2" ry="2" ` +
316
+ `fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="0.5" />` +
317
+ `\n${renderMultilineText(
318
+ rel.label,
319
+ mid.x,
320
+ mid.y,
321
+ fontSizes.edgeLabel,
322
+ `text-anchor="middle" font-size="${fontSizes.edgeLabel}" font-weight="${FONT_WEIGHTS.edgeLabel}" fill="var(--_text-muted)"`,
323
+ )}`
324
+ )
325
+ }
326
+
327
+ /**
328
+ * Render crow's foot cardinality markers at both endpoints of a relationship.
329
+ *
330
+ * Crow's foot notation:
331
+ * 'one': ─║─ (single vertical line)
332
+ * 'zero-one': ─o║─ (circle + single line)
333
+ * 'many': ─╢─ (crow's foot + single line)
334
+ * 'zero-many': ─o╣─ (circle + crow's foot)
335
+ */
336
+ function renderCardinality(rel: PositionedErRelationship): string {
337
+ if (rel.points.length < 2) return ''
338
+ const parts: string[] = []
339
+
340
+ // Entity1 side (first point, direction toward second point)
341
+ const p1 = rel.points[0]!
342
+ const p2 = rel.points[1]!
343
+ parts.push(renderCrowsFoot(p1, p2, rel.cardinality1))
344
+
345
+ // Entity2 side (last point, direction toward second-to-last point)
346
+ const pN = rel.points[rel.points.length - 1]!
347
+ const pN1 = rel.points[rel.points.length - 2]!
348
+ parts.push(renderCrowsFoot(pN, pN1, rel.cardinality2))
349
+
350
+ return parts.join('\n')
351
+ }
352
+
353
+ /**
354
+ * Render a crow's foot marker at a given endpoint.
355
+ * `point` is the endpoint, `toward` gives the direction the line comes from.
356
+ */
357
+ function renderCrowsFoot(
358
+ point: { x: number; y: number },
359
+ toward: { x: number; y: number },
360
+ cardinality: Cardinality,
361
+ ): string {
362
+ const parts: string[] = []
363
+ const sw = STROKE_WIDTHS.connector + 0.25
364
+
365
+ // Calculate direction from toward → point (unit vector)
366
+ const dx = point.x - toward.x
367
+ const dy = point.y - toward.y
368
+ const len = Math.sqrt(dx * dx + dy * dy)
369
+ if (len === 0) return ''
370
+ const ux = dx / len
371
+ const uy = dy / len
372
+
373
+ // Perpendicular direction
374
+ const px = -uy
375
+ const py = ux
376
+
377
+ // Marker sits 4px from the endpoint, extending 12px back along the edge
378
+ const tipX = point.x - ux * 4
379
+ const tipY = point.y - uy * 4
380
+ const backX = point.x - ux * 16
381
+ const backY = point.y - uy * 16
382
+
383
+ // Single line: always present for 'one' and part of others
384
+ const hasOneLine = cardinality === 'one' || cardinality === 'zero-one'
385
+ const hasCrowsFoot = cardinality === 'many' || cardinality === 'zero-many'
386
+ const hasCircle = cardinality === 'zero-one' || cardinality === 'zero-many'
387
+
388
+ // Draw single vertical line (perpendicular to edge) at the tip
389
+ if (hasOneLine) {
390
+ const halfW = 6
391
+ parts.push(
392
+ `<line x1="${tipX + px * halfW}" y1="${tipY + py * halfW}" ` +
393
+ `x2="${tipX - px * halfW}" y2="${tipY - py * halfW}" ` +
394
+ `stroke="var(--_line)" stroke-width="${sw}" />`,
395
+ )
396
+ // Second line slightly back for "exactly one" emphasis
397
+ const line2X = tipX - ux * 4
398
+ const line2Y = tipY - uy * 4
399
+ parts.push(
400
+ `<line x1="${line2X + px * halfW}" y1="${line2Y + py * halfW}" ` +
401
+ `x2="${line2X - px * halfW}" y2="${line2Y - py * halfW}" ` +
402
+ `stroke="var(--_line)" stroke-width="${sw}" />`,
403
+ )
404
+ }
405
+
406
+ // Crow's foot (three lines fanning out from tip)
407
+ if (hasCrowsFoot) {
408
+ const fanW = 7
409
+ // Center line
410
+ const cfTipX = tipX
411
+ const cfTipY = tipY
412
+ // Three lines from tip to back, fanning out
413
+ parts.push(
414
+ // Top fan line
415
+ `<line x1="${cfTipX + px * fanW}" y1="${cfTipY + py * fanW}" ` +
416
+ `x2="${backX}" y2="${backY}" ` +
417
+ `stroke="var(--_line)" stroke-width="${sw}" />`,
418
+ )
419
+ parts.push(
420
+ // Center line
421
+ `<line x1="${cfTipX}" y1="${cfTipY}" ` +
422
+ `x2="${backX}" y2="${backY}" ` +
423
+ `stroke="var(--_line)" stroke-width="${sw}" />`,
424
+ )
425
+ parts.push(
426
+ // Bottom fan line
427
+ `<line x1="${cfTipX - px * fanW}" y1="${cfTipY - py * fanW}" ` +
428
+ `x2="${backX}" y2="${backY}" ` +
429
+ `stroke="var(--_line)" stroke-width="${sw}" />`,
430
+ )
431
+ }
432
+
433
+ // Circle (for zero variants)
434
+ if (hasCircle) {
435
+ const circleOffset = hasCrowsFoot ? 20 : 12
436
+ const circleX = point.x - ux * circleOffset
437
+ const circleY = point.y - uy * circleOffset
438
+ parts.push(
439
+ `<circle cx="${circleX}" cy="${circleY}" r="4" ` +
440
+ `fill="var(--bg)" stroke="var(--_line)" stroke-width="${sw}" />`,
441
+ )
442
+ }
443
+
444
+ return parts.join('\n')
445
+ }
446
+
447
+ /** Compute the arc-length midpoint of a polyline path.
448
+ * Walks along each segment, finds the point at exactly 50% of total path length.
449
+ * This ensures the label sits ON the path even for orthogonal routes with bends,
450
+ * unlike the naive first/last geometric center which floats in space for L/Z shapes. */
451
+ function midpoint(points: Array<{ x: number; y: number }>): {
452
+ x: number
453
+ y: number
454
+ } {
455
+ if (points.length === 0) return { x: 0, y: 0 }
456
+ if (points.length === 1) return points[0]!
457
+
458
+ // Compute total path length
459
+ let totalLen = 0
460
+ for (let i = 1; i < points.length; i++) {
461
+ const dx = points[i]!.x - points[i - 1]!.x
462
+ const dy = points[i]!.y - points[i - 1]!.y
463
+ totalLen += Math.sqrt(dx * dx + dy * dy)
464
+ }
465
+
466
+ if (totalLen === 0) return points[0]!
467
+
468
+ // Walk to 50% of total length, interpolating within the segment that crosses the halfway mark
469
+ const halfLen = totalLen / 2
470
+ let walked = 0
471
+ for (let i = 1; i < points.length; i++) {
472
+ const dx = points[i]!.x - points[i - 1]!.x
473
+ const dy = points[i]!.y - points[i - 1]!.y
474
+ const segLen = Math.sqrt(dx * dx + dy * dy)
475
+ if (walked + segLen >= halfLen) {
476
+ const t = segLen > 0 ? (halfLen - walked) / segLen : 0
477
+ return {
478
+ x: points[i - 1]!.x + dx * t,
479
+ y: points[i - 1]!.y + dy * t,
480
+ }
481
+ }
482
+ walked += segLen
483
+ }
484
+
485
+ return points[points.length - 1]!
486
+ }
487
+
488
+ // ============================================================================
489
+ // Utilities
490
+ // ============================================================================
491
+
492
+ // Use shared escapeXml from multiline-utils
493
+ const escapeXml = escapeXmlUtil
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ // ============================================================================
2
+ // @zombie-mermaid/svg-renderer — SVG emission and ELK-backed layout
3
+ //
4
+ // Every file re-exported below was grep-verified (zombie-mermaid#625,
5
+ // umbrella #620) as unreachable from `src/ascii/index.ts` except through
6
+ // the SVG-side modules the umbrella still owns — so nothing here can put
7
+ // `elkjs` on the ASCII renderer's dependency path once #623 extracts it.
8
+ //
9
+ // This package depends on `@zombie-mermaid/core` and, since #624, on
10
+ // `@zombie-mermaid/mermaid-parser` too — an ordinary, acyclic dependency
11
+ // (both `svg-renderer` and the future `ascii-renderer` depend on
12
+ // `mermaid-parser` directly, per the scoping doc's finding 2), not a cycle:
13
+ // every per-type `layout.ts`/`renderer.ts` below needs the positioned-
14
+ // diagram types `mermaid-parser`'s `types.ts` halves own, and
15
+ // `class/layout.ts` needs `formatClassMember` from `mermaid-parser`'s
16
+ // `class/format.ts`. It deliberately does NOT re-export the internal
17
+ // `layout-engine/` modules the umbrella never imports
18
+ // (`constants.ts` — whose `DEFAULTS` would collide with `core`'s theme
19
+ // `DEFAULTS` anyway — plus `edge-bundling.ts`, `from-elk.ts`,
20
+ // `layer-alignment.ts` and `to-elk.ts`): they are `layoutGraphSync()`'s
21
+ // implementation, not its API. `elk-graph-builder.ts` is the exception:
22
+ // `class/layout.ts` and `er/layout.ts` (moved into this package under
23
+ // #624) call its primitives directly (zombie-mermaid#616), so it is part
24
+ // of the public API alongside `elk-adapter-utils.ts`.
25
+ //
26
+ // `layout.ts` re-exports `layoutGraphSync` from `layout-engine.ts`, so the
27
+ // two star-exports below resolve to one and the same binding — legal, and
28
+ // not an ambiguous re-export. It stays a module of its own (rather than
29
+ // being folded away here) because #625 is a move, not a redesign.
30
+ //
31
+ // `class/`, `er/`, `sequence/`, `xychart/` hold each diagram type's
32
+ // renderer half (`layout.ts` + `renderer.ts`) — the other half
33
+ // (`parser.ts`/`types.ts`, plus `class/format.ts`, `sequence/box-color.ts`,
34
+ // `sequence/activation-check.ts`, `xychart/colors.ts`) moved to
35
+ // `@zombie-mermaid/mermaid-parser` under the same issue (#624), per the
36
+ // scoping doc's finding 1: each per-type directory used to mix both halves
37
+ // in one place, and the split point already existed at the file level.
38
+ // ============================================================================
39
+
40
+ export * from './edge-curves.ts'
41
+ export * from './elk-instance.ts'
42
+ export * from './layout.ts'
43
+ export * from './layout-engine.ts'
44
+ export * from './layout-engine/elk-adapter-utils.ts'
45
+ export * from './layout-engine/elk-graph-builder.ts'
46
+ export * from './renderer.ts'
47
+ export * from './resolve-colors.ts'
48
+ export * from './shape-clipping.ts'
49
+ export * from './styles.ts'
50
+
51
+ export * from './class/layout.ts'
52
+ export * from './class/renderer.ts'
53
+ export * from './er/layout.ts'
54
+ export * from './er/renderer.ts'
55
+ export * from './sequence/layout.ts'
56
+ export * from './sequence/renderer.ts'
57
+ export * from './xychart/layout.ts'
58
+ export * from './xychart/renderer.ts'
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared defaults for the ELK-based layout engine.
3
+ * Split out of layout-engine.ts so both the ELK conversion and the
4
+ * post-processing stages (layer alignment, elk-result conversion) can
5
+ * reference the same values without importing the top-level module.
6
+ */
7
+
8
+ import { FONT_SIZES } from '../styles.ts'
9
+
10
+ /** Default render options (layout-only) */
11
+ export const DEFAULTS = {
12
+ font: 'Inter',
13
+ padding: 40,
14
+ nodeSpacing: 28,
15
+ layerSpacing: 48,
16
+ mergeEdges: true,
17
+ thoroughness: 3,
18
+ fontSizes: FONT_SIZES,
19
+ } as const