@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/arc/parser.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// arc parser door — Story 109.2 (arch-review).
|
|
3
|
-
//
|
|
4
|
-
// The six visualizations share one parse state machine (visualizations/parse.ts);
|
|
5
|
-
// this is arc's typed entry point into it, returning the narrowed
|
|
6
|
-
// ParsedArc shape. The registry binds the 'arc' chart type to this door.
|
|
7
|
-
// ============================================================
|
|
8
|
-
|
|
9
|
-
import type { PaletteColors } from '../palettes';
|
|
10
|
-
import { parseVisualization } from '../visualizations/parse';
|
|
11
|
-
import type { ParsedArc } from '../visualizations/types';
|
|
12
|
-
|
|
13
|
-
export function parseArc(content: string, palette?: PaletteColors): ParsedArc {
|
|
14
|
-
return parseVisualization(content, palette) as ParsedArc;
|
|
15
|
-
}
|
package/src/arc/renderer.ts
DELETED
|
@@ -1,633 +0,0 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Arc renderer — Story 109.2 (arch-review). Extracted from d3.ts.
|
|
3
|
-
// ============================================================
|
|
4
|
-
|
|
5
|
-
import * as d3Scale from 'd3-scale';
|
|
6
|
-
import * as d3Selection from 'd3-selection';
|
|
7
|
-
import * as d3Array from 'd3-array';
|
|
8
|
-
import type { D3ExportDimensions } from '../utils/d3-types';
|
|
9
|
-
import { ScaleContext } from '../utils/scaling';
|
|
10
|
-
import { initD3Chart, renderChartTitle } from '../utils/d3-helpers';
|
|
11
|
-
import type {
|
|
12
|
-
ParsedArc,
|
|
13
|
-
ArcLink,
|
|
14
|
-
ArcOrder,
|
|
15
|
-
ArcNodeGroup,
|
|
16
|
-
} from '../visualizations/types';
|
|
17
|
-
import type { PaletteColors } from '../palettes';
|
|
18
|
-
|
|
19
|
-
export function orderArcNodes(
|
|
20
|
-
links: ArcLink[],
|
|
21
|
-
order: ArcOrder,
|
|
22
|
-
groups: ArcNodeGroup[]
|
|
23
|
-
): string[] {
|
|
24
|
-
// Collect all unique nodes in first-appearance order
|
|
25
|
-
const nodeSet = new Set<string>();
|
|
26
|
-
for (const link of links) {
|
|
27
|
-
nodeSet.add(link.source);
|
|
28
|
-
nodeSet.add(link.target);
|
|
29
|
-
}
|
|
30
|
-
const allNodes = Array.from(nodeSet);
|
|
31
|
-
|
|
32
|
-
if (order === 'name') {
|
|
33
|
-
return allNodes.slice().sort((a, b) => a.localeCompare(b));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (order === 'degree') {
|
|
37
|
-
const degree = new Map<string, number>();
|
|
38
|
-
for (const node of allNodes) degree.set(node, 0);
|
|
39
|
-
for (const link of links) {
|
|
40
|
-
degree.set(link.source, degree.get(link.source)! + link.value);
|
|
41
|
-
degree.set(link.target, degree.get(link.target)! + link.value);
|
|
42
|
-
}
|
|
43
|
-
return allNodes.slice().sort((a, b) => {
|
|
44
|
-
const diff = degree.get(b)! - degree.get(a)!;
|
|
45
|
-
return diff !== 0 ? diff : a.localeCompare(b);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (order === 'group') {
|
|
50
|
-
if (groups.length > 0) {
|
|
51
|
-
// Explicit groups: order by ## header order, appearance within each group
|
|
52
|
-
const ordered: string[] = [];
|
|
53
|
-
const placed = new Set<string>();
|
|
54
|
-
for (const group of groups) {
|
|
55
|
-
for (const node of group.nodes) {
|
|
56
|
-
if (!placed.has(node)) {
|
|
57
|
-
ordered.push(node);
|
|
58
|
-
placed.add(node);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
// Orphans at end in first-appearance order
|
|
63
|
-
for (const node of allNodes) {
|
|
64
|
-
if (!placed.has(node)) {
|
|
65
|
-
ordered.push(node);
|
|
66
|
-
placed.add(node);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return ordered;
|
|
70
|
-
}
|
|
71
|
-
// No explicit groups: connectivity clustering via BFS
|
|
72
|
-
const adj = new Map<string, Set<string>>();
|
|
73
|
-
for (const node of allNodes) adj.set(node, new Set());
|
|
74
|
-
for (const link of links) {
|
|
75
|
-
adj.get(link.source)!.add(link.target);
|
|
76
|
-
adj.get(link.target)!.add(link.source);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const degree = new Map<string, number>();
|
|
80
|
-
for (const node of allNodes) degree.set(node, 0);
|
|
81
|
-
for (const link of links) {
|
|
82
|
-
degree.set(link.source, degree.get(link.source)! + link.value);
|
|
83
|
-
degree.set(link.target, degree.get(link.target)! + link.value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const visited = new Set<string>();
|
|
87
|
-
const components: string[][] = [];
|
|
88
|
-
|
|
89
|
-
const remaining = new Set(allNodes);
|
|
90
|
-
while (remaining.size > 0) {
|
|
91
|
-
// Pick highest-degree unvisited node as BFS root
|
|
92
|
-
let root = '';
|
|
93
|
-
let maxDeg = -1;
|
|
94
|
-
for (const node of remaining) {
|
|
95
|
-
if (degree.get(node)! > maxDeg) {
|
|
96
|
-
maxDeg = degree.get(node)!;
|
|
97
|
-
root = node;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// BFS
|
|
101
|
-
const component: string[] = [];
|
|
102
|
-
const queue = [root];
|
|
103
|
-
visited.add(root);
|
|
104
|
-
remaining.delete(root);
|
|
105
|
-
while (queue.length > 0) {
|
|
106
|
-
const curr = queue.shift()!;
|
|
107
|
-
component.push(curr);
|
|
108
|
-
for (const neighbor of adj.get(curr)!) {
|
|
109
|
-
if (!visited.has(neighbor)) {
|
|
110
|
-
visited.add(neighbor);
|
|
111
|
-
remaining.delete(neighbor);
|
|
112
|
-
queue.push(neighbor);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
components.push(component);
|
|
117
|
-
}
|
|
118
|
-
// Sort components by size descending
|
|
119
|
-
components.sort((a, b) => b.length - a.length);
|
|
120
|
-
return components.flat();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 'appearance' — first-appearance order (default)
|
|
124
|
-
return allNodes;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ============================================================
|
|
128
|
-
// Arc Diagram Renderer
|
|
129
|
-
// ============================================================
|
|
130
|
-
|
|
131
|
-
const ARC_MARGIN_TOP = 60;
|
|
132
|
-
const ARC_MARGIN_RIGHT = 40;
|
|
133
|
-
const ARC_MARGIN_BOTTOM = 60;
|
|
134
|
-
const ARC_MARGIN_LEFT = 40;
|
|
135
|
-
const ARC_MARGIN_LEFT_VERTICAL = 120;
|
|
136
|
-
const ARC_NODE_RADIUS = 5;
|
|
137
|
-
const ARC_NODE_STROKE_WIDTH = 1.5;
|
|
138
|
-
const ARC_NODE_LABEL_FONT = 11;
|
|
139
|
-
const ARC_GROUP_LABEL_FONT = 12;
|
|
140
|
-
const ARC_BAND_HALF_W = 60;
|
|
141
|
-
const ARC_BAND_HALF_H = 40;
|
|
142
|
-
const ARC_BAND_RADIUS = 4;
|
|
143
|
-
const ARC_BAND_LABEL_X_OFFSET = 6;
|
|
144
|
-
const ARC_BAND_LABEL_Y_OFFSET = 14;
|
|
145
|
-
const ARC_BAND_LABEL_BOTTOM_OFFSET = 4;
|
|
146
|
-
const ARC_NODE_LABEL_X_OFFSET = 14;
|
|
147
|
-
const ARC_NODE_LABEL_Y_OFFSET = 20;
|
|
148
|
-
const ARC_STROKE_MIN = 1.5;
|
|
149
|
-
const ARC_STROKE_MAX = 6;
|
|
150
|
-
const ARC_BASELINE_STROKE_WIDTH = 1;
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Renders an arc diagram into the given container using D3.
|
|
154
|
-
*/
|
|
155
|
-
export function renderArcDiagram(
|
|
156
|
-
container: HTMLDivElement,
|
|
157
|
-
parsed: ParsedArc,
|
|
158
|
-
palette: PaletteColors,
|
|
159
|
-
_isDark: boolean,
|
|
160
|
-
onClickItem?: (lineNumber: number) => void,
|
|
161
|
-
exportDims?: D3ExportDimensions
|
|
162
|
-
): void {
|
|
163
|
-
const { links, orientation, arcOrder, arcNodeGroups } = parsed;
|
|
164
|
-
const title = parsed.noTitle ? null : parsed.title;
|
|
165
|
-
if (links.length === 0) return;
|
|
166
|
-
|
|
167
|
-
const init = initD3Chart(container, palette, exportDims);
|
|
168
|
-
if (!init) return;
|
|
169
|
-
const { svg, width, height, textColor, mutedColor, bgColor, colors } = init;
|
|
170
|
-
|
|
171
|
-
const isVertical = orientation === 'vertical';
|
|
172
|
-
|
|
173
|
-
const nodes = orderArcNodes(links, arcOrder, arcNodeGroups);
|
|
174
|
-
|
|
175
|
-
const idealWidth = isVertical
|
|
176
|
-
? ARC_MARGIN_LEFT_VERTICAL + ARC_MARGIN_RIGHT + ARC_BAND_HALF_W * 2 + 100
|
|
177
|
-
: nodes.length * 20 + ARC_MARGIN_LEFT + ARC_MARGIN_RIGHT;
|
|
178
|
-
const ctx = exportDims
|
|
179
|
-
? ScaleContext.identity()
|
|
180
|
-
: ScaleContext.from(width, idealWidth);
|
|
181
|
-
|
|
182
|
-
const sMarginTop = ctx.aesthetic(ARC_MARGIN_TOP);
|
|
183
|
-
const sMarginRight = ctx.aesthetic(ARC_MARGIN_RIGHT);
|
|
184
|
-
const sMarginBottom = ctx.aesthetic(ARC_MARGIN_BOTTOM);
|
|
185
|
-
const sMarginLeft = isVertical
|
|
186
|
-
? ctx.aesthetic(ARC_MARGIN_LEFT_VERTICAL)
|
|
187
|
-
: ctx.aesthetic(ARC_MARGIN_LEFT);
|
|
188
|
-
const sNodeRadius = ctx.structural(ARC_NODE_RADIUS);
|
|
189
|
-
const sNodeStrokeWidth = ctx.structural(ARC_NODE_STROKE_WIDTH);
|
|
190
|
-
const sNodeLabelFont = ctx.text(ARC_NODE_LABEL_FONT);
|
|
191
|
-
const sGroupLabelFont = ctx.text(ARC_GROUP_LABEL_FONT);
|
|
192
|
-
const sBandHalfW = ctx.aesthetic(ARC_BAND_HALF_W);
|
|
193
|
-
const sBandHalfH = ctx.aesthetic(ARC_BAND_HALF_H);
|
|
194
|
-
const sBandRadius = ctx.structural(ARC_BAND_RADIUS);
|
|
195
|
-
const sBandLabelXOffset = ctx.structural(ARC_BAND_LABEL_X_OFFSET);
|
|
196
|
-
const sBandLabelYOffset = ctx.structural(ARC_BAND_LABEL_Y_OFFSET);
|
|
197
|
-
const sBandLabelBottomOffset = ctx.structural(ARC_BAND_LABEL_BOTTOM_OFFSET);
|
|
198
|
-
const sNodeLabelXOffset = ctx.structural(ARC_NODE_LABEL_X_OFFSET);
|
|
199
|
-
const sNodeLabelYOffset = ctx.structural(ARC_NODE_LABEL_Y_OFFSET);
|
|
200
|
-
const sStrokeMin = ctx.structural(ARC_STROKE_MIN);
|
|
201
|
-
const sStrokeMax = ctx.structural(ARC_STROKE_MAX);
|
|
202
|
-
const sBaselineDash = `${ctx.structural(4)},${ctx.structural(4)}`;
|
|
203
|
-
const sBaselineStrokeWidth = ctx.structural(ARC_BASELINE_STROKE_WIDTH);
|
|
204
|
-
|
|
205
|
-
svg.attr('preserveAspectRatio', 'xMidYMin meet');
|
|
206
|
-
if (ctx.isBelowFloor) {
|
|
207
|
-
svg.attr('width', '100%');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const margin = {
|
|
211
|
-
top: sMarginTop,
|
|
212
|
-
right: sMarginRight,
|
|
213
|
-
bottom: sMarginBottom,
|
|
214
|
-
left: sMarginLeft,
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
const innerWidth = width - margin.left - margin.right;
|
|
218
|
-
const innerHeight = height - margin.top - margin.bottom;
|
|
219
|
-
|
|
220
|
-
const nodeColorMap = new Map<string, string>();
|
|
221
|
-
for (const group of arcNodeGroups) {
|
|
222
|
-
if (group.color) {
|
|
223
|
-
for (const node of group.nodes) {
|
|
224
|
-
if (!nodeColorMap.has(node)) {
|
|
225
|
-
nodeColorMap.set(node, group.color);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const groupNodeSets = new Map<string, Set<string>>();
|
|
232
|
-
for (const group of arcNodeGroups) {
|
|
233
|
-
groupNodeSets.set(group.name, new Set(group.nodes));
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const values = links.map((l) => l.value);
|
|
237
|
-
const [minVal, maxVal] = d3Array.extent(values) as [number, number];
|
|
238
|
-
const strokeScale = d3Scale
|
|
239
|
-
.scaleLinear()
|
|
240
|
-
.domain([minVal, maxVal])
|
|
241
|
-
.range([sStrokeMin, sStrokeMax]);
|
|
242
|
-
|
|
243
|
-
const g = svg
|
|
244
|
-
.append('g')
|
|
245
|
-
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
246
|
-
|
|
247
|
-
// Title
|
|
248
|
-
renderChartTitle(
|
|
249
|
-
svg,
|
|
250
|
-
title,
|
|
251
|
-
parsed.titleLineNumber,
|
|
252
|
-
width,
|
|
253
|
-
textColor,
|
|
254
|
-
onClickItem
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
// Build adjacency map for hover interactions
|
|
258
|
-
const neighbors = new Map<string, Set<string>>();
|
|
259
|
-
for (const node of nodes) neighbors.set(node, new Set());
|
|
260
|
-
for (const link of links) {
|
|
261
|
-
neighbors.get(link.source)!.add(link.target);
|
|
262
|
-
neighbors.get(link.target)!.add(link.source);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const FADE_OPACITY = 0.1;
|
|
266
|
-
|
|
267
|
-
function handleMouseEnter(hovered: string) {
|
|
268
|
-
const connected = neighbors.get(hovered)!;
|
|
269
|
-
|
|
270
|
-
g.selectAll<SVGPathElement, unknown>('.arc-link').each(function () {
|
|
271
|
-
const el = d3Selection.select(this);
|
|
272
|
-
const src = el.attr('data-source');
|
|
273
|
-
const tgt = el.attr('data-target');
|
|
274
|
-
const isRelated = src === hovered || tgt === hovered;
|
|
275
|
-
el.attr('stroke-opacity', isRelated ? 0.85 : FADE_OPACITY);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
g.selectAll<SVGGElement, unknown>('.arc-node').each(function () {
|
|
279
|
-
const el = d3Selection.select(this);
|
|
280
|
-
const name = el.attr('data-node');
|
|
281
|
-
const isRelated = name === hovered || connected.has(name!);
|
|
282
|
-
el.attr('opacity', isRelated ? 1 : FADE_OPACITY);
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function handleMouseLeave() {
|
|
287
|
-
g.selectAll<SVGPathElement, unknown>('.arc-link').attr(
|
|
288
|
-
'stroke-opacity',
|
|
289
|
-
0.7
|
|
290
|
-
);
|
|
291
|
-
g.selectAll<SVGGElement, unknown>('.arc-node').attr('opacity', 1);
|
|
292
|
-
g.selectAll<SVGRectElement, unknown>('.arc-group-band').attr(
|
|
293
|
-
'fill-opacity',
|
|
294
|
-
0.06
|
|
295
|
-
);
|
|
296
|
-
g.selectAll<SVGTextElement, unknown>('.arc-group-label').attr(
|
|
297
|
-
'fill-opacity',
|
|
298
|
-
0.5
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function handleGroupEnter(groupName: string) {
|
|
303
|
-
const members = groupNodeSets.get(groupName);
|
|
304
|
-
if (!members) return;
|
|
305
|
-
|
|
306
|
-
g.selectAll<SVGPathElement, unknown>('.arc-link').each(function () {
|
|
307
|
-
const el = d3Selection.select(this);
|
|
308
|
-
const isRelated =
|
|
309
|
-
members.has(el.attr('data-source')!) ||
|
|
310
|
-
members.has(el.attr('data-target')!);
|
|
311
|
-
el.attr('stroke-opacity', isRelated ? 0.85 : FADE_OPACITY);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
g.selectAll<SVGGElement, unknown>('.arc-node').each(function () {
|
|
315
|
-
const el = d3Selection.select(this);
|
|
316
|
-
el.attr('opacity', members.has(el.attr('data-node')!) ? 1 : FADE_OPACITY);
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
g.selectAll<SVGRectElement, unknown>('.arc-group-band').each(function () {
|
|
320
|
-
const el = d3Selection.select(this);
|
|
321
|
-
el.attr(
|
|
322
|
-
'fill-opacity',
|
|
323
|
-
el.attr('data-group') === groupName ? 0.18 : 0.03
|
|
324
|
-
);
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
g.selectAll<SVGTextElement, unknown>('.arc-group-label').each(function () {
|
|
328
|
-
const el = d3Selection.select(this);
|
|
329
|
-
el.attr('fill-opacity', el.attr('data-group') === groupName ? 1 : 0.2);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (isVertical) {
|
|
334
|
-
// Vertical layout: nodes along Y axis, arcs curve to the right
|
|
335
|
-
const yScale = d3Scale
|
|
336
|
-
.scalePoint<string>()
|
|
337
|
-
.domain(nodes)
|
|
338
|
-
.range([0, innerHeight])
|
|
339
|
-
.padding(0.5);
|
|
340
|
-
|
|
341
|
-
const baseX = innerWidth / 2;
|
|
342
|
-
|
|
343
|
-
// Group bands (shaded regions bounding grouped nodes)
|
|
344
|
-
if (arcNodeGroups.length > 0) {
|
|
345
|
-
const bandPad = (yScale.step?.() ?? 20) * 0.4;
|
|
346
|
-
for (const group of arcNodeGroups) {
|
|
347
|
-
const groupNodes = group.nodes.filter((n) => nodes.includes(n));
|
|
348
|
-
if (groupNodes.length === 0) continue;
|
|
349
|
-
const positions = groupNodes.map((n) => yScale(n)!);
|
|
350
|
-
const minY = Math.min(...positions) - bandPad;
|
|
351
|
-
const maxY = Math.max(...positions) + bandPad;
|
|
352
|
-
|
|
353
|
-
g.append('rect')
|
|
354
|
-
.attr('class', 'arc-group-band')
|
|
355
|
-
.attr('data-group', group.name)
|
|
356
|
-
.attr('data-line-number', String(group.lineNumber))
|
|
357
|
-
.attr('x', baseX - sBandHalfW)
|
|
358
|
-
.attr('y', minY)
|
|
359
|
-
.attr('width', sBandHalfW * 2)
|
|
360
|
-
.attr('height', maxY - minY)
|
|
361
|
-
.attr('rx', sBandRadius)
|
|
362
|
-
.attr('fill', textColor)
|
|
363
|
-
.attr('fill-opacity', 0.06)
|
|
364
|
-
.style('cursor', 'pointer')
|
|
365
|
-
.on('mouseenter', () => handleGroupEnter(group.name))
|
|
366
|
-
.on('mouseleave', handleMouseLeave)
|
|
367
|
-
.on('click', () => {
|
|
368
|
-
if (onClickItem) onClickItem(group.lineNumber);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
g.append('text')
|
|
372
|
-
.attr('class', 'arc-group-label')
|
|
373
|
-
.attr('data-group', group.name)
|
|
374
|
-
.attr('data-line-number', String(group.lineNumber))
|
|
375
|
-
.attr('x', baseX - sBandHalfW + sBandLabelXOffset)
|
|
376
|
-
.attr('y', minY + sBandLabelYOffset)
|
|
377
|
-
.attr('fill', textColor)
|
|
378
|
-
.attr('font-size', `${sGroupLabelFont}px`)
|
|
379
|
-
.attr('font-weight', '600')
|
|
380
|
-
.attr('fill-opacity', 0.5)
|
|
381
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
382
|
-
.text(group.name)
|
|
383
|
-
.on('mouseenter', () => handleGroupEnter(group.name))
|
|
384
|
-
.on('mouseleave', handleMouseLeave)
|
|
385
|
-
.on('click', () => {
|
|
386
|
-
if (onClickItem) onClickItem(group.lineNumber);
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Dashed vertical baseline
|
|
392
|
-
g.append('line')
|
|
393
|
-
.attr('x1', baseX)
|
|
394
|
-
.attr('y1', 0)
|
|
395
|
-
.attr('x2', baseX)
|
|
396
|
-
.attr('y2', innerHeight)
|
|
397
|
-
.attr('stroke', mutedColor)
|
|
398
|
-
.attr('stroke-width', sBaselineStrokeWidth)
|
|
399
|
-
.attr('stroke-dasharray', sBaselineDash);
|
|
400
|
-
|
|
401
|
-
// Arcs
|
|
402
|
-
links.forEach((link, idx) => {
|
|
403
|
-
const y1 = yScale(link.source)!;
|
|
404
|
-
const y2 = yScale(link.target)!;
|
|
405
|
-
const midY = (y1 + y2) / 2;
|
|
406
|
-
const distance = Math.abs(y2 - y1);
|
|
407
|
-
const controlX = baseX + distance * 0.4;
|
|
408
|
-
// colors is non-empty; modulo guarantees in-bounds.
|
|
409
|
-
const color = link.color ?? colors[idx % colors.length]!;
|
|
410
|
-
|
|
411
|
-
g.append('path')
|
|
412
|
-
.attr('class', 'arc-link')
|
|
413
|
-
.attr('data-source', link.source)
|
|
414
|
-
.attr('data-target', link.target)
|
|
415
|
-
.attr('data-line-number', String(link.lineNumber))
|
|
416
|
-
.attr('d', `M ${baseX},${y1} Q ${controlX},${midY} ${baseX},${y2}`)
|
|
417
|
-
.attr('fill', 'none')
|
|
418
|
-
.attr('stroke', color)
|
|
419
|
-
.attr('stroke-width', strokeScale(link.value))
|
|
420
|
-
.attr('stroke-opacity', 0.7)
|
|
421
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
422
|
-
.on('click', () => {
|
|
423
|
-
if (onClickItem && link.lineNumber) onClickItem(link.lineNumber);
|
|
424
|
-
});
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
// Node circles and labels
|
|
428
|
-
for (const node of nodes) {
|
|
429
|
-
const y = yScale(node)!;
|
|
430
|
-
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
431
|
-
// Find the first link involving this node (for line number and click target)
|
|
432
|
-
const nodeLink = links.find(
|
|
433
|
-
(l) => l.source === node || l.target === node
|
|
434
|
-
);
|
|
435
|
-
|
|
436
|
-
const nodeG = g
|
|
437
|
-
.append('g')
|
|
438
|
-
.attr('class', 'arc-node')
|
|
439
|
-
.attr('data-node', node)
|
|
440
|
-
.attr(
|
|
441
|
-
'data-line-number',
|
|
442
|
-
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
443
|
-
)
|
|
444
|
-
.style('cursor', 'pointer')
|
|
445
|
-
.on('mouseenter', () => handleMouseEnter(node))
|
|
446
|
-
.on('mouseleave', handleMouseLeave)
|
|
447
|
-
.on('click', () => {
|
|
448
|
-
if (onClickItem && nodeLink?.lineNumber)
|
|
449
|
-
onClickItem(nodeLink.lineNumber);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
nodeG
|
|
453
|
-
.append('circle')
|
|
454
|
-
.attr('cx', baseX)
|
|
455
|
-
.attr('cy', y)
|
|
456
|
-
.attr('r', sNodeRadius)
|
|
457
|
-
.attr('fill', nodeColor)
|
|
458
|
-
.attr('stroke', bgColor)
|
|
459
|
-
.attr('stroke-width', sNodeStrokeWidth);
|
|
460
|
-
|
|
461
|
-
nodeG
|
|
462
|
-
.append('text')
|
|
463
|
-
.attr('x', baseX - sNodeLabelXOffset)
|
|
464
|
-
.attr('y', y)
|
|
465
|
-
.attr('dy', '0.35em')
|
|
466
|
-
.attr('text-anchor', 'end')
|
|
467
|
-
.attr('fill', textColor)
|
|
468
|
-
.attr('font-size', `${sNodeLabelFont}px`)
|
|
469
|
-
.text(node);
|
|
470
|
-
}
|
|
471
|
-
} else {
|
|
472
|
-
// Horizontal layout (default): nodes along X axis, arcs curve upward
|
|
473
|
-
const xScale = d3Scale
|
|
474
|
-
.scalePoint<string>()
|
|
475
|
-
.domain(nodes)
|
|
476
|
-
.range([0, innerWidth])
|
|
477
|
-
.padding(0.5);
|
|
478
|
-
|
|
479
|
-
// Live preview centers the baseline in the host container; export sizes the
|
|
480
|
-
// canvas to the arc band so the SVG carries no dead whitespace (arcs bow up
|
|
481
|
-
// by ~distance*0.2 above the baseline, labels/bands sit just below it).
|
|
482
|
-
let baseY = innerHeight / 2;
|
|
483
|
-
if (exportDims) {
|
|
484
|
-
let maxDist = 0;
|
|
485
|
-
for (const l of links) {
|
|
486
|
-
const a = xScale(l.source);
|
|
487
|
-
const b = xScale(l.target);
|
|
488
|
-
if (a != null && b != null)
|
|
489
|
-
maxDist = Math.max(maxDist, Math.abs(b - a));
|
|
490
|
-
}
|
|
491
|
-
const above = maxDist * 0.2 + sNodeRadius;
|
|
492
|
-
const below = Math.max(sBandHalfH, sNodeLabelYOffset + sNodeLabelFont);
|
|
493
|
-
baseY = above;
|
|
494
|
-
const tightHeight = margin.top + above + below + margin.bottom;
|
|
495
|
-
svg
|
|
496
|
-
.attr('height', tightHeight)
|
|
497
|
-
.attr('viewBox', `0 0 ${width} ${tightHeight}`);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Group bands (shaded regions bounding grouped nodes)
|
|
501
|
-
if (arcNodeGroups.length > 0) {
|
|
502
|
-
const bandPad = (xScale.step?.() ?? 20) * 0.4;
|
|
503
|
-
for (const group of arcNodeGroups) {
|
|
504
|
-
const groupNodes = group.nodes.filter((n) => nodes.includes(n));
|
|
505
|
-
if (groupNodes.length === 0) continue;
|
|
506
|
-
const positions = groupNodes.map((n) => xScale(n)!);
|
|
507
|
-
const minX = Math.min(...positions) - bandPad;
|
|
508
|
-
const maxX = Math.max(...positions) + bandPad;
|
|
509
|
-
|
|
510
|
-
g.append('rect')
|
|
511
|
-
.attr('class', 'arc-group-band')
|
|
512
|
-
.attr('data-group', group.name)
|
|
513
|
-
.attr('data-line-number', String(group.lineNumber))
|
|
514
|
-
.attr('x', minX)
|
|
515
|
-
.attr('y', baseY - sBandHalfH)
|
|
516
|
-
.attr('width', maxX - minX)
|
|
517
|
-
.attr('height', sBandHalfH * 2)
|
|
518
|
-
.attr('rx', sBandRadius)
|
|
519
|
-
.attr('fill', textColor)
|
|
520
|
-
.attr('fill-opacity', 0.06)
|
|
521
|
-
.style('cursor', 'pointer')
|
|
522
|
-
.on('mouseenter', () => handleGroupEnter(group.name))
|
|
523
|
-
.on('mouseleave', handleMouseLeave)
|
|
524
|
-
.on('click', () => {
|
|
525
|
-
if (onClickItem) onClickItem(group.lineNumber);
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
g.append('text')
|
|
529
|
-
.attr('class', 'arc-group-label')
|
|
530
|
-
.attr('data-group', group.name)
|
|
531
|
-
.attr('data-line-number', String(group.lineNumber))
|
|
532
|
-
.attr('x', (minX + maxX) / 2)
|
|
533
|
-
.attr('y', baseY + sBandHalfH - sBandLabelBottomOffset)
|
|
534
|
-
.attr('text-anchor', 'middle')
|
|
535
|
-
.attr('fill', textColor)
|
|
536
|
-
.attr('font-size', `${sGroupLabelFont}px`)
|
|
537
|
-
.attr('font-weight', '600')
|
|
538
|
-
.attr('fill-opacity', 0.5)
|
|
539
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
540
|
-
.text(group.name)
|
|
541
|
-
.on('mouseenter', () => handleGroupEnter(group.name))
|
|
542
|
-
.on('mouseleave', handleMouseLeave)
|
|
543
|
-
.on('click', () => {
|
|
544
|
-
if (onClickItem) onClickItem(group.lineNumber);
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
// Dashed horizontal baseline
|
|
550
|
-
g.append('line')
|
|
551
|
-
.attr('x1', 0)
|
|
552
|
-
.attr('y1', baseY)
|
|
553
|
-
.attr('x2', innerWidth)
|
|
554
|
-
.attr('y2', baseY)
|
|
555
|
-
.attr('stroke', mutedColor)
|
|
556
|
-
.attr('stroke-width', sBaselineStrokeWidth)
|
|
557
|
-
.attr('stroke-dasharray', sBaselineDash);
|
|
558
|
-
|
|
559
|
-
// Arcs
|
|
560
|
-
links.forEach((link, idx) => {
|
|
561
|
-
const x1 = xScale(link.source)!;
|
|
562
|
-
const x2 = xScale(link.target)!;
|
|
563
|
-
const midX = (x1 + x2) / 2;
|
|
564
|
-
const distance = Math.abs(x2 - x1);
|
|
565
|
-
const controlY = baseY - distance * 0.4;
|
|
566
|
-
// colors is non-empty; modulo guarantees in-bounds.
|
|
567
|
-
const color = link.color ?? colors[idx % colors.length]!;
|
|
568
|
-
|
|
569
|
-
g.append('path')
|
|
570
|
-
.attr('class', 'arc-link')
|
|
571
|
-
.attr('data-source', link.source)
|
|
572
|
-
.attr('data-target', link.target)
|
|
573
|
-
.attr('data-line-number', String(link.lineNumber))
|
|
574
|
-
.attr('d', `M ${x1},${baseY} Q ${midX},${controlY} ${x2},${baseY}`)
|
|
575
|
-
.attr('fill', 'none')
|
|
576
|
-
.attr('stroke', color)
|
|
577
|
-
.attr('stroke-width', strokeScale(link.value))
|
|
578
|
-
.attr('stroke-opacity', 0.7)
|
|
579
|
-
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
580
|
-
.on('click', () => {
|
|
581
|
-
if (onClickItem && link.lineNumber) onClickItem(link.lineNumber);
|
|
582
|
-
});
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
// Node circles and labels
|
|
586
|
-
for (const node of nodes) {
|
|
587
|
-
const x = xScale(node)!;
|
|
588
|
-
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
589
|
-
// Find the first link involving this node (for line number and click target)
|
|
590
|
-
const nodeLink = links.find(
|
|
591
|
-
(l) => l.source === node || l.target === node
|
|
592
|
-
);
|
|
593
|
-
|
|
594
|
-
const nodeG = g
|
|
595
|
-
.append('g')
|
|
596
|
-
.attr('class', 'arc-node')
|
|
597
|
-
.attr('data-node', node)
|
|
598
|
-
.attr(
|
|
599
|
-
'data-line-number',
|
|
600
|
-
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
601
|
-
)
|
|
602
|
-
.style('cursor', 'pointer')
|
|
603
|
-
.on('mouseenter', () => handleMouseEnter(node))
|
|
604
|
-
.on('mouseleave', handleMouseLeave)
|
|
605
|
-
.on('click', () => {
|
|
606
|
-
if (onClickItem && nodeLink?.lineNumber)
|
|
607
|
-
onClickItem(nodeLink.lineNumber);
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
nodeG
|
|
611
|
-
.append('circle')
|
|
612
|
-
.attr('cx', x)
|
|
613
|
-
.attr('cy', baseY)
|
|
614
|
-
.attr('r', sNodeRadius)
|
|
615
|
-
.attr('fill', nodeColor)
|
|
616
|
-
.attr('stroke', bgColor)
|
|
617
|
-
.attr('stroke-width', sNodeStrokeWidth);
|
|
618
|
-
|
|
619
|
-
nodeG
|
|
620
|
-
.append('text')
|
|
621
|
-
.attr('x', x)
|
|
622
|
-
.attr('y', baseY + sNodeLabelYOffset)
|
|
623
|
-
.attr('text-anchor', 'middle')
|
|
624
|
-
.attr('fill', textColor)
|
|
625
|
-
.attr('font-size', `${sNodeLabelFont}px`)
|
|
626
|
-
.text(node);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// ============================================================
|
|
632
|
-
// Timeline Era Bands
|
|
633
|
-
// ============================================================
|