@diagrammo/dgmo 0.47.0 → 0.48.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/auto.js +1 -1
- package/dist/auto.mjs +1 -1
- package/dist/element.js +1 -1
- package/dist/element.mjs +1 -1
- package/package.json +19 -70
- package/dist/advanced.cjs +0 -71648
- package/dist/advanced.d.cts +0 -5719
- package/dist/auto.cjs +0 -70648
- package/dist/auto.d.cts +0 -54
- package/dist/block.cjs +0 -70208
- package/dist/block.d.cts +0 -115
- package/dist/editor.cjs +0 -526
- package/dist/editor.d.cts +0 -26
- package/dist/element.cjs +0 -70530
- package/dist/element.d.cts +0 -62
- package/dist/highlight.cjs +0 -852
- package/dist/highlight.d.cts +0 -49
- package/dist/index.cjs +0 -70491
- package/dist/index.d.cts +0 -160
- package/dist/pert.cjs +0 -325
- package/dist/pert.d.cts +0 -559
- package/dist/themes-1-CuKpeH.d.cts +0 -908
- package/src/advanced.ts +0 -873
- package/src/arc/parser.ts +0 -15
- package/src/arc/renderer.ts +0 -633
- package/src/auto/index.ts +0 -621
- package/src/auto/shared.ts +0 -370
- package/src/auto/styles.ts +0 -73
- package/src/block/layout.ts +0 -198
- package/src/block/parser.ts +0 -427
- package/src/block/renderer.ts +0 -484
- package/src/block/types.ts +0 -67
- package/src/boxes-and-lines/collapse.ts +0 -96
- package/src/boxes-and-lines/focus.ts +0 -257
- package/src/boxes-and-lines/label-placement.ts +0 -339
- package/src/boxes-and-lines/layout-grouped.ts +0 -912
- package/src/boxes-and-lines/layout-layered.ts +0 -722
- package/src/boxes-and-lines/layout-search.ts +0 -1678
- package/src/boxes-and-lines/layout.ts +0 -398
- package/src/boxes-and-lines/parser.ts +0 -1181
- package/src/boxes-and-lines/renderer.ts +0 -1465
- package/src/boxes-and-lines/types.ts +0 -66
- package/src/c4/layout.ts +0 -2441
- package/src/c4/parser.ts +0 -1158
- package/src/c4/renderer.ts +0 -2008
- package/src/c4/types.ts +0 -89
- package/src/chart-type-registry.ts +0 -529
- package/src/chart-types.ts +0 -225
- package/src/chart.ts +0 -685
- package/src/charts-d3/bar.ts +0 -338
- package/src/charts-d3/chord.ts +0 -140
- package/src/charts-d3/function.ts +0 -147
- package/src/charts-d3/funnel.ts +0 -104
- package/src/charts-d3/heatmap.ts +0 -145
- package/src/charts-d3/index.ts +0 -337
- package/src/charts-d3/interactions.ts +0 -477
- package/src/charts-d3/line.ts +0 -344
- package/src/charts-d3/mount.ts +0 -151
- package/src/charts-d3/pie.ts +0 -117
- package/src/charts-d3/polar.ts +0 -89
- package/src/charts-d3/radar.ts +0 -0
- package/src/charts-d3/sankey.ts +0 -0
- package/src/charts-d3/scatter.ts +0 -185
- package/src/charts-d3/shared.ts +0 -208
- package/src/class/layout.ts +0 -326
- package/src/class/parser.ts +0 -635
- package/src/class/renderer.ts +0 -793
- package/src/class/types.ts +0 -58
- package/src/cli-banner.ts +0 -106
- package/src/cli.ts +0 -1080
- package/src/colors-diagnostics.ts +0 -35
- package/src/colors.ts +0 -379
- package/src/completion-types.ts +0 -27
- package/src/completion.ts +0 -2668
- package/src/cycle/layout.ts +0 -1026
- package/src/cycle/parser.ts +0 -387
- package/src/cycle/renderer.ts +0 -653
- package/src/cycle/types.ts +0 -101
- package/src/d3.ts +0 -1434
- package/src/data-chart-parser.ts +0 -1007
- package/src/dgmo-router.ts +0 -296
- package/src/diagnostics-registry.ts +0 -108
- package/src/diagnostics.ts +0 -0
- package/src/dimensions.ts +0 -20
- package/src/directives-registry.ts +0 -396
- package/src/editor/dgmo.grammar +0 -76
- package/src/editor/dgmo.grammar.d.ts +0 -2
- package/src/editor/dgmo.grammar.js +0 -18
- package/src/editor/dgmo.grammar.terms.d.ts +0 -5
- package/src/editor/dgmo.grammar.terms.js +0 -35
- package/src/editor/highlight-api.ts +0 -596
- package/src/editor/highlight.ts +0 -36
- package/src/editor/index.ts +0 -27
- package/src/editor/keywords.ts +0 -128
- package/src/editor/tokens.ts +0 -30
- package/src/element/index.ts +0 -432
- package/src/embed/css.ts +0 -312
- package/src/embed/escape.ts +0 -15
- package/src/embed/index.ts +0 -280
- package/src/er/classify.ts +0 -228
- package/src/er/layout.ts +0 -530
- package/src/er/parser.ts +0 -750
- package/src/er/renderer.ts +0 -725
- package/src/er/types.ts +0 -52
- package/src/error-card.ts +0 -269
- package/src/event-line/diagnostics.ts +0 -117
- package/src/event-line/parser.ts +0 -643
- package/src/event-line/renderer.ts +0 -1742
- package/src/event-line/types.ts +0 -74
- package/src/fonts.ts +0 -3
- package/src/gantt/calculator.ts +0 -979
- package/src/gantt/parser.ts +0 -1641
- package/src/gantt/renderer.ts +0 -3787
- package/src/gantt/resolver.ts +0 -172
- package/src/gantt/types.ts +0 -206
- package/src/graph/diagnostics.ts +0 -64
- package/src/graph/edge-spline.ts +0 -29
- package/src/graph/flowchart-parser.ts +0 -759
- package/src/graph/flowchart-renderer.ts +0 -817
- package/src/graph/layout.ts +0 -472
- package/src/graph/notes.ts +0 -21
- package/src/graph/state-collapse.ts +0 -78
- package/src/graph/state-parser.ts +0 -574
- package/src/graph/state-renderer.ts +0 -653
- package/src/graph/types.ts +0 -65
- package/src/index.ts +0 -261
- package/src/infra/compute.ts +0 -1385
- package/src/infra/layout.ts +0 -888
- package/src/infra/parser.ts +0 -1234
- package/src/infra/renderer.ts +0 -2534
- package/src/infra/roles.ts +0 -75
- package/src/infra/types.ts +0 -216
- package/src/infra/validation.ts +0 -201
- package/src/journey-map/layout.ts +0 -490
- package/src/journey-map/parser.ts +0 -557
- package/src/journey-map/renderer.ts +0 -1580
- package/src/journey-map/types.ts +0 -47
- package/src/kanban/mutations.ts +0 -175
- package/src/kanban/parser.ts +0 -509
- package/src/kanban/renderer.ts +0 -1449
- package/src/kanban/types.ts +0 -39
- package/src/label-layout.ts +0 -368
- package/src/map/colorize.ts +0 -54
- package/src/map/completion.ts +0 -316
- package/src/map/context-labels.ts +0 -609
- package/src/map/data/PROVENANCE.json +0 -1
- package/src/map/data/README.md +0 -40
- package/src/map/data/airport-collisions.json +0 -1
- package/src/map/data/airports.json +0 -1
- package/src/map/data/gazetteer.json +0 -1
- package/src/map/data/lakes.json +0 -1
- package/src/map/data/mountain-ranges.json +0 -1
- package/src/map/data/na-lakes.json +0 -1
- package/src/map/data/na-land.json +0 -1
- package/src/map/data/region-names.json +0 -1
- package/src/map/data/rivers.json +0 -1
- package/src/map/data/types.ts +0 -121
- package/src/map/data/us-states.json +0 -1
- package/src/map/data/water-bodies.json +0 -1
- package/src/map/data/world-coarse.json +0 -1
- package/src/map/data/world-detail.json +0 -1
- package/src/map/diagnostics.ts +0 -134
- package/src/map/dimensions.ts +0 -150
- package/src/map/geo-query.ts +0 -295
- package/src/map/geo.ts +0 -438
- package/src/map/invert.ts +0 -111
- package/src/map/layout.ts +0 -5069
- package/src/map/legend-band.ts +0 -127
- package/src/map/load-data.ts +0 -186
- package/src/map/parser.ts +0 -783
- package/src/map/renderer.ts +0 -1445
- package/src/map/resolved-types.ts +0 -160
- package/src/map/resolver.ts +0 -1146
- package/src/map/types.ts +0 -194
- package/src/mindmap/collapse.ts +0 -67
- package/src/mindmap/layout.ts +0 -649
- package/src/mindmap/parser.ts +0 -430
- package/src/mindmap/renderer.ts +0 -637
- package/src/mindmap/text-wrap.ts +0 -218
- package/src/mindmap/types.ts +0 -56
- package/src/org/collapse.ts +0 -151
- package/src/org/layout.ts +0 -1262
- package/src/org/parser.ts +0 -519
- package/src/org/renderer.ts +0 -866
- package/src/org/resolver.ts +0 -636
- package/src/palettes/atlas.ts +0 -77
- package/src/palettes/blueprint.ts +0 -73
- package/src/palettes/catppuccin.ts +0 -84
- package/src/palettes/color-utils.ts +0 -443
- package/src/palettes/index.ts +0 -67
- package/src/palettes/nord.ts +0 -67
- package/src/palettes/registry.ts +0 -114
- package/src/palettes/slate.ts +0 -73
- package/src/palettes/tidewater.ts +0 -73
- package/src/palettes/tokyo-night.ts +0 -86
- package/src/palettes/types.ts +0 -88
- package/src/pert/analyzer.ts +0 -1183
- package/src/pert/diagnostics.ts +0 -110
- package/src/pert/index.ts +0 -43
- package/src/pert/internal.ts +0 -252
- package/src/pert/layout.ts +0 -971
- package/src/pert/monte-carlo.ts +0 -446
- package/src/pert/parser.ts +0 -1606
- package/src/pert/renderer.ts +0 -4009
- package/src/pert/share-normalize.ts +0 -62
- package/src/pert/types.ts +0 -604
- package/src/pyramid/parser.ts +0 -185
- package/src/pyramid/renderer.ts +0 -768
- package/src/pyramid/types.ts +0 -28
- package/src/quadrant/parser.ts +0 -18
- package/src/quadrant/renderer.ts +0 -638
- package/src/raci/diagnostics.ts +0 -213
- package/src/raci/index.ts +0 -23
- package/src/raci/mutations.ts +0 -321
- package/src/raci/parser.ts +0 -699
- package/src/raci/renderer.ts +0 -1627
- package/src/raci/types.ts +0 -82
- package/src/raci/variants.ts +0 -320
- package/src/render.ts +0 -268
- package/src/ring/parser.ts +0 -212
- package/src/ring/renderer.ts +0 -490
- package/src/ring/types.ts +0 -27
- package/src/sequence/collapse.ts +0 -174
- package/src/sequence/parser.ts +0 -1596
- package/src/sequence/participant-inference.ts +0 -152
- package/src/sequence/renderer.ts +0 -3168
- package/src/sequence/tag-resolution.ts +0 -164
- package/src/sharing.ts +0 -211
- package/src/sitemap/collapse.ts +0 -100
- package/src/sitemap/layout.ts +0 -925
- package/src/sitemap/parser.ts +0 -755
- package/src/sitemap/renderer.ts +0 -828
- package/src/sitemap/types.ts +0 -42
- package/src/slope/parser.ts +0 -18
- package/src/slope/renderer.ts +0 -378
- package/src/swimlane/diagnostics.ts +0 -94
- package/src/swimlane/layout.ts +0 -597
- package/src/swimlane/parser.ts +0 -613
- package/src/swimlane/renderer.ts +0 -681
- package/src/swimlane/types.ts +0 -151
- package/src/tech-radar/interactive.ts +0 -1129
- package/src/tech-radar/layout.ts +0 -197
- package/src/tech-radar/parser.ts +0 -436
- package/src/tech-radar/renderer.ts +0 -1239
- package/src/tech-radar/shared.ts +0 -151
- package/src/tech-radar/types.ts +0 -86
- package/src/themes.ts +0 -22
- package/src/timeline/parser.ts +0 -472
- package/src/timeline/renderer.ts +0 -3024
- package/src/timeline/types.ts +0 -33
- package/src/timeline/viz-parser.ts +0 -21
- package/src/treemap/diagnostics.ts +0 -62
- package/src/treemap/layout-radial.ts +0 -233
- package/src/treemap/layout.ts +0 -189
- package/src/treemap/parser.ts +0 -463
- package/src/treemap/renderer-radial.ts +0 -438
- package/src/treemap/renderer.ts +0 -572
- package/src/treemap/treemap-shared.ts +0 -309
- package/src/treemap/types.ts +0 -60
- package/src/utils/arrow-markers.ts +0 -51
- package/src/utils/arrows-diagnostics.ts +0 -62
- package/src/utils/arrows.ts +0 -227
- package/src/utils/brand.ts +0 -61
- package/src/utils/card.ts +0 -183
- package/src/utils/collapse-engine/tree-edges.ts +0 -115
- package/src/utils/collapse-engine/tree.ts +0 -117
- package/src/utils/d3-helpers.ts +0 -219
- package/src/utils/d3-types.ts +0 -9
- package/src/utils/description-helpers.ts +0 -38
- package/src/utils/duration.ts +0 -312
- package/src/utils/export-container.ts +0 -41
- package/src/utils/expr-eval.ts +0 -27
- package/src/utils/fit-canvas.ts +0 -64
- package/src/utils/hover-styles.ts +0 -673
- package/src/utils/inline-markdown.ts +0 -96
- package/src/utils/legend-constants.ts +0 -48
- package/src/utils/legend-d3.ts +0 -618
- package/src/utils/legend-integration.ts +0 -107
- package/src/utils/legend-layout.ts +0 -776
- package/src/utils/legend-svg.ts +0 -214
- package/src/utils/legend-types.ts +0 -272
- package/src/utils/name-normalize.ts +0 -163
- package/src/utils/note-box/constants.ts +0 -25
- package/src/utils/note-box/index.ts +0 -11
- package/src/utils/note-box/metrics.ts +0 -90
- package/src/utils/note-box/svg.ts +0 -331
- package/src/utils/notes/bounds.ts +0 -30
- package/src/utils/notes/build.ts +0 -131
- package/src/utils/notes/index.ts +0 -18
- package/src/utils/notes/model.ts +0 -19
- package/src/utils/notes/parse.ts +0 -131
- package/src/utils/notes/place.ts +0 -177
- package/src/utils/notes/resolve.ts +0 -88
- package/src/utils/number-format.ts +0 -36
- package/src/utils/parsing.ts +0 -953
- package/src/utils/reserved-key-registry.ts +0 -239
- package/src/utils/safe-href.ts +0 -38
- package/src/utils/scaling.ts +0 -97
- package/src/utils/svg-embed.ts +0 -251
- package/src/utils/tag-groups.ts +0 -607
- package/src/utils/text-measure.ts +0 -122
- package/src/utils/time-ticks.ts +0 -253
- package/src/utils/title-constants.ts +0 -24
- package/src/utils/visual-conventions.ts +0 -61
- package/src/utils/wrapped-desc.ts +0 -87
- package/src/venn/parser.ts +0 -18
- package/src/venn/renderer.ts +0 -1047
- package/src/version-control/diagnostics.ts +0 -67
- package/src/version-control/parser.ts +0 -544
- package/src/version-control/renderer.ts +0 -724
- package/src/version-control/types.ts +0 -100
- package/src/visualizations/parse.ts +0 -1560
- package/src/visualizations/types.ts +0 -262
- package/src/wireframe/layout.ts +0 -479
- package/src/wireframe/parser.ts +0 -1114
- package/src/wireframe/renderer.ts +0 -1318
- package/src/wireframe/types.ts +0 -110
- package/src/wordcloud/parser.ts +0 -18
- package/src/wordcloud/renderer.ts +0 -357
package/src/cycle/renderer.ts
DELETED
|
@@ -1,653 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Cycle Diagram — D3 SVG Renderer
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import * as d3Selection from 'd3-selection';
|
|
6
|
-
import { FONT_FAMILY } from '../fonts';
|
|
7
|
-
import {
|
|
8
|
-
TITLE_FONT_SIZE,
|
|
9
|
-
TITLE_FONT_WEIGHT,
|
|
10
|
-
TITLE_Y,
|
|
11
|
-
} from '../utils/title-constants';
|
|
12
|
-
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
13
|
-
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
14
|
-
import type {
|
|
15
|
-
LegendCallbacks,
|
|
16
|
-
ControlsGroupToggle,
|
|
17
|
-
} from '../utils/legend-types';
|
|
18
|
-
import { contrastText, shapeFill } from '../palettes/color-utils';
|
|
19
|
-
import { resolveColor } from '../colors';
|
|
20
|
-
import { renderInlineText } from '../utils/inline-markdown';
|
|
21
|
-
import type { PaletteColors } from '../palettes';
|
|
22
|
-
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
23
|
-
import type { CompactViewState } from '../sharing';
|
|
24
|
-
import {
|
|
25
|
-
DEFAULT_EDGE_WIDTH,
|
|
26
|
-
MIN_EDGE_WIDTH,
|
|
27
|
-
arrowHeadLength,
|
|
28
|
-
type ParsedCycle,
|
|
29
|
-
} from './types';
|
|
30
|
-
import { computeCycleLayout, wrapEdgeLabelText } from './layout';
|
|
31
|
-
import { ScaleContext } from '../utils/scaling';
|
|
32
|
-
import { measureText } from '../utils/text-measure';
|
|
33
|
-
|
|
34
|
-
// ── Constants ────────────────────────────────────────────────
|
|
35
|
-
const NODE_FONT_SIZE = 13;
|
|
36
|
-
const DESC_FONT_SIZE = 11;
|
|
37
|
-
const EDGE_LABEL_FONT_SIZE = 11;
|
|
38
|
-
const DESC_LINE_HEIGHT = 15;
|
|
39
|
-
const TITLE_AREA_HEIGHT = 50;
|
|
40
|
-
|
|
41
|
-
export interface CycleRenderOptions {
|
|
42
|
-
onClickItem?: (lineNumber: number) => void;
|
|
43
|
-
exportDims?: D3ExportDimensions;
|
|
44
|
-
viewState?: CompactViewState;
|
|
45
|
-
hideDescriptions?: boolean;
|
|
46
|
-
controlsExpanded?: boolean;
|
|
47
|
-
onToggleDescriptions?: (active: boolean) => void;
|
|
48
|
-
onToggleControlsExpand?: () => void;
|
|
49
|
-
exportMode?: boolean;
|
|
50
|
-
/** When 'app', the description toggle is hosted by the app overlay strip:
|
|
51
|
-
* the inline gear is suppressed and a controls row + anchor are reserved.
|
|
52
|
-
* Default (inline) renders the gear as before. */
|
|
53
|
-
controlsHost?: 'app' | 'inline';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Render a cycle diagram into the given container.
|
|
58
|
-
*/
|
|
59
|
-
export function renderCycle(
|
|
60
|
-
container: HTMLDivElement,
|
|
61
|
-
parsed: ParsedCycle,
|
|
62
|
-
palette: PaletteColors,
|
|
63
|
-
isDark: boolean,
|
|
64
|
-
onClickItem?: (lineNumber: number) => void,
|
|
65
|
-
exportDims?: D3ExportDimensions,
|
|
66
|
-
viewState?: CompactViewState,
|
|
67
|
-
renderOptions?: CycleRenderOptions
|
|
68
|
-
): void {
|
|
69
|
-
if (parsed.nodes.length === 0) return;
|
|
70
|
-
const solid = parsed.options?.['solid-fill'] === 'on';
|
|
71
|
-
|
|
72
|
-
// Clear previous render
|
|
73
|
-
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
74
|
-
const width = exportDims?.width ?? container.clientWidth;
|
|
75
|
-
const height = exportDims?.height ?? container.clientHeight;
|
|
76
|
-
if (width <= 0 || height <= 0) return;
|
|
77
|
-
|
|
78
|
-
const idealWidth = width;
|
|
79
|
-
const sctx = exportDims
|
|
80
|
-
? ScaleContext.identity()
|
|
81
|
-
: ScaleContext.from(width, idealWidth);
|
|
82
|
-
|
|
83
|
-
const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
|
|
84
|
-
const sTitleY = sctx.structural(TITLE_Y);
|
|
85
|
-
const sTitleAreaHeight = sctx.structural(TITLE_AREA_HEIGHT);
|
|
86
|
-
const sLegendHeight = sctx.structural(LEGEND_HEIGHT);
|
|
87
|
-
|
|
88
|
-
const hideDescriptions =
|
|
89
|
-
(renderOptions?.hideDescriptions ?? false) ||
|
|
90
|
-
parsed.options['no-descriptions'] === 'true' ||
|
|
91
|
-
viewState?.hd === true;
|
|
92
|
-
const showDescriptions = !hideDescriptions;
|
|
93
|
-
|
|
94
|
-
// Check if descriptions exist in the diagram
|
|
95
|
-
const hasDescriptions =
|
|
96
|
-
parsed.nodes.some((n) => n.description.length > 0) ||
|
|
97
|
-
parsed.edges.some((e) => e.description.length > 0);
|
|
98
|
-
// App-hosted: controls live in the app overlay strip. Cycle has no tag groups,
|
|
99
|
-
// so there's no in-SVG legend left to render — don't reserve a legend band.
|
|
100
|
-
const appHostedControls = renderOptions?.controlsHost === 'app';
|
|
101
|
-
const hasLegend =
|
|
102
|
-
!appHostedControls &&
|
|
103
|
-
hasDescriptions &&
|
|
104
|
-
!!renderOptions?.onToggleDescriptions;
|
|
105
|
-
|
|
106
|
-
const showTitle = !!parsed.title && parsed.options['no-title'] !== 'on';
|
|
107
|
-
const legendOffset = hasLegend ? sLegendHeight : 0;
|
|
108
|
-
const layoutHeight =
|
|
109
|
-
height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
|
|
110
|
-
const layout = computeCycleLayout(parsed, {
|
|
111
|
-
width,
|
|
112
|
-
height: layoutHeight,
|
|
113
|
-
hideDescriptions,
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const svg = d3Selection
|
|
117
|
-
.select(container)
|
|
118
|
-
.append('svg')
|
|
119
|
-
.attr('width', width)
|
|
120
|
-
.attr('height', height)
|
|
121
|
-
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
122
|
-
.attr('preserveAspectRatio', 'xMidYMid meet')
|
|
123
|
-
.attr('xmlns', 'http://www.w3.org/2000/svg')
|
|
124
|
-
.style('font-family', FONT_FAMILY);
|
|
125
|
-
|
|
126
|
-
if (sctx.isBelowFloor) {
|
|
127
|
-
svg.attr('width', '100%');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
svg
|
|
131
|
-
.append('rect')
|
|
132
|
-
.attr('width', width)
|
|
133
|
-
.attr('height', height)
|
|
134
|
-
.attr('fill', palette.bg);
|
|
135
|
-
|
|
136
|
-
if (showTitle) {
|
|
137
|
-
const titleText = svg
|
|
138
|
-
.append('text')
|
|
139
|
-
.attr('x', width / 2)
|
|
140
|
-
.attr('y', sTitleY)
|
|
141
|
-
.attr('text-anchor', 'middle')
|
|
142
|
-
.attr('fill', palette.text)
|
|
143
|
-
.attr('font-family', FONT_FAMILY)
|
|
144
|
-
.attr('font-size', sTitleFontSize)
|
|
145
|
-
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
146
|
-
.attr('data-line-number', parsed.titleLineNumber)
|
|
147
|
-
.text(parsed.title)
|
|
148
|
-
.style('cursor', onClickItem ? 'pointer' : 'default');
|
|
149
|
-
if (onClickItem) {
|
|
150
|
-
titleText.on('click', () => onClickItem(parsed.titleLineNumber));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Legend (controls toggle for descriptions)
|
|
155
|
-
if (hasLegend) {
|
|
156
|
-
const controlsGroup: { toggles: ControlsGroupToggle[] } = {
|
|
157
|
-
toggles: [
|
|
158
|
-
{
|
|
159
|
-
id: 'descriptions',
|
|
160
|
-
type: 'toggle',
|
|
161
|
-
label: 'Descriptions',
|
|
162
|
-
active: !hideDescriptions,
|
|
163
|
-
onToggle: () => {},
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
};
|
|
167
|
-
const legendCallbacks: LegendCallbacks = {
|
|
168
|
-
...(renderOptions?.onToggleControlsExpand !== undefined && {
|
|
169
|
-
onControlsExpand: renderOptions.onToggleControlsExpand,
|
|
170
|
-
}),
|
|
171
|
-
onControlsToggle: (toggleId, active) => {
|
|
172
|
-
if (
|
|
173
|
-
toggleId === 'descriptions' &&
|
|
174
|
-
renderOptions?.onToggleDescriptions
|
|
175
|
-
) {
|
|
176
|
-
renderOptions.onToggleDescriptions(active);
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
};
|
|
180
|
-
const titleOffsetForLegend = showTitle ? sTitleAreaHeight : 0;
|
|
181
|
-
const legendG = svg
|
|
182
|
-
.append('g')
|
|
183
|
-
.attr('transform', `translate(0, ${titleOffsetForLegend + 4})`);
|
|
184
|
-
renderIntegratedLegend(legendG, {
|
|
185
|
-
groups: [],
|
|
186
|
-
activeGroup: null,
|
|
187
|
-
mode: renderOptions?.exportMode ? 'export' : 'preview',
|
|
188
|
-
controlsGroup,
|
|
189
|
-
...(renderOptions?.controlsHost !== undefined && {
|
|
190
|
-
controlsHost: renderOptions.controlsHost,
|
|
191
|
-
}),
|
|
192
|
-
...(renderOptions?.controlsExpanded !== undefined && {
|
|
193
|
-
controlsExpanded: renderOptions.controlsExpanded,
|
|
194
|
-
}),
|
|
195
|
-
callbacks: legendCallbacks,
|
|
196
|
-
palette,
|
|
197
|
-
isDark,
|
|
198
|
-
width,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const diagramTop = (showTitle ? sTitleAreaHeight : 0) + legendOffset;
|
|
203
|
-
const g = svg.append('g').attr('transform', `translate(0, ${diagramTop})`);
|
|
204
|
-
|
|
205
|
-
// Defs for arrowheads
|
|
206
|
-
const defs = svg.append('defs');
|
|
207
|
-
|
|
208
|
-
// Resolve default node color: first palette color (uniform)
|
|
209
|
-
const defaultNodeColor = palette.primary;
|
|
210
|
-
|
|
211
|
-
// ── Arrowhead markers (per color+width, markerUnits=strokeWidth) ──
|
|
212
|
-
const markerKeys = new Set<string>();
|
|
213
|
-
for (const edge of parsed.edges) {
|
|
214
|
-
const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
|
|
215
|
-
const sw = Math.max(edge.width ?? DEFAULT_EDGE_WIDTH, MIN_EDGE_WIDTH);
|
|
216
|
-
const key = `${color}|${sw}`;
|
|
217
|
-
if (!markerKeys.has(key)) {
|
|
218
|
-
markerKeys.add(key);
|
|
219
|
-
ensureArrowMarker(defs, color, sw);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ── Render edges paths (below nodes); labels rendered later, on top ──
|
|
224
|
-
const edgeLabelInfo: Array<{
|
|
225
|
-
le: (typeof layout.edges)[number];
|
|
226
|
-
edge: (typeof parsed.edges)[number];
|
|
227
|
-
}> = [];
|
|
228
|
-
for (let i = 0; i < layout.edges.length; i++) {
|
|
229
|
-
// In-bounds by loop guard; layout.edges and parsed.edges are parallel arrays.
|
|
230
|
-
const le = layout.edges[i]!;
|
|
231
|
-
const edge = parsed.edges[i]!;
|
|
232
|
-
const color = resolveEdgeColor(edge, parsed, palette, defaultNodeColor);
|
|
233
|
-
const strokeWidth = Math.max(
|
|
234
|
-
edge.width ?? DEFAULT_EDGE_WIDTH,
|
|
235
|
-
MIN_EDGE_WIDTH
|
|
236
|
-
);
|
|
237
|
-
const markerId = arrowMarkerId(color, strokeWidth);
|
|
238
|
-
|
|
239
|
-
const edgeG = g.append('g').attr('class', 'cycle-edge');
|
|
240
|
-
|
|
241
|
-
if (edge.lineNumber) {
|
|
242
|
-
edgeG.attr('data-line-number', edge.lineNumber);
|
|
243
|
-
}
|
|
244
|
-
// Endpoint node indices for baked-CSS connection-highlight.
|
|
245
|
-
edgeG
|
|
246
|
-
.attr('data-from', String(edge.sourceIndex))
|
|
247
|
-
.attr('data-to', String(edge.targetIndex));
|
|
248
|
-
|
|
249
|
-
const pathEl = edgeG
|
|
250
|
-
.append('path')
|
|
251
|
-
.attr('d', le.path)
|
|
252
|
-
.attr('fill', 'none')
|
|
253
|
-
.attr('stroke', color)
|
|
254
|
-
.attr('stroke-width', strokeWidth)
|
|
255
|
-
.attr('marker-end', `url(#${markerId})`);
|
|
256
|
-
|
|
257
|
-
if (onClickItem && edge.lineNumber) {
|
|
258
|
-
const ln = edge.lineNumber;
|
|
259
|
-
pathEl.style('cursor', 'pointer').on('click', () => onClickItem(ln));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
edgeLabelInfo.push({ le, edge });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ── Render nodes ──
|
|
266
|
-
const HEADER_H = 36 * layout.scale;
|
|
267
|
-
// Font floors are intentionally low (≈ half the natural size) so text keeps
|
|
268
|
-
// shrinking alongside the shrinking nodes when the canvas is small. Higher
|
|
269
|
-
// floors decouple text from shape scale, causing labels to overflow node
|
|
270
|
-
// boundaries at small canvas sizes.
|
|
271
|
-
const scaledNodeFont = Math.max(6, Math.round(NODE_FONT_SIZE * layout.scale));
|
|
272
|
-
const scaledEdgeLabelFont = Math.max(
|
|
273
|
-
6,
|
|
274
|
-
Math.round(EDGE_LABEL_FONT_SIZE * layout.scale)
|
|
275
|
-
);
|
|
276
|
-
const CIRCLE_LABEL_FONT_SIZE = 16;
|
|
277
|
-
const scaledCircleLabelFont = Math.max(
|
|
278
|
-
8,
|
|
279
|
-
Math.round(CIRCLE_LABEL_FONT_SIZE * layout.scale)
|
|
280
|
-
);
|
|
281
|
-
const scaledDescFont = Math.max(5, Math.round(DESC_FONT_SIZE * layout.scale));
|
|
282
|
-
const scaledDescLineH = Math.max(
|
|
283
|
-
7,
|
|
284
|
-
Math.round(DESC_LINE_HEIGHT * layout.scale)
|
|
285
|
-
);
|
|
286
|
-
const scaledEdgeLineH = Math.max(
|
|
287
|
-
8,
|
|
288
|
-
Math.round(DESC_LINE_HEIGHT * layout.scale)
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
for (let i = 0; i < layout.nodes.length; i++) {
|
|
292
|
-
// In-bounds by loop guard; layout.nodes and parsed.nodes are parallel arrays.
|
|
293
|
-
const ln = layout.nodes[i]!;
|
|
294
|
-
const node = parsed.nodes[i]!;
|
|
295
|
-
const solidColor = resolveNodeColor(node.color, palette, defaultNodeColor);
|
|
296
|
-
// Canonical 25% tinted fill via shapeFill() (or full intent when solid-fill is on).
|
|
297
|
-
const fillColor = shapeFill(palette, solidColor, isDark, { solid });
|
|
298
|
-
const textColor = contrastText(
|
|
299
|
-
fillColor,
|
|
300
|
-
palette.textOnFillLight,
|
|
301
|
-
palette.textOnFillDark
|
|
302
|
-
);
|
|
303
|
-
// Description text sits on top of the node fill — must follow the same
|
|
304
|
-
// contrast rule as the title, NOT use a fixed `palette.textMuted` gray
|
|
305
|
-
// (which is only legible against bg/surface, not against saturated fills).
|
|
306
|
-
const descColor = textColor;
|
|
307
|
-
const nodeW = ln.width;
|
|
308
|
-
const nodeH = ln.height;
|
|
309
|
-
const wrappedDesc = ln.wrappedDesc;
|
|
310
|
-
const hasDesc = showDescriptions && wrappedDesc.length > 0;
|
|
311
|
-
|
|
312
|
-
const nodeG = g
|
|
313
|
-
.append('g')
|
|
314
|
-
.attr('class', 'cycle-node')
|
|
315
|
-
.attr('data-line-number', node.lineNumber)
|
|
316
|
-
// Node index for baked-CSS connection-highlight (cycle nodes have no
|
|
317
|
-
// string id; edges key on the same index).
|
|
318
|
-
.attr('data-node-index', String(i))
|
|
319
|
-
.style('cursor', onClickItem ? 'pointer' : 'default');
|
|
320
|
-
|
|
321
|
-
if (onClickItem) {
|
|
322
|
-
const lineNum = node.lineNumber;
|
|
323
|
-
nodeG.on('click', () => onClickItem(lineNum));
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (ln.isCircle) {
|
|
327
|
-
// ── Circle node shape ──
|
|
328
|
-
const r = nodeW / 2;
|
|
329
|
-
nodeG
|
|
330
|
-
.append('circle')
|
|
331
|
-
.attr('cx', ln.x)
|
|
332
|
-
.attr('cy', ln.y)
|
|
333
|
-
.attr('r', r)
|
|
334
|
-
.attr('fill', fillColor)
|
|
335
|
-
.attr('stroke', solidColor)
|
|
336
|
-
.attr('stroke-width', 2);
|
|
337
|
-
|
|
338
|
-
if (hasDesc) {
|
|
339
|
-
// Label + descriptions vertically centered in circle
|
|
340
|
-
const labelFont = scaledCircleLabelFont;
|
|
341
|
-
const blockH = labelFont + 4 + wrappedDesc.length * scaledDescLineH;
|
|
342
|
-
const startY = ln.y - blockH / 2 + labelFont;
|
|
343
|
-
|
|
344
|
-
const labelText = nodeG
|
|
345
|
-
.append('text')
|
|
346
|
-
.attr('x', ln.x)
|
|
347
|
-
.attr('y', startY)
|
|
348
|
-
.attr('text-anchor', 'middle')
|
|
349
|
-
.attr('fill', textColor)
|
|
350
|
-
.attr('font-family', FONT_FAMILY)
|
|
351
|
-
.attr('font-size', labelFont)
|
|
352
|
-
.attr('font-weight', '600');
|
|
353
|
-
renderInlineText(labelText, node.label, palette, labelFont);
|
|
354
|
-
|
|
355
|
-
let descY = startY + scaledDescLineH + 4;
|
|
356
|
-
wrappedDesc.forEach((line) => {
|
|
357
|
-
const descText = nodeG
|
|
358
|
-
.append('text')
|
|
359
|
-
.attr('x', ln.x)
|
|
360
|
-
.attr('y', descY)
|
|
361
|
-
.attr('text-anchor', 'middle')
|
|
362
|
-
.attr('fill', descColor)
|
|
363
|
-
.attr('font-family', FONT_FAMILY)
|
|
364
|
-
.attr('font-size', scaledDescFont);
|
|
365
|
-
renderInlineText(descText, line.text, palette, DESC_FONT_SIZE);
|
|
366
|
-
descY += scaledDescLineH;
|
|
367
|
-
});
|
|
368
|
-
} else {
|
|
369
|
-
// Label centered in circle
|
|
370
|
-
const labelFont = scaledCircleLabelFont;
|
|
371
|
-
const labelText = nodeG
|
|
372
|
-
.append('text')
|
|
373
|
-
.attr('x', ln.x)
|
|
374
|
-
.attr('y', ln.y + labelFont / 3)
|
|
375
|
-
.attr('text-anchor', 'middle')
|
|
376
|
-
.attr('fill', textColor)
|
|
377
|
-
.attr('font-family', FONT_FAMILY)
|
|
378
|
-
.attr('font-size', labelFont)
|
|
379
|
-
.attr('font-weight', '600');
|
|
380
|
-
renderInlineText(labelText, node.label, palette, labelFont);
|
|
381
|
-
}
|
|
382
|
-
} else {
|
|
383
|
-
// ── Rectangular node shape ──
|
|
384
|
-
const rx = 6;
|
|
385
|
-
nodeG
|
|
386
|
-
.append('rect')
|
|
387
|
-
.attr('x', ln.x - nodeW / 2)
|
|
388
|
-
.attr('y', ln.y - nodeH / 2)
|
|
389
|
-
.attr('width', nodeW)
|
|
390
|
-
.attr('height', nodeH)
|
|
391
|
-
.attr('rx', rx)
|
|
392
|
-
.attr('ry', rx)
|
|
393
|
-
.attr('fill', fillColor)
|
|
394
|
-
.attr('stroke', solidColor)
|
|
395
|
-
.attr('stroke-width', 2);
|
|
396
|
-
|
|
397
|
-
if (hasDesc) {
|
|
398
|
-
// ── Described node: header + separator + description ──
|
|
399
|
-
const headerCenterY = ln.y - nodeH / 2 + HEADER_H / 2;
|
|
400
|
-
const labelText = nodeG
|
|
401
|
-
.append('text')
|
|
402
|
-
.attr('x', ln.x)
|
|
403
|
-
.attr('y', headerCenterY + scaledNodeFont / 3)
|
|
404
|
-
.attr('text-anchor', 'middle')
|
|
405
|
-
.attr('fill', textColor)
|
|
406
|
-
.attr('font-family', FONT_FAMILY)
|
|
407
|
-
.attr('font-size', scaledNodeFont)
|
|
408
|
-
.attr('font-weight', '600');
|
|
409
|
-
renderInlineText(labelText, node.label, palette, scaledNodeFont);
|
|
410
|
-
|
|
411
|
-
const sepY = ln.y - nodeH / 2 + HEADER_H;
|
|
412
|
-
nodeG
|
|
413
|
-
.append('line')
|
|
414
|
-
.attr('x1', ln.x - nodeW / 2)
|
|
415
|
-
.attr('y1', sepY)
|
|
416
|
-
.attr('x2', ln.x + nodeW / 2)
|
|
417
|
-
.attr('y2', sepY)
|
|
418
|
-
.attr('stroke', solid ? descColor : solidColor)
|
|
419
|
-
.attr('stroke-opacity', 0.3)
|
|
420
|
-
.attr('stroke-width', 1);
|
|
421
|
-
|
|
422
|
-
const descStartY = sepY + 4 + scaledDescFont;
|
|
423
|
-
const descPadX = Math.max(8, 12 * layout.scale);
|
|
424
|
-
const descX = ln.x - nodeW / 2 + descPadX;
|
|
425
|
-
// Bullet body column — body of bullet items + their continuations
|
|
426
|
-
// share this x so wrapped text aligns under the first word past "•".
|
|
427
|
-
const bulletBodyX = descX + Math.max(8, 12 * layout.scale);
|
|
428
|
-
wrappedDesc.forEach((line, li) => {
|
|
429
|
-
const lineY = descStartY + li * scaledDescLineH;
|
|
430
|
-
if (line.kind === 'bullet-first') {
|
|
431
|
-
// Bullet glyph as its own text element at descX
|
|
432
|
-
nodeG
|
|
433
|
-
.append('text')
|
|
434
|
-
.attr('x', descX)
|
|
435
|
-
.attr('y', lineY)
|
|
436
|
-
.attr('text-anchor', 'start')
|
|
437
|
-
.attr('fill', descColor)
|
|
438
|
-
.attr('font-family', FONT_FAMILY)
|
|
439
|
-
.attr('font-size', scaledDescFont)
|
|
440
|
-
.text('•');
|
|
441
|
-
// Body text at the bullet column
|
|
442
|
-
const bodyText = nodeG
|
|
443
|
-
.append('text')
|
|
444
|
-
.attr('x', bulletBodyX)
|
|
445
|
-
.attr('y', lineY)
|
|
446
|
-
.attr('text-anchor', 'start')
|
|
447
|
-
.attr('fill', descColor)
|
|
448
|
-
.attr('font-family', FONT_FAMILY)
|
|
449
|
-
.attr('font-size', scaledDescFont);
|
|
450
|
-
renderInlineText(bodyText, line.text, palette, DESC_FONT_SIZE);
|
|
451
|
-
} else {
|
|
452
|
-
const x = line.kind === 'bullet-cont' ? bulletBodyX : descX;
|
|
453
|
-
const descText = nodeG
|
|
454
|
-
.append('text')
|
|
455
|
-
.attr('x', x)
|
|
456
|
-
.attr('y', lineY)
|
|
457
|
-
.attr('text-anchor', 'start')
|
|
458
|
-
.attr('fill', descColor)
|
|
459
|
-
.attr('font-family', FONT_FAMILY)
|
|
460
|
-
.attr('font-size', scaledDescFont);
|
|
461
|
-
renderInlineText(descText, line.text, palette, DESC_FONT_SIZE);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
} else {
|
|
465
|
-
// ── Plain node: label centered ──
|
|
466
|
-
const labelText = nodeG
|
|
467
|
-
.append('text')
|
|
468
|
-
.attr('x', ln.x)
|
|
469
|
-
.attr('y', ln.y + scaledNodeFont / 3)
|
|
470
|
-
.attr('text-anchor', 'middle')
|
|
471
|
-
.attr('fill', textColor)
|
|
472
|
-
.attr('font-family', FONT_FAMILY)
|
|
473
|
-
.attr('font-size', scaledNodeFont)
|
|
474
|
-
.attr('font-weight', '600');
|
|
475
|
-
renderInlineText(labelText, node.label, palette, scaledNodeFont);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ── Render edge labels (in a new group appended after nodes so they sit
|
|
481
|
-
// on top of nodes in document order, regardless of append-time ordering). ──
|
|
482
|
-
const labelLayer = g.append('g').attr('class', 'cycle-edge-labels');
|
|
483
|
-
for (const { le, edge } of edgeLabelInfo) {
|
|
484
|
-
const hasEdgeLabel = !!le.label;
|
|
485
|
-
const hasEdgeDesc = showDescriptions && edge.description.length > 0;
|
|
486
|
-
const { labelLines, descLines } = wrapEdgeLabelText(
|
|
487
|
-
hasEdgeLabel ? le.label : undefined,
|
|
488
|
-
hasEdgeDesc ? edge.description : []
|
|
489
|
-
);
|
|
490
|
-
if (labelLines.length === 0 && descLines.length === 0) continue;
|
|
491
|
-
const edgeG = labelLayer.append('g').attr('class', 'cycle-edge');
|
|
492
|
-
if (edge.lineNumber) {
|
|
493
|
-
edgeG.attr('data-line-number', edge.lineNumber);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const normAngle =
|
|
497
|
-
((le.labelAngle % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI);
|
|
498
|
-
const isRight = normAngle < Math.PI * 0.4 || normAngle > Math.PI * 1.6;
|
|
499
|
-
const isLeft = normAngle > Math.PI * 0.6 && normAngle < Math.PI * 1.4;
|
|
500
|
-
const anchor = isRight ? 'start' : isLeft ? 'end' : 'middle';
|
|
501
|
-
|
|
502
|
-
const lineCount = labelLines.length + descLines.length;
|
|
503
|
-
// Measure the widest rendered line in pixels at the scaled edge-label font
|
|
504
|
-
// so the background box matches the actual ink (same measurer the layout
|
|
505
|
-
// uses to size + place the label).
|
|
506
|
-
let maxLineW = 0;
|
|
507
|
-
for (const l of labelLines)
|
|
508
|
-
maxLineW = Math.max(maxLineW, measureText(l, scaledEdgeLabelFont));
|
|
509
|
-
for (const l of descLines)
|
|
510
|
-
maxLineW = Math.max(maxLineW, measureText(l, scaledDescFont));
|
|
511
|
-
|
|
512
|
-
const bgW = maxLineW + 12;
|
|
513
|
-
const bgH = lineCount * scaledEdgeLineH + 6;
|
|
514
|
-
const bgX = isRight
|
|
515
|
-
? le.labelX - 4
|
|
516
|
-
: isLeft
|
|
517
|
-
? le.labelX - bgW + 4
|
|
518
|
-
: le.labelX - bgW / 2;
|
|
519
|
-
const bgY = le.labelY - scaledEdgeLabelFont - 2;
|
|
520
|
-
|
|
521
|
-
edgeG
|
|
522
|
-
.append('rect')
|
|
523
|
-
.attr('x', bgX)
|
|
524
|
-
.attr('y', bgY)
|
|
525
|
-
.attr('width', bgW)
|
|
526
|
-
.attr('height', bgH)
|
|
527
|
-
.attr('rx', 3)
|
|
528
|
-
.attr('fill', palette.bg)
|
|
529
|
-
.attr('fill-opacity', 0.85);
|
|
530
|
-
|
|
531
|
-
let textY = le.labelY;
|
|
532
|
-
for (const line of labelLines) {
|
|
533
|
-
const labelText = edgeG
|
|
534
|
-
.append('text')
|
|
535
|
-
.attr('x', le.labelX)
|
|
536
|
-
.attr('y', textY)
|
|
537
|
-
.attr('text-anchor', anchor)
|
|
538
|
-
.attr('fill', palette.text)
|
|
539
|
-
.attr('font-family', FONT_FAMILY)
|
|
540
|
-
.attr('font-size', scaledEdgeLabelFont)
|
|
541
|
-
.attr('font-weight', '600');
|
|
542
|
-
renderInlineText(labelText, line, palette, scaledEdgeLabelFont);
|
|
543
|
-
textY += scaledEdgeLineH;
|
|
544
|
-
}
|
|
545
|
-
for (const line of descLines) {
|
|
546
|
-
const descText = edgeG
|
|
547
|
-
.append('text')
|
|
548
|
-
.attr('x', le.labelX)
|
|
549
|
-
.attr('y', textY)
|
|
550
|
-
.attr('text-anchor', anchor)
|
|
551
|
-
.attr('fill', palette.textMuted)
|
|
552
|
-
.attr('font-family', FONT_FAMILY)
|
|
553
|
-
.attr('font-size', scaledDescFont);
|
|
554
|
-
renderInlineText(descText, line, palette, scaledDescFont);
|
|
555
|
-
textY += scaledEdgeLineH;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Render for CLI/export (no click handlers).
|
|
562
|
-
*/
|
|
563
|
-
export function renderCycleForExport(
|
|
564
|
-
container: HTMLDivElement,
|
|
565
|
-
parsed: ParsedCycle,
|
|
566
|
-
palette: PaletteColors,
|
|
567
|
-
isDark: boolean,
|
|
568
|
-
exportDims?: D3ExportDimensions,
|
|
569
|
-
viewState?: CompactViewState,
|
|
570
|
-
exportMode?: boolean
|
|
571
|
-
): void {
|
|
572
|
-
renderCycle(
|
|
573
|
-
container,
|
|
574
|
-
parsed,
|
|
575
|
-
palette,
|
|
576
|
-
isDark,
|
|
577
|
-
undefined,
|
|
578
|
-
exportDims,
|
|
579
|
-
viewState,
|
|
580
|
-
{ ...(exportMode !== undefined && { exportMode }) }
|
|
581
|
-
);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// ── Helpers ──────────────────────────────────────────────────
|
|
585
|
-
|
|
586
|
-
function resolveNodeColor(
|
|
587
|
-
color: string | undefined,
|
|
588
|
-
palette: PaletteColors,
|
|
589
|
-
defaultColor: string
|
|
590
|
-
): string {
|
|
591
|
-
if (!color) return defaultColor;
|
|
592
|
-
return resolveColor(color, palette) ?? defaultColor;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
function resolveEdgeColor(
|
|
596
|
-
edge: ParsedCycle['edges'][0],
|
|
597
|
-
parsed: ParsedCycle,
|
|
598
|
-
palette: PaletteColors,
|
|
599
|
-
defaultNodeColor: string
|
|
600
|
-
): string {
|
|
601
|
-
if (edge.color) {
|
|
602
|
-
return resolveColor(edge.color, palette) ?? defaultNodeColor;
|
|
603
|
-
}
|
|
604
|
-
// Inherit from source node
|
|
605
|
-
const sourceNode = parsed.nodes[edge.sourceIndex];
|
|
606
|
-
if (sourceNode?.color) {
|
|
607
|
-
return resolveColor(sourceNode.color, palette) ?? defaultNodeColor;
|
|
608
|
-
}
|
|
609
|
-
return defaultNodeColor;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
/** Stable marker ID for a (color, strokeWidth) pair. */
|
|
613
|
-
function arrowMarkerId(color: string, strokeWidth: number): string {
|
|
614
|
-
return `cycle-arrow-${color.replace('#', '')}-w${strokeWidth}`;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Create an arrowhead marker using markerUnits="strokeWidth" (SVG default)
|
|
619
|
-
* with per-edge dimensions. The marker base automatically equals the stroke
|
|
620
|
-
* width — no gaps or lollipop effects. Marker dimensions are computed so
|
|
621
|
-
* the rendered arrowhead length follows a sublinear formula:
|
|
622
|
-
*
|
|
623
|
-
* rendered length = markerWidth × strokeWidth = arrowHeadLength(sw)
|
|
624
|
-
* → markerWidth = arrowHeadLength(sw) / sw
|
|
625
|
-
*
|
|
626
|
-
* The height is fixed at 1 strokeWidth unit so the base = stroke width.
|
|
627
|
-
*/
|
|
628
|
-
function ensureArrowMarker(
|
|
629
|
-
defs: d3Selection.Selection<SVGDefsElement, unknown, null, undefined>,
|
|
630
|
-
color: string,
|
|
631
|
-
strokeWidth: number
|
|
632
|
-
): void {
|
|
633
|
-
const id = arrowMarkerId(color, strokeWidth);
|
|
634
|
-
// Marker dimensions in strokeWidth units.
|
|
635
|
-
// Rendered size = mw × sw (length) and mh × sw (height).
|
|
636
|
-
const mw = arrowHeadLength(strokeWidth) / strokeWidth;
|
|
637
|
-
// Height proportional to length (½ ratio) but at least 1.5× stroke width
|
|
638
|
-
// so the arrowhead is always visibly wider than the stroke.
|
|
639
|
-
const mh = Math.max(1.5, mw * 0.5);
|
|
640
|
-
|
|
641
|
-
defs
|
|
642
|
-
.append('marker')
|
|
643
|
-
.attr('id', id)
|
|
644
|
-
.attr('viewBox', `0 0 ${mw} ${mh}`)
|
|
645
|
-
.attr('refX', mw * 0.1)
|
|
646
|
-
.attr('refY', mh / 2)
|
|
647
|
-
.attr('markerWidth', mw)
|
|
648
|
-
.attr('markerHeight', mh)
|
|
649
|
-
.attr('orient', 'auto')
|
|
650
|
-
.append('polygon')
|
|
651
|
-
.attr('points', `0,0 ${mw},${mh / 2} 0,${mh}`)
|
|
652
|
-
.attr('fill', color);
|
|
653
|
-
}
|