@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/org/renderer.ts
DELETED
|
@@ -1,866 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Org Chart SVG Renderer
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import { tagAttrKey } from '../utils/tag-groups';
|
|
6
|
-
import * as d3Selection from 'd3-selection';
|
|
7
|
-
import { FONT_FAMILY } from '../fonts';
|
|
8
|
-
import {
|
|
9
|
-
runInExportContainer,
|
|
10
|
-
extractExportSvg,
|
|
11
|
-
} from '../utils/export-container';
|
|
12
|
-
import { ScaleContext } from '../utils/scaling';
|
|
13
|
-
import type { PaletteColors } from '../palettes';
|
|
14
|
-
import {
|
|
15
|
-
contrastText,
|
|
16
|
-
mix,
|
|
17
|
-
shapeFill,
|
|
18
|
-
themeBaseBg,
|
|
19
|
-
} from '../palettes/color-utils';
|
|
20
|
-
import { resolveTagColor } from '../utils/tag-groups';
|
|
21
|
-
import type { ParsedOrg } from './parser';
|
|
22
|
-
import type { OrgLayoutResult } from './layout';
|
|
23
|
-
import type { AncestorInfo } from './collapse';
|
|
24
|
-
import { parseOrg } from './parser';
|
|
25
|
-
import { layoutOrg } from './layout';
|
|
26
|
-
import {
|
|
27
|
-
LEGEND_HEIGHT,
|
|
28
|
-
LEGEND_GROUP_GAP,
|
|
29
|
-
LEGEND_EYE_SIZE,
|
|
30
|
-
LEGEND_EYE_GAP,
|
|
31
|
-
EYE_OPEN_PATH,
|
|
32
|
-
EYE_CLOSED_PATH,
|
|
33
|
-
} from '../utils/legend-constants';
|
|
34
|
-
import { renderIntegratedLegend } from '../utils/legend-integration';
|
|
35
|
-
import { renderNodeCard, renderCollapseBar } from '../utils/card';
|
|
36
|
-
import { measureText } from '../utils/text-measure';
|
|
37
|
-
import { getMaxLegendReservedHeight } from '../utils/legend-layout';
|
|
38
|
-
import type { LegendConfig } from '../utils/legend-types';
|
|
39
|
-
|
|
40
|
-
// ============================================================
|
|
41
|
-
// Constants
|
|
42
|
-
// ============================================================
|
|
43
|
-
|
|
44
|
-
const DIAGRAM_PADDING = 20;
|
|
45
|
-
const MAX_SCALE = 3;
|
|
46
|
-
import { TITLE_FONT_SIZE, TITLE_FONT_WEIGHT } from '../utils/title-constants';
|
|
47
|
-
const TITLE_HEIGHT = 30;
|
|
48
|
-
// Shared card / group / collapse constants (Story 111.1). org matches every
|
|
49
|
-
// convention default, so it imports the full set.
|
|
50
|
-
import {
|
|
51
|
-
LABEL_FONT_SIZE,
|
|
52
|
-
META_FONT_SIZE,
|
|
53
|
-
META_LINE_HEIGHT,
|
|
54
|
-
HEADER_HEIGHT,
|
|
55
|
-
SEPARATOR_GAP,
|
|
56
|
-
EDGE_STROKE_WIDTH,
|
|
57
|
-
NODE_STROKE_WIDTH,
|
|
58
|
-
CARD_RADIUS,
|
|
59
|
-
CONTAINER_RADIUS,
|
|
60
|
-
CONTAINER_LABEL_FONT_SIZE,
|
|
61
|
-
CONTAINER_META_FONT_SIZE,
|
|
62
|
-
CONTAINER_META_LINE_HEIGHT,
|
|
63
|
-
CONTAINER_HEADER_HEIGHT,
|
|
64
|
-
COLLAPSE_BAR_HEIGHT,
|
|
65
|
-
COLLAPSE_BAR_INSET,
|
|
66
|
-
} from '../utils/visual-conventions';
|
|
67
|
-
|
|
68
|
-
// Ancestor breadcrumb trail (focus mode)
|
|
69
|
-
const ANCESTOR_DOT_R = 4;
|
|
70
|
-
const ANCESTOR_LABEL_FONT_SIZE = 11;
|
|
71
|
-
const ANCESTOR_ROW_HEIGHT = 22;
|
|
72
|
-
const ANCESTOR_TRAIL_BOTTOM_GAP = 16;
|
|
73
|
-
|
|
74
|
-
const LEGEND_FIXED_GAP = 8; // gap between fixed legend and scaled diagram — local, not shared
|
|
75
|
-
|
|
76
|
-
// ============================================================
|
|
77
|
-
// Color helpers
|
|
78
|
-
// ============================================================
|
|
79
|
-
|
|
80
|
-
function nodeFill(
|
|
81
|
-
palette: PaletteColors,
|
|
82
|
-
isDark: boolean,
|
|
83
|
-
nodeColor?: string,
|
|
84
|
-
solid?: boolean
|
|
85
|
-
): string {
|
|
86
|
-
const color = nodeColor ?? palette.primary;
|
|
87
|
-
return shapeFill(palette, color, isDark, {
|
|
88
|
-
...(solid !== undefined && { solid }),
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function nodeStroke(palette: PaletteColors, nodeColor?: string): string {
|
|
93
|
-
return nodeColor ?? palette.primary;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function containerFill(
|
|
97
|
-
palette: PaletteColors,
|
|
98
|
-
isDark: boolean,
|
|
99
|
-
nodeColor?: string
|
|
100
|
-
): string {
|
|
101
|
-
if (nodeColor) {
|
|
102
|
-
return mix(nodeColor, themeBaseBg(palette, isDark), 10);
|
|
103
|
-
}
|
|
104
|
-
return mix(palette.surface, palette.bg, 40);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function containerStroke(palette: PaletteColors, nodeColor?: string): string {
|
|
108
|
-
return nodeColor ?? palette.textMuted;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ============================================================
|
|
112
|
-
// Main Renderer
|
|
113
|
-
// ============================================================
|
|
114
|
-
|
|
115
|
-
type GSelection = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
|
|
116
|
-
|
|
117
|
-
export function renderOrg(
|
|
118
|
-
container: HTMLDivElement,
|
|
119
|
-
parsed: ParsedOrg,
|
|
120
|
-
layout: OrgLayoutResult,
|
|
121
|
-
palette: PaletteColors,
|
|
122
|
-
isDark: boolean,
|
|
123
|
-
onClickItem?: (lineNumber: number) => void,
|
|
124
|
-
exportDims?: { width?: number; height?: number },
|
|
125
|
-
activeTagGroup?: string | null,
|
|
126
|
-
hiddenAttributes?: Set<string>,
|
|
127
|
-
ancestorPath?: AncestorInfo[],
|
|
128
|
-
exportMode?: boolean
|
|
129
|
-
): void {
|
|
130
|
-
// Clear existing content
|
|
131
|
-
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
132
|
-
|
|
133
|
-
const width = exportDims?.width ?? container.clientWidth;
|
|
134
|
-
const height = exportDims?.height ?? container.clientHeight;
|
|
135
|
-
if (width <= 0 || height <= 0) return;
|
|
136
|
-
|
|
137
|
-
const ctx = ScaleContext.identity();
|
|
138
|
-
|
|
139
|
-
const sDiagramPadding = ctx.aesthetic(DIAGRAM_PADDING);
|
|
140
|
-
const sTitleHeight = ctx.structural(TITLE_HEIGHT);
|
|
141
|
-
const sLabelFontSize = ctx.text(LABEL_FONT_SIZE);
|
|
142
|
-
const sMetaFontSize = ctx.text(META_FONT_SIZE);
|
|
143
|
-
const sMetaLineHeight = ctx.structural(META_LINE_HEIGHT);
|
|
144
|
-
const sHeaderHeight = ctx.structural(HEADER_HEIGHT);
|
|
145
|
-
const sSeparatorGap = ctx.structural(SEPARATOR_GAP);
|
|
146
|
-
const sEdgeStrokeWidth = ctx.structural(EDGE_STROKE_WIDTH);
|
|
147
|
-
const sNodeStrokeWidth = ctx.structural(NODE_STROKE_WIDTH);
|
|
148
|
-
const sCardRadius = ctx.structural(CARD_RADIUS);
|
|
149
|
-
const sContainerRadius = ctx.structural(CONTAINER_RADIUS);
|
|
150
|
-
const sContainerLabelFontSize = ctx.text(CONTAINER_LABEL_FONT_SIZE);
|
|
151
|
-
const sContainerMetaFontSize = ctx.text(CONTAINER_META_FONT_SIZE);
|
|
152
|
-
const sContainerMetaLineHeight = ctx.structural(CONTAINER_META_LINE_HEIGHT);
|
|
153
|
-
const sContainerHeaderHeight = ctx.structural(CONTAINER_HEADER_HEIGHT);
|
|
154
|
-
const sCollapseBarHeight = ctx.structural(COLLAPSE_BAR_HEIGHT);
|
|
155
|
-
const sCollapseBarInset = ctx.structural(COLLAPSE_BAR_INSET);
|
|
156
|
-
const sAncestorDotR = ctx.structural(ANCESTOR_DOT_R);
|
|
157
|
-
const sAncestorLabelFontSize = ctx.text(ANCESTOR_LABEL_FONT_SIZE);
|
|
158
|
-
const sAncestorRowHeight = ctx.structural(ANCESTOR_ROW_HEIGHT);
|
|
159
|
-
const sAncestorTrailBottomGap = ctx.structural(ANCESTOR_TRAIL_BOTTOM_GAP);
|
|
160
|
-
const sLegendFixedGap = ctx.aesthetic(LEGEND_FIXED_GAP);
|
|
161
|
-
|
|
162
|
-
const showTitle = !!parsed.title && parsed.options['no-title'] !== 'on';
|
|
163
|
-
const titleOffset = showTitle ? sTitleHeight : 0;
|
|
164
|
-
const legendOnly = layout.nodes.length === 0;
|
|
165
|
-
const hasLegend = layout.legend.length > 0;
|
|
166
|
-
|
|
167
|
-
const layoutLegendShift = LEGEND_HEIGHT + LEGEND_GROUP_GAP;
|
|
168
|
-
const fixedLegend = !exportDims && hasLegend && !legendOnly;
|
|
169
|
-
const legendReserve = fixedLegend
|
|
170
|
-
? getMaxLegendReservedHeight(
|
|
171
|
-
{
|
|
172
|
-
groups: parsed.tagGroups,
|
|
173
|
-
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
174
|
-
mode: 'preview',
|
|
175
|
-
capsulePillAddonWidth: LEGEND_EYE_SIZE + LEGEND_EYE_GAP,
|
|
176
|
-
},
|
|
177
|
-
width
|
|
178
|
-
) + sLegendFixedGap
|
|
179
|
-
: 0;
|
|
180
|
-
|
|
181
|
-
const fixedTitle = !exportDims && showTitle;
|
|
182
|
-
const titleReserve = fixedTitle ? sTitleHeight : 0;
|
|
183
|
-
|
|
184
|
-
const hasAncestorTrail =
|
|
185
|
-
!exportDims && ancestorPath && ancestorPath.length > 0;
|
|
186
|
-
const ancestorTrailHeight = hasAncestorTrail
|
|
187
|
-
? ancestorPath.length * sAncestorRowHeight + sAncestorTrailBottomGap
|
|
188
|
-
: 0;
|
|
189
|
-
|
|
190
|
-
const diagramW = layout.width;
|
|
191
|
-
let diagramH =
|
|
192
|
-
layout.height + (fixedTitle ? 0 : titleOffset) + ancestorTrailHeight;
|
|
193
|
-
if (fixedLegend) {
|
|
194
|
-
diagramH -= layoutLegendShift;
|
|
195
|
-
}
|
|
196
|
-
const availH = height - sDiagramPadding * 2 - legendReserve - titleReserve;
|
|
197
|
-
const scaleX = (width - sDiagramPadding * 2) / diagramW;
|
|
198
|
-
const scaleY = availH / diagramH;
|
|
199
|
-
const scale = Math.min(MAX_SCALE, scaleX, scaleY);
|
|
200
|
-
|
|
201
|
-
const scaledW = diagramW * scale;
|
|
202
|
-
const offsetX = (width - scaledW) / 2;
|
|
203
|
-
const offsetY = fixedLegend
|
|
204
|
-
? sDiagramPadding + legendReserve + titleReserve
|
|
205
|
-
: sDiagramPadding + titleReserve;
|
|
206
|
-
|
|
207
|
-
const svg = d3Selection
|
|
208
|
-
.select(container)
|
|
209
|
-
.append('svg')
|
|
210
|
-
.attr('width', width)
|
|
211
|
-
.attr('height', height)
|
|
212
|
-
.attr('viewBox', `0 0 ${width} ${height}`)
|
|
213
|
-
.attr('preserveAspectRatio', 'xMidYMin meet')
|
|
214
|
-
.style('font-family', FONT_FAMILY);
|
|
215
|
-
|
|
216
|
-
if (ctx.isBelowFloor) {
|
|
217
|
-
svg.attr('width', '100%');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const mainG = svg
|
|
221
|
-
.append('g')
|
|
222
|
-
.attr('transform', `translate(${offsetX}, ${offsetY}) scale(${scale})`);
|
|
223
|
-
|
|
224
|
-
// Title
|
|
225
|
-
// In non-export mode (fixedTitle), render at native size directly on the SVG
|
|
226
|
-
// so it stays legible regardless of chart scale. In export mode, render inside
|
|
227
|
-
// mainG so it scales with the diagram to match the exported dimensions.
|
|
228
|
-
if (showTitle) {
|
|
229
|
-
const titleParent = fixedTitle ? svg : mainG;
|
|
230
|
-
const titleX = fixedTitle ? width / 2 : diagramW / 2;
|
|
231
|
-
const titleY = fixedTitle
|
|
232
|
-
? sDiagramPadding + TITLE_FONT_SIZE
|
|
233
|
-
: TITLE_FONT_SIZE;
|
|
234
|
-
const titleEl = titleParent
|
|
235
|
-
.append('text')
|
|
236
|
-
.attr('x', titleX)
|
|
237
|
-
.attr('y', titleY)
|
|
238
|
-
.attr('text-anchor', 'middle')
|
|
239
|
-
.attr('fill', palette.text)
|
|
240
|
-
.attr('font-size', TITLE_FONT_SIZE)
|
|
241
|
-
.attr('font-weight', TITLE_FONT_WEIGHT)
|
|
242
|
-
.attr('class', 'org-title chart-title')
|
|
243
|
-
.style(
|
|
244
|
-
'cursor',
|
|
245
|
-
onClickItem && parsed.titleLineNumber ? 'pointer' : 'default'
|
|
246
|
-
)
|
|
247
|
-
.text(parsed.title);
|
|
248
|
-
|
|
249
|
-
if (parsed.titleLineNumber) {
|
|
250
|
-
titleEl.attr('data-line-number', parsed.titleLineNumber);
|
|
251
|
-
if (onClickItem) {
|
|
252
|
-
titleEl
|
|
253
|
-
.on('click', () => onClickItem(parsed.titleLineNumber!))
|
|
254
|
-
.on('mouseenter', function () {
|
|
255
|
-
d3Selection.select(this).attr('opacity', 0.7);
|
|
256
|
-
})
|
|
257
|
-
.on('mouseleave', function () {
|
|
258
|
-
d3Selection.select(this).attr('opacity', 1);
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Content group (offset by title + ancestor trail height)
|
|
265
|
-
const contentYShift = (fixedTitle ? 0 : titleOffset) + ancestorTrailHeight;
|
|
266
|
-
const contentG = mainG
|
|
267
|
-
.append('g')
|
|
268
|
-
.attr('transform', `translate(0, ${contentYShift})`);
|
|
269
|
-
|
|
270
|
-
// Build display name map from tag groups (lowercase key → original casing)
|
|
271
|
-
const displayNames = new Map<string, string>();
|
|
272
|
-
for (const group of parsed.tagGroups) {
|
|
273
|
-
displayNames.set(tagAttrKey(group.name), group.name);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Root node IDs — focus icon is suppressed on these (already the tree root)
|
|
277
|
-
const rootNodeIds = new Set(parsed.roots.map((r) => r.id));
|
|
278
|
-
|
|
279
|
-
// Render container backgrounds (bottom layer)
|
|
280
|
-
const colorOff = parsed.options?.['color'] === 'off';
|
|
281
|
-
for (const c of layout.containers) {
|
|
282
|
-
const cG = contentG
|
|
283
|
-
.append('g')
|
|
284
|
-
.attr('transform', `translate(${c.x}, ${c.y})`)
|
|
285
|
-
.attr('class', 'org-container')
|
|
286
|
-
.attr('data-line-number', String(c.lineNumber)) as GSelection;
|
|
287
|
-
|
|
288
|
-
// Expose active tag group value for legend-entry hover dimming
|
|
289
|
-
// Use tagMetadata (unfiltered) so hover-highlight works even when the
|
|
290
|
-
// active tag group is hidden from the visible card body via the eye toggle.
|
|
291
|
-
if (activeTagGroup) {
|
|
292
|
-
const tagKey = tagAttrKey(activeTagGroup);
|
|
293
|
-
const metaValue = c.tagMetadata[tagKey];
|
|
294
|
-
if (metaValue) {
|
|
295
|
-
cG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Toggle attribute for containers that have (or had) children
|
|
300
|
-
if (c.hasChildren) {
|
|
301
|
-
cG.attr('data-node-toggle', c.nodeId)
|
|
302
|
-
.attr('tabindex', '0')
|
|
303
|
-
.attr('role', 'button')
|
|
304
|
-
.attr('aria-expanded', String(!c.hiddenCount))
|
|
305
|
-
.attr('aria-label', c.label);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (onClickItem) {
|
|
309
|
-
cG.style('cursor', 'pointer').on('click', () => {
|
|
310
|
-
onClickItem(c.lineNumber);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const fill = containerFill(palette, isDark, colorOff ? undefined : c.color);
|
|
315
|
-
const stroke = containerStroke(palette, colorOff ? undefined : c.color);
|
|
316
|
-
|
|
317
|
-
cG.append('rect')
|
|
318
|
-
.attr('x', 0)
|
|
319
|
-
.attr('y', 0)
|
|
320
|
-
.attr('width', c.width)
|
|
321
|
-
.attr('height', c.height)
|
|
322
|
-
.attr('rx', sContainerRadius)
|
|
323
|
-
.attr('fill', fill)
|
|
324
|
-
.attr('stroke', stroke)
|
|
325
|
-
.attr('stroke-opacity', 0.35)
|
|
326
|
-
.attr('stroke-width', sNodeStrokeWidth);
|
|
327
|
-
|
|
328
|
-
cG.append('text')
|
|
329
|
-
.attr('x', c.width / 2)
|
|
330
|
-
.attr('y', sContainerHeaderHeight / 2 + sContainerLabelFontSize / 2 - 2)
|
|
331
|
-
.attr('text-anchor', 'middle')
|
|
332
|
-
.attr('fill', palette.text)
|
|
333
|
-
.attr('font-size', sContainerLabelFontSize)
|
|
334
|
-
.attr('font-weight', 'bold')
|
|
335
|
-
.text(c.label);
|
|
336
|
-
|
|
337
|
-
const metaEntries = Object.entries(c.metadata);
|
|
338
|
-
if (metaEntries.length > 0) {
|
|
339
|
-
const metaDisplayKeys = metaEntries.map(
|
|
340
|
-
([k]) => displayNames.get(k) ?? k
|
|
341
|
-
);
|
|
342
|
-
const maxKeyWidth = Math.max(
|
|
343
|
-
...metaDisplayKeys.map((k) =>
|
|
344
|
-
measureText(`${k}: `, sContainerMetaFontSize)
|
|
345
|
-
)
|
|
346
|
-
);
|
|
347
|
-
const valueX = 10 + maxKeyWidth;
|
|
348
|
-
|
|
349
|
-
const metaStartY = sContainerHeaderHeight + sContainerMetaFontSize - 2;
|
|
350
|
-
for (let i = 0; i < metaEntries.length; i++) {
|
|
351
|
-
const [, value] = metaEntries[i]!;
|
|
352
|
-
const displayKey = metaDisplayKeys[i]!;
|
|
353
|
-
const rowY = metaStartY + i * sContainerMetaLineHeight;
|
|
354
|
-
|
|
355
|
-
cG.append('text')
|
|
356
|
-
.attr('x', 10)
|
|
357
|
-
.attr('y', rowY)
|
|
358
|
-
.attr('fill', palette.textMuted)
|
|
359
|
-
.attr('font-size', sContainerMetaFontSize)
|
|
360
|
-
.text(`${displayKey}: `);
|
|
361
|
-
|
|
362
|
-
cG.append('text')
|
|
363
|
-
.attr('x', valueX)
|
|
364
|
-
.attr('y', rowY)
|
|
365
|
-
.attr('fill', palette.text)
|
|
366
|
-
.attr('font-size', sContainerMetaFontSize)
|
|
367
|
-
.text(value);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (!exportDims && c.hiddenCount && c.hiddenCount > 0) {
|
|
372
|
-
renderCollapseBar(cG, {
|
|
373
|
-
width: c.width,
|
|
374
|
-
height: c.height,
|
|
375
|
-
barHeight: sCollapseBarHeight,
|
|
376
|
-
inset: sCollapseBarInset,
|
|
377
|
-
rx: sContainerRadius,
|
|
378
|
-
fill: containerStroke(palette, colorOff ? undefined : c.color),
|
|
379
|
-
clipId: `clip-${c.nodeId}`,
|
|
380
|
-
className: 'org-collapse-bar',
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Focus icon (hover-reveal, interactive only) — for non-root containers with children
|
|
385
|
-
if (!exportDims && c.hasChildren && !rootNodeIds.has(c.nodeId)) {
|
|
386
|
-
const iconSize = 14;
|
|
387
|
-
const iconPad = 5;
|
|
388
|
-
const iconX = c.width - iconSize - iconPad;
|
|
389
|
-
const iconY = iconPad;
|
|
390
|
-
|
|
391
|
-
const focusG = cG
|
|
392
|
-
.append('g')
|
|
393
|
-
.attr('class', 'org-focus-icon')
|
|
394
|
-
.attr('data-focus-node', c.nodeId)
|
|
395
|
-
.attr('data-export-ignore', 'true')
|
|
396
|
-
.attr('transform', `translate(${iconX}, ${iconY})`);
|
|
397
|
-
|
|
398
|
-
focusG
|
|
399
|
-
.append('rect')
|
|
400
|
-
.attr('x', -3)
|
|
401
|
-
.attr('y', -3)
|
|
402
|
-
.attr('width', iconSize + 6)
|
|
403
|
-
.attr('height', iconSize + 6)
|
|
404
|
-
.attr('fill', 'transparent');
|
|
405
|
-
|
|
406
|
-
// Use the container's contrast text color so it stays legible on darker
|
|
407
|
-
// fills, mirroring the node icon.
|
|
408
|
-
const iconColor = contrastText(
|
|
409
|
-
fill,
|
|
410
|
-
palette.textOnFillLight,
|
|
411
|
-
palette.textOnFillDark
|
|
412
|
-
);
|
|
413
|
-
const cx = iconSize / 2;
|
|
414
|
-
const cy = iconSize / 2;
|
|
415
|
-
focusG
|
|
416
|
-
.append('circle')
|
|
417
|
-
.attr('cx', cx)
|
|
418
|
-
.attr('cy', cy)
|
|
419
|
-
.attr('r', iconSize / 2 - 1)
|
|
420
|
-
.attr('fill', 'none')
|
|
421
|
-
.attr('stroke', iconColor)
|
|
422
|
-
.attr('stroke-width', 1.5);
|
|
423
|
-
focusG
|
|
424
|
-
.append('circle')
|
|
425
|
-
.attr('cx', cx)
|
|
426
|
-
.attr('cy', cy)
|
|
427
|
-
.attr('r', 2)
|
|
428
|
-
.attr('fill', iconColor);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Render edges
|
|
433
|
-
for (const edge of layout.edges) {
|
|
434
|
-
if (edge.points.length < 2) continue;
|
|
435
|
-
|
|
436
|
-
const pathParts: string[] = [];
|
|
437
|
-
// In-bounds by length >= 2 guard above.
|
|
438
|
-
pathParts.push(`M ${edge.points[0]!.x} ${edge.points[0]!.y}`);
|
|
439
|
-
for (let i = 1; i < edge.points.length; i++) {
|
|
440
|
-
// In-bounds by loop guard.
|
|
441
|
-
pathParts.push(`L ${edge.points[i]!.x} ${edge.points[i]!.y}`);
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
contentG
|
|
445
|
-
.append('path')
|
|
446
|
-
.attr('d', pathParts.join(' '))
|
|
447
|
-
.attr('fill', 'none')
|
|
448
|
-
.attr('stroke', palette.textMuted)
|
|
449
|
-
.attr('stroke-width', sEdgeStrokeWidth)
|
|
450
|
-
.attr('class', 'org-edge')
|
|
451
|
-
// Endpoint node ids for baked-CSS connection-highlight (hover-styles.ts).
|
|
452
|
-
.attr('data-from', edge.sourceId)
|
|
453
|
-
.attr('data-to', edge.targetId);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// Collect container node IDs so we can skip them in card rendering
|
|
457
|
-
const containerNodeIds = new Set(layout.containers.map((c) => c.nodeId));
|
|
458
|
-
|
|
459
|
-
// Render node cards (top layer) — skip containers (already drawn as background boxes)
|
|
460
|
-
for (const node of layout.nodes) {
|
|
461
|
-
if (containerNodeIds.has(node.id)) continue;
|
|
462
|
-
|
|
463
|
-
const nodeG = contentG
|
|
464
|
-
.append('g')
|
|
465
|
-
.attr('transform', `translate(${node.x - node.width / 2}, ${node.y})`)
|
|
466
|
-
.attr('class', 'org-node')
|
|
467
|
-
.attr('data-line-number', String(node.lineNumber))
|
|
468
|
-
.attr('data-node-id', node.id) as GSelection;
|
|
469
|
-
|
|
470
|
-
// Expose active tag group value for legend-entry hover dimming
|
|
471
|
-
// Use tagMetadata (unfiltered) so hover-highlight works even when the
|
|
472
|
-
// active tag group is hidden from the visible card body via the eye toggle.
|
|
473
|
-
if (activeTagGroup) {
|
|
474
|
-
const tagKey = tagAttrKey(activeTagGroup);
|
|
475
|
-
const metaValue = node.tagMetadata[tagKey];
|
|
476
|
-
if (metaValue) {
|
|
477
|
-
nodeG.attr(`data-tag-${tagKey}`, metaValue.toLowerCase());
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Toggle attribute for nodes that have (or had) children
|
|
482
|
-
if (node.hasChildren) {
|
|
483
|
-
nodeG
|
|
484
|
-
.attr('data-node-toggle', node.id)
|
|
485
|
-
.attr('tabindex', '0')
|
|
486
|
-
.attr('role', 'button')
|
|
487
|
-
.attr('aria-expanded', String(!node.hiddenCount))
|
|
488
|
-
.attr('aria-label', node.label);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (onClickItem) {
|
|
492
|
-
nodeG.style('cursor', 'pointer').on('click', () => {
|
|
493
|
-
onClickItem(node.lineNumber);
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Card background
|
|
498
|
-
const solid = parsed.options['solid-fill'] === 'on';
|
|
499
|
-
const fill = nodeFill(
|
|
500
|
-
palette,
|
|
501
|
-
isDark,
|
|
502
|
-
colorOff ? undefined : node.color,
|
|
503
|
-
solid
|
|
504
|
-
);
|
|
505
|
-
const stroke = nodeStroke(palette, colorOff ? undefined : node.color);
|
|
506
|
-
|
|
507
|
-
const labelColor = contrastText(
|
|
508
|
-
fill,
|
|
509
|
-
palette.textOnFillLight,
|
|
510
|
-
palette.textOnFillDark
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
const metaEntries = Object.entries(node.metadata);
|
|
514
|
-
renderNodeCard(nodeG, {
|
|
515
|
-
width: node.width,
|
|
516
|
-
height: node.height,
|
|
517
|
-
rx: sCardRadius,
|
|
518
|
-
fill,
|
|
519
|
-
stroke,
|
|
520
|
-
strokeWidth: sNodeStrokeWidth,
|
|
521
|
-
...(node.isContainer && { dashed: true }),
|
|
522
|
-
label: node.label,
|
|
523
|
-
labelColor,
|
|
524
|
-
labelFontSize: sLabelFontSize,
|
|
525
|
-
headerHeight: sHeaderHeight,
|
|
526
|
-
...(metaEntries.length > 0 && {
|
|
527
|
-
meta: {
|
|
528
|
-
rows: metaEntries.map(
|
|
529
|
-
([k, value]) => [displayNames.get(k) ?? k, value] as const
|
|
530
|
-
),
|
|
531
|
-
fontSize: sMetaFontSize,
|
|
532
|
-
lineHeight: sMetaLineHeight,
|
|
533
|
-
separatorGap: sSeparatorGap,
|
|
534
|
-
separatorColor: solid ? labelColor : stroke,
|
|
535
|
-
textColor: labelColor,
|
|
536
|
-
},
|
|
537
|
-
}),
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
if (!exportDims && node.hiddenCount && node.hiddenCount > 0) {
|
|
541
|
-
renderCollapseBar(nodeG, {
|
|
542
|
-
width: node.width,
|
|
543
|
-
height: node.height,
|
|
544
|
-
barHeight: sCollapseBarHeight,
|
|
545
|
-
inset: sCollapseBarInset,
|
|
546
|
-
rx: sCardRadius,
|
|
547
|
-
fill: solid
|
|
548
|
-
? labelColor
|
|
549
|
-
: nodeStroke(palette, colorOff ? undefined : node.color),
|
|
550
|
-
clipId: `clip-${node.id}`,
|
|
551
|
-
className: 'org-collapse-bar',
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// Focus icon (hover-reveal, interactive only) — for non-root nodes with children
|
|
556
|
-
if (!exportDims && node.hasChildren && !rootNodeIds.has(node.id)) {
|
|
557
|
-
const iconSize = 14;
|
|
558
|
-
const iconPad = 5;
|
|
559
|
-
const iconX = node.width - iconSize - iconPad;
|
|
560
|
-
const iconY = iconPad;
|
|
561
|
-
|
|
562
|
-
const focusG = nodeG
|
|
563
|
-
.append('g')
|
|
564
|
-
.attr('class', 'org-focus-icon')
|
|
565
|
-
.attr('data-focus-node', node.id)
|
|
566
|
-
.attr('data-export-ignore', 'true')
|
|
567
|
-
.attr('transform', `translate(${iconX}, ${iconY})`);
|
|
568
|
-
|
|
569
|
-
// Hit area
|
|
570
|
-
focusG
|
|
571
|
-
.append('rect')
|
|
572
|
-
.attr('x', -3)
|
|
573
|
-
.attr('y', -3)
|
|
574
|
-
.attr('width', iconSize + 6)
|
|
575
|
-
.attr('height', iconSize + 6)
|
|
576
|
-
.attr('fill', 'transparent');
|
|
577
|
-
|
|
578
|
-
// Scope/target icon: outer circle + inner dot. Use the card's contrast
|
|
579
|
-
// text color so it stays legible on solid-fill dark cards, not just the
|
|
580
|
-
// light default surface.
|
|
581
|
-
const iconColor = labelColor;
|
|
582
|
-
const cx = iconSize / 2;
|
|
583
|
-
const cy = iconSize / 2;
|
|
584
|
-
focusG
|
|
585
|
-
.append('circle')
|
|
586
|
-
.attr('cx', cx)
|
|
587
|
-
.attr('cy', cy)
|
|
588
|
-
.attr('r', iconSize / 2 - 1)
|
|
589
|
-
.attr('fill', 'none')
|
|
590
|
-
.attr('stroke', iconColor)
|
|
591
|
-
.attr('stroke-width', 1.5);
|
|
592
|
-
focusG
|
|
593
|
-
.append('circle')
|
|
594
|
-
.attr('cx', cx)
|
|
595
|
-
.attr('cy', cy)
|
|
596
|
-
.attr('r', 2)
|
|
597
|
-
.attr('fill', iconColor);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Render ancestor breadcrumb trail (focus mode) — inside scaled group,
|
|
602
|
-
// centered on and connected to the root node
|
|
603
|
-
if (hasAncestorTrail) {
|
|
604
|
-
// Find the root node/container position in the layout
|
|
605
|
-
const rootNode = layout.nodes.find((n) => rootNodeIds.has(n.id));
|
|
606
|
-
const rootContainer = !rootNode
|
|
607
|
-
? layout.containers.find((c) => rootNodeIds.has(c.nodeId))
|
|
608
|
-
: null;
|
|
609
|
-
// Nodes: x is center. Containers: x is left edge, so center = x + width/2
|
|
610
|
-
const rootCenterX = rootNode
|
|
611
|
-
? rootNode.x
|
|
612
|
-
: rootContainer
|
|
613
|
-
? rootContainer.x + rootContainer.width / 2
|
|
614
|
-
: null;
|
|
615
|
-
const rootTopY = rootNode
|
|
616
|
-
? rootNode.y
|
|
617
|
-
: rootContainer
|
|
618
|
-
? rootContainer.y
|
|
619
|
-
: null;
|
|
620
|
-
if (rootCenterX !== null && rootTopY !== null) {
|
|
621
|
-
// Trail connects directly to the top edge of the root node.
|
|
622
|
-
// The last ancestor dot sits ANCESTOR_TRAIL_BOTTOM_GAP above the root.
|
|
623
|
-
const trailBottomY = rootTopY - sAncestorTrailBottomGap;
|
|
624
|
-
|
|
625
|
-
const trailG = contentG.append('g').attr('class', 'org-ancestor-trail');
|
|
626
|
-
|
|
627
|
-
const count = ancestorPath!.length;
|
|
628
|
-
|
|
629
|
-
const dotPositions: number[] = [];
|
|
630
|
-
for (let i = 0; i < count; i++) {
|
|
631
|
-
const fromBottom = count - 1 - i;
|
|
632
|
-
dotPositions.push(trailBottomY - fromBottom * sAncestorRowHeight);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Single continuous line from topmost dot to root node top edge — in-bounds because hasAncestorTrail implies count >= 1.
|
|
636
|
-
const lineTopY = dotPositions[0]!;
|
|
637
|
-
trailG
|
|
638
|
-
.append('line')
|
|
639
|
-
.attr('x1', rootCenterX)
|
|
640
|
-
.attr('y1', lineTopY)
|
|
641
|
-
.attr('x2', rootCenterX)
|
|
642
|
-
.attr('y2', rootTopY)
|
|
643
|
-
.attr('stroke', palette.textMuted)
|
|
644
|
-
.attr('stroke-width', 1.5)
|
|
645
|
-
.attr('stroke-opacity', 0.4);
|
|
646
|
-
|
|
647
|
-
// Dots and labels on top of the line
|
|
648
|
-
for (let i = 0; i < count; i++) {
|
|
649
|
-
// In-bounds: count === ancestorPath.length; dotPositions parallel by construction.
|
|
650
|
-
const ancestor = ancestorPath![i]!;
|
|
651
|
-
const dotY = dotPositions[i]!;
|
|
652
|
-
|
|
653
|
-
// Resolve color from tag groups (same logic as node cards)
|
|
654
|
-
const resolvedColor =
|
|
655
|
-
ancestor.color ??
|
|
656
|
-
resolveTagColor(
|
|
657
|
-
ancestor.metadata,
|
|
658
|
-
[...parsed.tagGroups],
|
|
659
|
-
activeTagGroup ?? null,
|
|
660
|
-
ancestor.isContainer
|
|
661
|
-
);
|
|
662
|
-
const dotColor = resolvedColor ?? palette.textMuted;
|
|
663
|
-
|
|
664
|
-
const rowG = trailG
|
|
665
|
-
.append('g')
|
|
666
|
-
.attr('class', 'org-ancestor-node')
|
|
667
|
-
.attr('data-focus-ancestor', ancestor.id)
|
|
668
|
-
.style('cursor', 'pointer')
|
|
669
|
-
.attr('transform', `translate(${rootCenterX}, ${dotY})`);
|
|
670
|
-
|
|
671
|
-
rowG
|
|
672
|
-
.append('rect')
|
|
673
|
-
.attr('x', -sAncestorDotR - 2)
|
|
674
|
-
.attr('y', -sAncestorDotR - 2)
|
|
675
|
-
.attr('width', 120)
|
|
676
|
-
.attr('height', sAncestorDotR * 2 + 4)
|
|
677
|
-
.attr('fill', 'transparent');
|
|
678
|
-
|
|
679
|
-
rowG
|
|
680
|
-
.append('circle')
|
|
681
|
-
.attr('cx', 0)
|
|
682
|
-
.attr('cy', 0)
|
|
683
|
-
.attr('r', sAncestorDotR)
|
|
684
|
-
.attr('fill', dotColor);
|
|
685
|
-
|
|
686
|
-
rowG
|
|
687
|
-
.append('text')
|
|
688
|
-
.attr('x', sAncestorDotR + 6)
|
|
689
|
-
.attr('y', sAncestorLabelFontSize * 0.35)
|
|
690
|
-
.attr('fill', palette.textMuted)
|
|
691
|
-
.attr('font-size', sAncestorLabelFontSize)
|
|
692
|
-
.text(ancestor.label);
|
|
693
|
-
|
|
694
|
-
rowG
|
|
695
|
-
.on('mouseenter', function () {
|
|
696
|
-
d3Selection
|
|
697
|
-
.select(this)
|
|
698
|
-
.select('circle')
|
|
699
|
-
.attr('r', sAncestorDotR + 1);
|
|
700
|
-
d3Selection.select(this).select('text').attr('fill', palette.text);
|
|
701
|
-
})
|
|
702
|
-
.on('mouseleave', function () {
|
|
703
|
-
d3Selection.select(this).select('circle').attr('r', sAncestorDotR);
|
|
704
|
-
d3Selection
|
|
705
|
-
.select(this)
|
|
706
|
-
.select('text')
|
|
707
|
-
.attr('fill', palette.textMuted);
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Render legend — capsule pills.
|
|
714
|
-
// In app mode (fixedLegend): render at native size outside the scaled group.
|
|
715
|
-
// In export mode: skip legend (unless legend-only chart).
|
|
716
|
-
// Legend-only (no nodes): all groups rendered as expanded capsules inside scaled group.
|
|
717
|
-
if (fixedLegend || legendOnly || (exportDims && hasLegend)) {
|
|
718
|
-
const groups = layout.legend.map((g) => ({
|
|
719
|
-
name: g.name,
|
|
720
|
-
entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
721
|
-
}));
|
|
722
|
-
|
|
723
|
-
const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
|
|
724
|
-
|
|
725
|
-
// Choose parent: unscaled group for fixedLegend, contentG for legend-only
|
|
726
|
-
const legendParentBase = fixedLegend
|
|
727
|
-
? svg
|
|
728
|
-
.append('g')
|
|
729
|
-
.attr('class', 'org-legend-fixed')
|
|
730
|
-
.attr('transform', `translate(0, ${sDiagramPadding + titleReserve})`)
|
|
731
|
-
: contentG.append('g');
|
|
732
|
-
|
|
733
|
-
let legendHandle;
|
|
734
|
-
if (legendOnly) {
|
|
735
|
-
// Legend-only mode: render each group expanded individually at layout positions
|
|
736
|
-
for (const lg of layout.legend) {
|
|
737
|
-
const singleConfig: LegendConfig = {
|
|
738
|
-
groups: [
|
|
739
|
-
{
|
|
740
|
-
name: lg.name,
|
|
741
|
-
entries: lg.entries.map((e) => ({
|
|
742
|
-
value: e.value,
|
|
743
|
-
color: e.color,
|
|
744
|
-
})),
|
|
745
|
-
},
|
|
746
|
-
],
|
|
747
|
-
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
748
|
-
mode: exportMode ? 'export' : 'preview',
|
|
749
|
-
};
|
|
750
|
-
const groupG = legendParentBase
|
|
751
|
-
.append('g')
|
|
752
|
-
.attr('transform', `translate(${lg.x}, ${lg.y})`);
|
|
753
|
-
renderIntegratedLegend(groupG, {
|
|
754
|
-
...singleConfig,
|
|
755
|
-
palette,
|
|
756
|
-
isDark,
|
|
757
|
-
width: lg.width,
|
|
758
|
-
activeGroup: lg.name,
|
|
759
|
-
});
|
|
760
|
-
groupG
|
|
761
|
-
.selectAll('[data-legend-group]')
|
|
762
|
-
.classed('org-legend-group', true);
|
|
763
|
-
}
|
|
764
|
-
legendHandle = null;
|
|
765
|
-
} else {
|
|
766
|
-
const legendConfig: LegendConfig = {
|
|
767
|
-
groups,
|
|
768
|
-
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
769
|
-
mode: exportMode ? 'export' : 'preview',
|
|
770
|
-
capsulePillAddonWidth: eyeAddonWidth,
|
|
771
|
-
};
|
|
772
|
-
legendHandle = renderIntegratedLegend(legendParentBase, {
|
|
773
|
-
...legendConfig,
|
|
774
|
-
palette,
|
|
775
|
-
isDark,
|
|
776
|
-
width: fixedLegend ? width : layout.width,
|
|
777
|
-
activeGroup: activeTagGroup ?? null,
|
|
778
|
-
});
|
|
779
|
-
legendParentBase
|
|
780
|
-
.selectAll('[data-legend-group]')
|
|
781
|
-
.classed('org-legend-group', true);
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// Inject eye icons into active group capsules (app mode only)
|
|
785
|
-
if (fixedLegend && legendHandle) {
|
|
786
|
-
const computedLayout = legendHandle.getLayout();
|
|
787
|
-
if (computedLayout.activeCapsule?.addonX != null) {
|
|
788
|
-
const capsule = computedLayout.activeCapsule;
|
|
789
|
-
const groupKey = tagAttrKey(capsule.groupName);
|
|
790
|
-
const isHidden = hiddenAttributes?.has(groupKey) ?? false;
|
|
791
|
-
|
|
792
|
-
// Find the rendered active group <g> and append eye icon
|
|
793
|
-
const activeGroupEl = legendParentBase.select(
|
|
794
|
-
`[data-legend-group="${groupKey}"]`
|
|
795
|
-
);
|
|
796
|
-
if (!activeGroupEl.empty()) {
|
|
797
|
-
const eyeX = capsule.addonX!;
|
|
798
|
-
const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
|
|
799
|
-
const hitPad = 6;
|
|
800
|
-
|
|
801
|
-
const eyeG = activeGroupEl
|
|
802
|
-
.append('g')
|
|
803
|
-
.attr('class', 'org-legend-eye')
|
|
804
|
-
.attr('data-legend-visibility', groupKey)
|
|
805
|
-
.style('cursor', 'pointer')
|
|
806
|
-
.attr('opacity', isHidden ? 0.4 : 0.7);
|
|
807
|
-
|
|
808
|
-
eyeG
|
|
809
|
-
.append('rect')
|
|
810
|
-
.attr('x', eyeX - hitPad)
|
|
811
|
-
.attr('y', eyeY - hitPad)
|
|
812
|
-
.attr('width', LEGEND_EYE_SIZE + hitPad * 2)
|
|
813
|
-
.attr('height', LEGEND_EYE_SIZE + hitPad * 2)
|
|
814
|
-
.attr('fill', 'transparent')
|
|
815
|
-
.attr('pointer-events', 'all');
|
|
816
|
-
|
|
817
|
-
eyeG
|
|
818
|
-
.append('path')
|
|
819
|
-
.attr('d', isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH)
|
|
820
|
-
.attr('transform', `translate(${eyeX}, ${eyeY})`)
|
|
821
|
-
.attr('fill', 'none')
|
|
822
|
-
.attr('stroke', palette.textMuted)
|
|
823
|
-
.attr('stroke-width', 1.2)
|
|
824
|
-
.attr('stroke-linecap', 'round')
|
|
825
|
-
.attr('stroke-linejoin', 'round');
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// ============================================================
|
|
833
|
-
// Export convenience function
|
|
834
|
-
// ============================================================
|
|
835
|
-
|
|
836
|
-
export function renderOrgForExport(
|
|
837
|
-
content: string,
|
|
838
|
-
theme: 'light' | 'dark' | 'transparent',
|
|
839
|
-
palette: PaletteColors
|
|
840
|
-
): string {
|
|
841
|
-
const parsed = parseOrg(content, palette);
|
|
842
|
-
if (parsed.error) return '';
|
|
843
|
-
|
|
844
|
-
// Extract hide option for export: cards sized without hidden attributes
|
|
845
|
-
const hideOption = parsed.options?.['hide'];
|
|
846
|
-
const exportHidden = hideOption
|
|
847
|
-
? new Set(hideOption.split(',').map((s) => s.trim().toLowerCase()))
|
|
848
|
-
: undefined;
|
|
849
|
-
|
|
850
|
-
const layout = layoutOrg(parsed, undefined, undefined, exportHidden);
|
|
851
|
-
const isDark = theme === 'dark';
|
|
852
|
-
|
|
853
|
-
const titleOffset =
|
|
854
|
-
parsed.title && parsed.options['no-title'] !== 'on' ? TITLE_HEIGHT : 0;
|
|
855
|
-
const exportWidth = layout.width + DIAGRAM_PADDING * 2;
|
|
856
|
-
const exportHeight = layout.height + DIAGRAM_PADDING * 2 + titleOffset;
|
|
857
|
-
|
|
858
|
-
// No hiddenAttributes passed to renderOrg — export never shows eye icons
|
|
859
|
-
return runInExportContainer(exportWidth, exportHeight, (container) => {
|
|
860
|
-
renderOrg(container, parsed, layout, palette, isDark, undefined, {
|
|
861
|
-
width: exportWidth,
|
|
862
|
-
height: exportHeight,
|
|
863
|
-
});
|
|
864
|
-
return extractExportSvg(container, theme);
|
|
865
|
-
});
|
|
866
|
-
}
|