@diagrammo/dgmo 0.8.9 → 0.8.11
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/AGENTS.md +3 -0
- package/dist/cli.cjs +245 -672
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.d.cts +2 -3
- package/dist/editor.d.ts +2 -3
- package/dist/editor.js.map +1 -1
- package/dist/index.cjs +1623 -800
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +153 -1
- package/dist/index.d.ts +153 -1
- package/dist/index.js +1619 -802
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +28 -2
- package/gallery/fixtures/sitemap-full.dgmo +1 -0
- package/package.json +14 -17
- package/src/boxes-and-lines/layout.ts +48 -8
- package/src/boxes-and-lines/parser.ts +59 -13
- package/src/boxes-and-lines/renderer.ts +34 -138
- package/src/c4/layout.ts +31 -10
- package/src/c4/renderer.ts +25 -138
- package/src/class/renderer.ts +185 -186
- package/src/d3.ts +194 -222
- package/src/echarts.ts +56 -57
- package/src/editor/index.ts +1 -2
- package/src/er/renderer.ts +52 -245
- package/src/gantt/renderer.ts +140 -182
- package/src/gantt/resolver.ts +19 -14
- package/src/index.ts +23 -1
- package/src/infra/renderer.ts +91 -244
- package/src/kanban/renderer.ts +29 -133
- package/src/label-layout.ts +286 -0
- package/src/org/renderer.ts +103 -170
- package/src/render.ts +39 -9
- package/src/sequence/parser.ts +4 -0
- package/src/sequence/renderer.ts +47 -154
- package/src/sitemap/layout.ts +180 -38
- package/src/sitemap/parser.ts +64 -23
- package/src/sitemap/renderer.ts +73 -161
- package/src/utils/arrows.ts +1 -1
- package/src/utils/legend-constants.ts +6 -0
- package/src/utils/legend-d3.ts +400 -0
- package/src/utils/legend-layout.ts +491 -0
- package/src/utils/legend-svg.ts +28 -2
- package/src/utils/legend-types.ts +166 -0
- package/src/utils/parsing.ts +1 -1
- package/src/utils/tag-groups.ts +1 -1
package/src/d3.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as d3Array from 'd3-array';
|
|
|
5
5
|
import cloud from 'd3-cloud';
|
|
6
6
|
import { FONT_FAMILY } from './fonts';
|
|
7
7
|
import { injectBranding } from './branding';
|
|
8
|
+
import { computeQuadrantPointLabels, type LabelRect } from './label-layout';
|
|
8
9
|
|
|
9
10
|
// ============================================================
|
|
10
11
|
// Types
|
|
@@ -19,22 +20,22 @@ export type VisualizationType =
|
|
|
19
20
|
| 'quadrant'
|
|
20
21
|
| 'sequence';
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
interface D3DataItem {
|
|
23
24
|
label: string;
|
|
24
25
|
values: number[];
|
|
25
26
|
color: string | null;
|
|
26
27
|
lineNumber: number;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
interface WordCloudWord {
|
|
30
31
|
text: string;
|
|
31
32
|
weight: number;
|
|
32
33
|
lineNumber: number;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
type WordCloudRotate = 'none' | 'mixed' | 'angled';
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
interface WordCloudOptions {
|
|
38
39
|
rotate: WordCloudRotate;
|
|
39
40
|
max: number;
|
|
40
41
|
minSize: number;
|
|
@@ -56,7 +57,7 @@ export interface ArcLink {
|
|
|
56
57
|
lineNumber: number;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
type ArcOrder = 'appearance' | 'name' | 'group' | 'degree';
|
|
60
61
|
|
|
61
62
|
export interface ArcNodeGroup {
|
|
62
63
|
name: string;
|
|
@@ -65,9 +66,9 @@ export interface ArcNodeGroup {
|
|
|
65
66
|
lineNumber: number;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
type TimelineSort = 'time' | 'group' | 'tag';
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
interface TimelineEvent {
|
|
71
72
|
date: string;
|
|
72
73
|
endDate: string | null;
|
|
73
74
|
label: string;
|
|
@@ -77,13 +78,13 @@ export interface TimelineEvent {
|
|
|
77
78
|
uncertain?: boolean;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
interface TimelineGroup {
|
|
81
82
|
name: string;
|
|
82
83
|
color: string | null;
|
|
83
84
|
lineNumber: number;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
interface TimelineEra {
|
|
87
88
|
startDate: string;
|
|
88
89
|
endDate: string;
|
|
89
90
|
label: string;
|
|
@@ -91,40 +92,40 @@ export interface TimelineEra {
|
|
|
91
92
|
lineNumber: number;
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
interface TimelineMarker {
|
|
95
96
|
date: string;
|
|
96
97
|
label: string;
|
|
97
98
|
color: string | null;
|
|
98
99
|
lineNumber: number;
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
interface VennSet {
|
|
102
103
|
name: string;
|
|
103
104
|
alias: string | null;
|
|
104
105
|
color: string | null;
|
|
105
106
|
lineNumber: number;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
interface VennOverlap {
|
|
109
110
|
sets: string[];
|
|
110
111
|
label: string | null;
|
|
111
112
|
lineNumber: number;
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
+
interface QuadrantLabel {
|
|
115
116
|
text: string;
|
|
116
117
|
color: string | null;
|
|
117
118
|
lineNumber: number;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
|
|
121
|
+
interface QuadrantPoint {
|
|
121
122
|
label: string;
|
|
122
123
|
x: number;
|
|
123
124
|
y: number;
|
|
124
125
|
lineNumber: number;
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
interface QuadrantLabels {
|
|
128
129
|
topRight: QuadrantLabel | null;
|
|
129
130
|
topLeft: QuadrantLabel | null;
|
|
130
131
|
bottomLeft: QuadrantLabel | null;
|
|
@@ -205,9 +206,14 @@ import {
|
|
|
205
206
|
LEGEND_ENTRY_FONT_SIZE as TL_LEGEND_ENTRY_FONT_SIZE,
|
|
206
207
|
LEGEND_ENTRY_DOT_GAP as TL_LEGEND_ENTRY_DOT_GAP,
|
|
207
208
|
LEGEND_ENTRY_TRAIL as TL_LEGEND_ENTRY_TRAIL,
|
|
208
|
-
LEGEND_GROUP_GAP as TL_LEGEND_GROUP_GAP,
|
|
209
209
|
measureLegendText,
|
|
210
210
|
} from './utils/legend-constants';
|
|
211
|
+
import { renderLegendD3 } from './utils/legend-d3';
|
|
212
|
+
import type {
|
|
213
|
+
LegendConfig,
|
|
214
|
+
LegendState,
|
|
215
|
+
LegendCallbacks,
|
|
216
|
+
} from './utils/legend-types';
|
|
211
217
|
import {
|
|
212
218
|
TITLE_FONT_SIZE,
|
|
213
219
|
TITLE_FONT_WEIGHT,
|
|
@@ -4520,8 +4526,7 @@ export function renderTimeline(
|
|
|
4520
4526
|
.attr('dy', '0.35em')
|
|
4521
4527
|
.attr('text-anchor', 'start')
|
|
4522
4528
|
.attr('fill', textColor)
|
|
4523
|
-
.attr('font-size', '
|
|
4524
|
-
.attr('font-weight', '700')
|
|
4529
|
+
.attr('font-size', '13px')
|
|
4525
4530
|
.text(ev.label);
|
|
4526
4531
|
} else {
|
|
4527
4532
|
// Text outside bar - check if it fits on left or must go right
|
|
@@ -4810,8 +4815,7 @@ export function renderTimeline(
|
|
|
4810
4815
|
.attr('dy', '0.35em')
|
|
4811
4816
|
.attr('text-anchor', 'start')
|
|
4812
4817
|
.attr('fill', textColor)
|
|
4813
|
-
.attr('font-size', '
|
|
4814
|
-
.attr('font-weight', '700')
|
|
4818
|
+
.attr('font-size', '13px')
|
|
4815
4819
|
.text(ev.label);
|
|
4816
4820
|
} else {
|
|
4817
4821
|
// Text outside bar - check if it fits on left or must go right
|
|
@@ -4866,7 +4870,7 @@ export function renderTimeline(
|
|
|
4866
4870
|
const LG_ENTRY_FONT_SIZE = TL_LEGEND_ENTRY_FONT_SIZE;
|
|
4867
4871
|
const LG_ENTRY_DOT_GAP = TL_LEGEND_ENTRY_DOT_GAP;
|
|
4868
4872
|
const LG_ENTRY_TRAIL = TL_LEGEND_ENTRY_TRAIL;
|
|
4869
|
-
|
|
4873
|
+
// LG_GROUP_GAP no longer needed — centralized legend handles spacing
|
|
4870
4874
|
const LG_ICON_W = 20; // swimlane icon area (icon + surrounding space) — local
|
|
4871
4875
|
|
|
4872
4876
|
const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
|
|
@@ -4875,10 +4879,6 @@ export function renderTimeline(
|
|
|
4875
4879
|
// Position legend at top, below title
|
|
4876
4880
|
const legendY = title ? 50 : 10;
|
|
4877
4881
|
|
|
4878
|
-
const groupBg = isDark
|
|
4879
|
-
? mix(palette.surface, palette.bg, 50)
|
|
4880
|
-
: mix(palette.surface, palette.bg, 30);
|
|
4881
|
-
|
|
4882
4882
|
// Pre-compute group widths (minified and expanded)
|
|
4883
4883
|
type LegendGroup = {
|
|
4884
4884
|
group: TagGroup;
|
|
@@ -4980,20 +4980,6 @@ export function renderTimeline(
|
|
|
4980
4980
|
|
|
4981
4981
|
if (visibleGroups.length === 0) return;
|
|
4982
4982
|
|
|
4983
|
-
// Compute total width and center horizontally in SVG
|
|
4984
|
-
const totalW =
|
|
4985
|
-
visibleGroups.reduce((s, lg) => {
|
|
4986
|
-
const isActive =
|
|
4987
|
-
viewMode ||
|
|
4988
|
-
(currentActiveGroup != null &&
|
|
4989
|
-
lg.group.name.toLowerCase() ===
|
|
4990
|
-
currentActiveGroup.toLowerCase());
|
|
4991
|
-
return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
|
|
4992
|
-
}, 0) +
|
|
4993
|
-
(visibleGroups.length - 1) * LG_GROUP_GAP;
|
|
4994
|
-
|
|
4995
|
-
let cx = (width - totalW) / 2;
|
|
4996
|
-
|
|
4997
4983
|
// Legend container for data-legend-active attribute
|
|
4998
4984
|
const legendContainer = mainSvg
|
|
4999
4985
|
.append('g')
|
|
@@ -5005,177 +4991,113 @@ export function renderTimeline(
|
|
|
5005
4991
|
);
|
|
5006
4992
|
}
|
|
5007
4993
|
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
currentActiveGroup
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
const entryG = gEl
|
|
5117
|
-
.append('g')
|
|
5118
|
-
.attr('class', 'tl-tag-legend-entry')
|
|
5119
|
-
.attr('data-tag-group', tagKey)
|
|
5120
|
-
.attr('data-legend-entry', tagVal);
|
|
5121
|
-
|
|
5122
|
-
if (!viewMode) {
|
|
5123
|
-
entryG
|
|
5124
|
-
.style('cursor', 'pointer')
|
|
5125
|
-
.on('mouseenter', (event: MouseEvent) => {
|
|
5126
|
-
event.stopPropagation();
|
|
5127
|
-
fadeToTagValue(mainG, tagKey, tagVal);
|
|
5128
|
-
mainSvg
|
|
5129
|
-
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
5130
|
-
.each(function () {
|
|
5131
|
-
const el = d3Selection.select(this);
|
|
5132
|
-
const ev = el.attr('data-legend-entry');
|
|
5133
|
-
if (ev === '__group__') return;
|
|
5134
|
-
const eg = el.attr('data-tag-group');
|
|
5135
|
-
el.attr(
|
|
5136
|
-
'opacity',
|
|
5137
|
-
eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
|
|
5138
|
-
);
|
|
5139
|
-
});
|
|
5140
|
-
})
|
|
5141
|
-
.on('mouseleave', (event: MouseEvent) => {
|
|
5142
|
-
event.stopPropagation();
|
|
5143
|
-
fadeReset(mainG);
|
|
5144
|
-
mainSvg
|
|
5145
|
-
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
5146
|
-
.attr('opacity', 1);
|
|
5147
|
-
})
|
|
5148
|
-
.on('click', (event: MouseEvent) => {
|
|
5149
|
-
event.stopPropagation();
|
|
5150
|
-
});
|
|
5151
|
-
}
|
|
5152
|
-
|
|
5153
|
-
entryG
|
|
5154
|
-
.append('circle')
|
|
5155
|
-
.attr('cx', entryX + LG_DOT_R)
|
|
5156
|
-
.attr('cy', LG_HEIGHT / 2)
|
|
5157
|
-
.attr('r', LG_DOT_R)
|
|
5158
|
-
.attr('fill', entry.color);
|
|
5159
|
-
|
|
5160
|
-
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
5161
|
-
entryG
|
|
5162
|
-
.append('text')
|
|
5163
|
-
.attr('x', textX)
|
|
5164
|
-
.attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
|
|
5165
|
-
.attr('font-size', LG_ENTRY_FONT_SIZE)
|
|
5166
|
-
.attr('font-family', FONT_FAMILY)
|
|
5167
|
-
.attr('fill', palette.textMuted)
|
|
5168
|
-
.text(entry.value);
|
|
5169
|
-
|
|
5170
|
-
entryX =
|
|
5171
|
-
textX +
|
|
5172
|
-
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
5173
|
-
LG_ENTRY_TRAIL;
|
|
5174
|
-
}
|
|
5175
|
-
}
|
|
5176
|
-
|
|
5177
|
-
cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
|
|
5178
|
-
}
|
|
4994
|
+
// Render tag groups via centralized legend system
|
|
4995
|
+
const iconAddon = viewMode ? 0 : LG_ICON_W;
|
|
4996
|
+
const centralGroups = visibleGroups.map((lg) => ({
|
|
4997
|
+
name: lg.group.name,
|
|
4998
|
+
entries: lg.group.entries.map((e) => ({
|
|
4999
|
+
value: e.value,
|
|
5000
|
+
color: e.color,
|
|
5001
|
+
})),
|
|
5002
|
+
}));
|
|
5003
|
+
|
|
5004
|
+
// Determine effective active group for centralized renderer
|
|
5005
|
+
const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
|
|
5006
|
+
|
|
5007
|
+
const centralConfig: LegendConfig = {
|
|
5008
|
+
groups: centralGroups,
|
|
5009
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
5010
|
+
mode: 'fixed',
|
|
5011
|
+
capsulePillAddonWidth: iconAddon,
|
|
5012
|
+
};
|
|
5013
|
+
const centralState: LegendState = { activeGroup: centralActive };
|
|
5014
|
+
|
|
5015
|
+
const centralCallbacks: LegendCallbacks = viewMode
|
|
5016
|
+
? {}
|
|
5017
|
+
: {
|
|
5018
|
+
onGroupToggle: (groupName) => {
|
|
5019
|
+
currentActiveGroup =
|
|
5020
|
+
currentActiveGroup === groupName.toLowerCase()
|
|
5021
|
+
? null
|
|
5022
|
+
: groupName.toLowerCase();
|
|
5023
|
+
drawLegend();
|
|
5024
|
+
recolorEvents();
|
|
5025
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
5026
|
+
},
|
|
5027
|
+
onEntryHover: (groupName, entryValue) => {
|
|
5028
|
+
const tagKey = groupName.toLowerCase();
|
|
5029
|
+
if (entryValue) {
|
|
5030
|
+
const tagVal = entryValue.toLowerCase();
|
|
5031
|
+
fadeToTagValue(mainG, tagKey, tagVal);
|
|
5032
|
+
mainSvg
|
|
5033
|
+
.selectAll<SVGGElement, unknown>('[data-legend-entry]')
|
|
5034
|
+
.each(function () {
|
|
5035
|
+
const el = d3Selection.select(this);
|
|
5036
|
+
const ev = el.attr('data-legend-entry');
|
|
5037
|
+
const eg =
|
|
5038
|
+
el.attr('data-tag-group') ??
|
|
5039
|
+
(el.node() as Element)
|
|
5040
|
+
?.closest?.('[data-tag-group]')
|
|
5041
|
+
?.getAttribute('data-tag-group');
|
|
5042
|
+
el.attr(
|
|
5043
|
+
'opacity',
|
|
5044
|
+
eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
|
|
5045
|
+
);
|
|
5046
|
+
});
|
|
5047
|
+
} else {
|
|
5048
|
+
fadeReset(mainG);
|
|
5049
|
+
mainSvg
|
|
5050
|
+
.selectAll<SVGGElement, unknown>('[data-legend-entry]')
|
|
5051
|
+
.attr('opacity', 1);
|
|
5052
|
+
}
|
|
5053
|
+
},
|
|
5054
|
+
onGroupRendered: (groupName, groupEl, isActive) => {
|
|
5055
|
+
const groupKey = groupName.toLowerCase();
|
|
5056
|
+
groupEl.attr('data-tag-group', groupKey);
|
|
5057
|
+
if (isActive && !viewMode) {
|
|
5058
|
+
const isSwimActive =
|
|
5059
|
+
currentSwimlaneGroup != null &&
|
|
5060
|
+
currentSwimlaneGroup.toLowerCase() === groupKey;
|
|
5061
|
+
const pillWidth =
|
|
5062
|
+
measureLegendText(groupName, LG_PILL_FONT_SIZE) +
|
|
5063
|
+
LG_PILL_PAD;
|
|
5064
|
+
const pillXOff = LG_CAPSULE_PAD;
|
|
5065
|
+
const iconX = pillXOff + pillWidth + 5;
|
|
5066
|
+
const iconY = (LG_HEIGHT - 10) / 2;
|
|
5067
|
+
const iconEl = drawSwimlaneIcon(
|
|
5068
|
+
groupEl,
|
|
5069
|
+
iconX,
|
|
5070
|
+
iconY,
|
|
5071
|
+
isSwimActive
|
|
5072
|
+
);
|
|
5073
|
+
iconEl
|
|
5074
|
+
.attr('data-swimlane-toggle', groupKey)
|
|
5075
|
+
.on('click', (event: MouseEvent) => {
|
|
5076
|
+
event.stopPropagation();
|
|
5077
|
+
currentSwimlaneGroup =
|
|
5078
|
+
currentSwimlaneGroup === groupKey ? null : groupKey;
|
|
5079
|
+
onTagStateChange?.(
|
|
5080
|
+
currentActiveGroup,
|
|
5081
|
+
currentSwimlaneGroup
|
|
5082
|
+
);
|
|
5083
|
+
relayout();
|
|
5084
|
+
});
|
|
5085
|
+
}
|
|
5086
|
+
},
|
|
5087
|
+
};
|
|
5088
|
+
|
|
5089
|
+
const legendInnerG = legendContainer
|
|
5090
|
+
.append('g')
|
|
5091
|
+
.attr('transform', `translate(0, ${legendY})`);
|
|
5092
|
+
renderLegendD3(
|
|
5093
|
+
legendInnerG,
|
|
5094
|
+
centralConfig,
|
|
5095
|
+
centralState,
|
|
5096
|
+
palette,
|
|
5097
|
+
isDark,
|
|
5098
|
+
centralCallbacks,
|
|
5099
|
+
width
|
|
5100
|
+
);
|
|
5179
5101
|
}
|
|
5180
5102
|
|
|
5181
5103
|
// Build a quick lineNumber→event lookup
|
|
@@ -5538,7 +5460,7 @@ export function renderVenn(
|
|
|
5538
5460
|
exportDims?: D3ExportDimensions
|
|
5539
5461
|
): void {
|
|
5540
5462
|
const { vennSets, vennOverlaps, title } = parsed;
|
|
5541
|
-
if (vennSets.length < 2) return;
|
|
5463
|
+
if (vennSets.length < 2 || vennSets.length > 3) return;
|
|
5542
5464
|
|
|
5543
5465
|
const init = initD3Chart(container, palette, exportDims);
|
|
5544
5466
|
if (!init) return;
|
|
@@ -5616,7 +5538,9 @@ export function renderVenn(
|
|
|
5616
5538
|
// Suppress WebKit focus ring on interactive SVG elements
|
|
5617
5539
|
svg
|
|
5618
5540
|
.append('style')
|
|
5619
|
-
.text(
|
|
5541
|
+
.text(
|
|
5542
|
+
'circle:focus, circle:focus-visible { outline-solid: none !important; }'
|
|
5543
|
+
);
|
|
5620
5544
|
|
|
5621
5545
|
// Title
|
|
5622
5546
|
renderChartTitle(
|
|
@@ -5947,7 +5871,7 @@ export function renderVenn(
|
|
|
5947
5871
|
.attr('class', 'venn-hit-target')
|
|
5948
5872
|
.attr('data-line-number', String(vennSets[i].lineNumber))
|
|
5949
5873
|
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
5950
|
-
.style('outline', 'none')
|
|
5874
|
+
.style('outline-solid', 'none')
|
|
5951
5875
|
.on('mouseenter', () => {
|
|
5952
5876
|
showRegionOverlay([i]);
|
|
5953
5877
|
})
|
|
@@ -6003,7 +5927,7 @@ export function renderVenn(
|
|
|
6003
5927
|
.attr('class', 'venn-hit-target')
|
|
6004
5928
|
.attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
|
|
6005
5929
|
.style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
|
|
6006
|
-
.style('outline', 'none')
|
|
5930
|
+
.style('outline-solid', 'none')
|
|
6007
5931
|
.on('mouseenter', () => {
|
|
6008
5932
|
showRegionOverlay(idxs);
|
|
6009
5933
|
})
|
|
@@ -6493,40 +6417,88 @@ export function renderQuadrant(
|
|
|
6493
6417
|
return 'bottom-right';
|
|
6494
6418
|
};
|
|
6495
6419
|
|
|
6420
|
+
// Build obstacle rects from quadrant watermark labels for collision avoidance
|
|
6421
|
+
const POINT_RADIUS = 6;
|
|
6422
|
+
const POINT_LABEL_FONT_SIZE = 12;
|
|
6423
|
+
const quadrantLabelObstacles: LabelRect[] = quadrantDefsWithLabel.map((d) => {
|
|
6424
|
+
const layout = labelLayouts.get(d.label!.text)!;
|
|
6425
|
+
const totalW =
|
|
6426
|
+
Math.max(...layout.lines.map((l) => l.length)) *
|
|
6427
|
+
layout.fontSize *
|
|
6428
|
+
CHAR_WIDTH_RATIO;
|
|
6429
|
+
const totalH = layout.lines.length * layout.fontSize * 1.2;
|
|
6430
|
+
return {
|
|
6431
|
+
x: d.labelX - totalW / 2,
|
|
6432
|
+
y: d.labelY - totalH / 2,
|
|
6433
|
+
w: totalW,
|
|
6434
|
+
h: totalH,
|
|
6435
|
+
};
|
|
6436
|
+
});
|
|
6437
|
+
|
|
6438
|
+
// Compute collision-free label positions for all points
|
|
6439
|
+
const pointPixels = quadrantPoints.map((point) => ({
|
|
6440
|
+
label: point.label,
|
|
6441
|
+
cx: xScale(point.x),
|
|
6442
|
+
cy: yScale(point.y),
|
|
6443
|
+
}));
|
|
6444
|
+
|
|
6445
|
+
const placedPointLabels = computeQuadrantPointLabels(
|
|
6446
|
+
pointPixels,
|
|
6447
|
+
{ left: 0, top: 0, right: chartWidth, bottom: chartHeight },
|
|
6448
|
+
quadrantLabelObstacles,
|
|
6449
|
+
POINT_RADIUS,
|
|
6450
|
+
POINT_LABEL_FONT_SIZE
|
|
6451
|
+
);
|
|
6452
|
+
|
|
6496
6453
|
// Draw data points (circles and labels)
|
|
6497
6454
|
const pointsG = chartG.append('g').attr('class', 'points');
|
|
6498
6455
|
|
|
6499
|
-
quadrantPoints.forEach((point) => {
|
|
6456
|
+
quadrantPoints.forEach((point, i) => {
|
|
6500
6457
|
const cx = xScale(point.x);
|
|
6501
6458
|
const cy = yScale(point.y);
|
|
6502
6459
|
const quadrant = getPointQuadrant(point.x, point.y);
|
|
6503
6460
|
const quadDef = quadrantDefs.find((d) => d.position === quadrant);
|
|
6504
6461
|
const pointColor =
|
|
6505
6462
|
quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
|
|
6463
|
+
const placed = placedPointLabels[i];
|
|
6506
6464
|
|
|
6507
6465
|
const pointG = pointsG
|
|
6508
6466
|
.append('g')
|
|
6509
6467
|
.attr('class', 'point-group')
|
|
6510
6468
|
.attr('data-line-number', String(point.lineNumber));
|
|
6511
6469
|
|
|
6470
|
+
// Connector line (drawn first so it renders behind circle and label)
|
|
6471
|
+
if (placed.connectorLine) {
|
|
6472
|
+
pointG
|
|
6473
|
+
.append('line')
|
|
6474
|
+
.attr('x1', placed.connectorLine.x1)
|
|
6475
|
+
.attr('y1', placed.connectorLine.y1)
|
|
6476
|
+
.attr('x2', placed.connectorLine.x2)
|
|
6477
|
+
.attr('y2', placed.connectorLine.y2)
|
|
6478
|
+
.attr('stroke', pointColor)
|
|
6479
|
+
.attr('stroke-width', 1)
|
|
6480
|
+
.attr('opacity', 0.5);
|
|
6481
|
+
}
|
|
6482
|
+
|
|
6512
6483
|
// Circle with white fill and colored border for visibility on opaque quadrants
|
|
6513
6484
|
pointG
|
|
6514
6485
|
.append('circle')
|
|
6515
6486
|
.attr('cx', cx)
|
|
6516
6487
|
.attr('cy', cy)
|
|
6517
|
-
.attr('r',
|
|
6488
|
+
.attr('r', POINT_RADIUS)
|
|
6518
6489
|
.attr('fill', '#ffffff')
|
|
6519
6490
|
.attr('stroke', pointColor)
|
|
6520
6491
|
.attr('stroke-width', 2);
|
|
6521
6492
|
|
|
6522
|
-
// Label
|
|
6493
|
+
// Label at computed position
|
|
6523
6494
|
pointG
|
|
6524
6495
|
.append('text')
|
|
6525
|
-
.attr('x',
|
|
6526
|
-
.attr('y',
|
|
6527
|
-
.attr('text-anchor',
|
|
6496
|
+
.attr('x', placed.x)
|
|
6497
|
+
.attr('y', placed.y)
|
|
6498
|
+
.attr('text-anchor', placed.anchor)
|
|
6499
|
+
.attr('dominant-baseline', 'central')
|
|
6528
6500
|
.attr('fill', textColor)
|
|
6529
|
-
.attr('font-size',
|
|
6501
|
+
.attr('font-size', `${POINT_LABEL_FONT_SIZE}px`)
|
|
6530
6502
|
.attr('font-weight', '700')
|
|
6531
6503
|
.style('text-shadow', `0 1px 2px ${shadowColor}`)
|
|
6532
6504
|
.text(point.label);
|
|
@@ -6545,7 +6517,7 @@ export function renderQuadrant(
|
|
|
6545
6517
|
})
|
|
6546
6518
|
.on('mouseleave', () => {
|
|
6547
6519
|
hideTooltip(tooltip);
|
|
6548
|
-
pointG.select('circle').attr('r',
|
|
6520
|
+
pointG.select('circle').attr('r', POINT_RADIUS);
|
|
6549
6521
|
})
|
|
6550
6522
|
.on('click', () => {
|
|
6551
6523
|
if (onClickItem && point.lineNumber) onClickItem(point.lineNumber);
|