@diagrammo/dgmo 0.8.17 → 0.8.19
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/cli.cjs +103 -103
- package/dist/editor.cjs +1 -1
- package/dist/editor.cjs.map +1 -1
- package/dist/editor.js +1 -1
- package/dist/editor.js.map +1 -1
- package/dist/highlight.cjs +1 -1
- package/dist/highlight.cjs.map +1 -1
- package/dist/highlight.js +1 -1
- package/dist/highlight.js.map +1 -1
- package/dist/index.cjs +1306 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -15
- package/dist/index.d.ts +120 -15
- package/dist/index.js +1325 -151
- package/dist/index.js.map +1 -1
- package/docs/guide/how-dgmo-thinks.md +277 -0
- package/docs/guide/registry.json +1 -0
- package/gallery/fixtures/gantt-sprints.dgmo +20 -0
- package/package.json +1 -1
- package/src/colors.ts +1 -1
- package/src/editor/dgmo.grammar +1 -1
- package/src/editor/dgmo.grammar.js +1 -1
- package/src/gantt/calculator.ts +120 -7
- package/src/gantt/parser.ts +98 -3
- package/src/gantt/renderer.ts +410 -95
- package/src/gantt/types.ts +23 -2
- package/src/index.ts +10 -2
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +14 -2
- package/src/sequence/renderer.ts +186 -49
- package/src/sharing.ts +86 -49
- package/src/utils/duration.ts +16 -2
- package/src/utils/legend-constants.ts +11 -0
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +148 -17
- package/src/utils/legend-types.ts +45 -0
package/src/sharing.ts
CHANGED
|
@@ -6,24 +6,45 @@ import {
|
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://online.diagrammo.app';
|
|
7
7
|
const COMPRESSED_SIZE_LIMIT = 8192; // 8 KB
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Compact view state schema (ADR-6).
|
|
11
|
+
* All fields optional. Only non-default values are encoded.
|
|
12
|
+
* `tag: null` means "user chose none"; absent `tag` means "use DSL default" (ADR-5).
|
|
13
|
+
*/
|
|
14
|
+
export interface CompactViewState {
|
|
15
|
+
tag?: string | null; // active tag override (null = "none")
|
|
16
|
+
cs?: number[]; // collapsed sections (sequence line numbers)
|
|
17
|
+
cg?: string[]; // collapsed groups/nodes (IDs or names)
|
|
18
|
+
swim?: string | null; // swimlane tag group
|
|
19
|
+
cl?: string[]; // collapsed lanes
|
|
20
|
+
cc?: string[]; // collapsed columns (kanban)
|
|
21
|
+
rm?: string; // render mode override
|
|
22
|
+
htv?: Record<string, string[]>; // hidden tag values
|
|
23
|
+
ha?: string[]; // hidden attributes
|
|
24
|
+
enl?: number[]; // expanded note lines (sequence)
|
|
25
|
+
sem?: boolean; // semantic colors (ER)
|
|
26
|
+
cm?: boolean; // compact meta (kanban)
|
|
27
|
+
c4l?: string; // C4 level
|
|
28
|
+
c4s?: string; // C4 system
|
|
29
|
+
c4c?: string; // C4 container
|
|
30
|
+
rps?: number; // RPS multiplier (infra)
|
|
31
|
+
spd?: number; // playback speed (infra)
|
|
32
|
+
io?: Record<string, number>; // instance overrides (infra)
|
|
16
33
|
}
|
|
17
34
|
|
|
18
35
|
export interface DecodedDiagramUrl {
|
|
19
36
|
dsl: string;
|
|
20
|
-
viewState:
|
|
37
|
+
viewState: CompactViewState;
|
|
38
|
+
palette?: string;
|
|
39
|
+
theme?: 'light' | 'dark';
|
|
21
40
|
filename?: string;
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
export interface EncodeDiagramUrlOptions {
|
|
25
44
|
baseUrl?: string;
|
|
26
|
-
viewState?:
|
|
45
|
+
viewState?: CompactViewState;
|
|
46
|
+
palette?: string;
|
|
47
|
+
theme?: 'light' | 'dark';
|
|
27
48
|
filename?: string;
|
|
28
49
|
}
|
|
29
50
|
|
|
@@ -36,6 +57,34 @@ export type EncodeDiagramUrlResult =
|
|
|
36
57
|
limit: number;
|
|
37
58
|
};
|
|
38
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Encode a CompactViewState to a compressed string for URL embedding.
|
|
62
|
+
* Returns empty string if state has no keys (ADR-4).
|
|
63
|
+
*/
|
|
64
|
+
export function encodeViewState(state: CompactViewState): string {
|
|
65
|
+
const keys = Object.keys(state);
|
|
66
|
+
if (keys.length === 0) return '';
|
|
67
|
+
return compressToEncodedURIComponent(JSON.stringify(state));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Decode a compressed view state string back to CompactViewState.
|
|
72
|
+
* Returns empty object on failure (no crash).
|
|
73
|
+
*/
|
|
74
|
+
export function decodeViewState(encoded: string): CompactViewState {
|
|
75
|
+
if (!encoded) return {};
|
|
76
|
+
try {
|
|
77
|
+
const json = decompressFromEncodedURIComponent(encoded);
|
|
78
|
+
if (!json) return {};
|
|
79
|
+
const parsed = JSON.parse(json);
|
|
80
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
81
|
+
return {};
|
|
82
|
+
return parsed as CompactViewState;
|
|
83
|
+
} catch {
|
|
84
|
+
return {};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
39
88
|
/**
|
|
40
89
|
* Compress a DGMO DSL string into a shareable URL.
|
|
41
90
|
* Returns `{ url }` on success, or `{ error: 'too-large', compressedSize, limit }` if the
|
|
@@ -59,28 +108,21 @@ export function encodeDiagramUrl(
|
|
|
59
108
|
|
|
60
109
|
let hash = `dgmo=${compressed}`;
|
|
61
110
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (options?.viewState?.swimlaneTagGroup) {
|
|
71
|
-
hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (options?.viewState?.collapsedLanes?.length) {
|
|
75
|
-
hash += `&cl=${encodeURIComponent(options.viewState.collapsedLanes.join(','))}`;
|
|
111
|
+
// View state as single compressed blob (ADR-1)
|
|
112
|
+
if (options?.viewState) {
|
|
113
|
+
const vsEncoded = encodeViewState(options.viewState);
|
|
114
|
+
if (vsEncoded) {
|
|
115
|
+
hash += `&vs=${vsEncoded}`;
|
|
116
|
+
}
|
|
76
117
|
}
|
|
77
118
|
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
// Palette and theme are app-level, kept as separate params
|
|
120
|
+
if (options?.palette && options.palette !== 'nord') {
|
|
121
|
+
hash += `&pal=${encodeURIComponent(options.palette)}`;
|
|
80
122
|
}
|
|
81
123
|
|
|
82
|
-
if (options?.
|
|
83
|
-
hash += `&th=${encodeURIComponent(options.
|
|
124
|
+
if (options?.theme && options.theme !== 'dark') {
|
|
125
|
+
hash += `&th=${encodeURIComponent(options.theme)}`;
|
|
84
126
|
}
|
|
85
127
|
|
|
86
128
|
if (options?.filename) {
|
|
@@ -95,8 +137,8 @@ export function encodeDiagramUrl(
|
|
|
95
137
|
/**
|
|
96
138
|
* Decode a DGMO DSL string and view state from a URL query string or hash.
|
|
97
139
|
* Accepts any of:
|
|
98
|
-
* - `?dgmo=<payload>&
|
|
99
|
-
* - `#dgmo=<payload>&
|
|
140
|
+
* - `?dgmo=<payload>&vs=<state>`
|
|
141
|
+
* - `#dgmo=<payload>&vs=<state>` (backwards compat)
|
|
100
142
|
* - `dgmo=<payload>`
|
|
101
143
|
* - `<bare payload>`
|
|
102
144
|
*
|
|
@@ -105,6 +147,8 @@ export function encodeDiagramUrl(
|
|
|
105
147
|
export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
106
148
|
const empty: DecodedDiagramUrl = { dsl: '', viewState: {} };
|
|
107
149
|
let filename: string | undefined;
|
|
150
|
+
let palette: string | undefined;
|
|
151
|
+
let theme: 'light' | 'dark' | undefined;
|
|
108
152
|
if (!hash) return empty;
|
|
109
153
|
|
|
110
154
|
let raw = hash;
|
|
@@ -120,29 +164,22 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
120
164
|
const parts = raw.split('&');
|
|
121
165
|
let payload = parts[0];
|
|
122
166
|
|
|
123
|
-
// Parse extra params
|
|
124
|
-
|
|
167
|
+
// Parse extra params
|
|
168
|
+
let viewState: CompactViewState = {};
|
|
125
169
|
for (let i = 1; i < parts.length; i++) {
|
|
126
170
|
const eq = parts[i].indexOf('=');
|
|
127
171
|
if (eq === -1) continue;
|
|
128
172
|
const key = parts[i].slice(0, eq);
|
|
129
|
-
const val =
|
|
130
|
-
if (key === '
|
|
131
|
-
viewState
|
|
132
|
-
}
|
|
133
|
-
if (key === 'cg' && val) {
|
|
134
|
-
viewState.collapsedGroups = val.split(',').filter(Boolean);
|
|
135
|
-
}
|
|
136
|
-
if (key === 'swim' && val) {
|
|
137
|
-
viewState.swimlaneTagGroup = val;
|
|
173
|
+
const val = parts[i].slice(eq + 1);
|
|
174
|
+
if (key === 'vs' && val) {
|
|
175
|
+
viewState = decodeViewState(val);
|
|
138
176
|
}
|
|
139
|
-
if (key === '
|
|
140
|
-
|
|
177
|
+
if (key === 'pal' && val) palette = decodeURIComponent(val);
|
|
178
|
+
if (key === 'th') {
|
|
179
|
+
const decoded = decodeURIComponent(val);
|
|
180
|
+
if (decoded === 'light' || decoded === 'dark') theme = decoded;
|
|
141
181
|
}
|
|
142
|
-
if (key === '
|
|
143
|
-
if (key === 'th' && (val === 'light' || val === 'dark'))
|
|
144
|
-
viewState.theme = val;
|
|
145
|
-
if (key === 'fn' && val) filename = val;
|
|
182
|
+
if (key === 'fn' && val) filename = decodeURIComponent(val);
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
// Strip 'dgmo=' prefix
|
|
@@ -150,12 +187,12 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
150
187
|
payload = payload.slice(5);
|
|
151
188
|
}
|
|
152
189
|
|
|
153
|
-
if (!payload) return { dsl: '', viewState, filename };
|
|
190
|
+
if (!payload) return { dsl: '', viewState, palette, theme, filename };
|
|
154
191
|
|
|
155
192
|
try {
|
|
156
193
|
const result = decompressFromEncodedURIComponent(payload);
|
|
157
|
-
return { dsl: result ?? '', viewState, filename };
|
|
194
|
+
return { dsl: result ?? '', viewState, palette, theme, filename };
|
|
158
195
|
} catch {
|
|
159
|
-
return { dsl: '', viewState, filename };
|
|
196
|
+
return { dsl: '', viewState, palette, theme, filename };
|
|
160
197
|
}
|
|
161
198
|
}
|
package/src/utils/duration.ts
CHANGED
|
@@ -120,7 +120,8 @@ export function addGanttDuration(
|
|
|
120
120
|
duration: Duration,
|
|
121
121
|
holidays: GanttHolidays,
|
|
122
122
|
holidaySet: Set<string>,
|
|
123
|
-
direction: 1 | -1 = 1
|
|
123
|
+
direction: 1 | -1 = 1,
|
|
124
|
+
opts?: { sprintLength?: Duration }
|
|
124
125
|
): Date {
|
|
125
126
|
const { amount, unit } = duration;
|
|
126
127
|
|
|
@@ -197,6 +198,19 @@ export function addGanttDuration(
|
|
|
197
198
|
result.setTime(result.getTime() + amount * 60000 * direction);
|
|
198
199
|
return result;
|
|
199
200
|
}
|
|
201
|
+
|
|
202
|
+
case 's': {
|
|
203
|
+
if (!opts?.sprintLength) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
'Sprint duration unit "s" requires sprintLength configuration'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const sl = opts.sprintLength;
|
|
209
|
+
const totalDays = amount * sl.amount * (sl.unit === 'w' ? 7 : 1);
|
|
210
|
+
const result = new Date(startDate);
|
|
211
|
+
result.setDate(result.getDate() + Math.round(totalDays) * direction);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
216
|
|
|
@@ -204,7 +218,7 @@ export function addGanttDuration(
|
|
|
204
218
|
* Parse a duration string like "3bd" or "5d".
|
|
205
219
|
*/
|
|
206
220
|
export function parseDuration(s: string): Duration | null {
|
|
207
|
-
const match = s.trim().match(/^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h)$/);
|
|
221
|
+
const match = s.trim().match(/^(\d+(?:\.\d+)?)(min|bd|d|w|m|q|y|h|s)$/);
|
|
208
222
|
if (!match) return null;
|
|
209
223
|
return { amount: parseFloat(match[1]), unit: match[2] as DurationUnit };
|
|
210
224
|
}
|
|
@@ -54,3 +54,14 @@ export const EYE_OPEN_PATH =
|
|
|
54
54
|
'M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z';
|
|
55
55
|
export const EYE_CLOSED_PATH =
|
|
56
56
|
'M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1';
|
|
57
|
+
|
|
58
|
+
// ── Controls group constants ────────────────────────────────
|
|
59
|
+
// Gear/cog icon (14×14 viewBox) — 6 flat teeth with center hole
|
|
60
|
+
// Computed from polar coordinates: outerR=5.5, innerR=3.5, holeR=2, center=(7,7)
|
|
61
|
+
// Uses evenodd fill-rule for the center hole
|
|
62
|
+
export const CONTROLS_ICON_PATH =
|
|
63
|
+
'M5.6 1.7L8.4 1.7L7.9 3.6L9.5 4.5L10.9 3.1L12.3 5.6L10.4 6.1L10.4 7.9L12.3 8.4L10.9 10.9L9.5 9.5L7.9 10.4L8.4 12.3L5.6 12.3L6.1 10.4L4.5 9.5L3.1 10.9L1.7 8.4L3.6 7.9L3.6 6.1L1.7 5.6L3.1 3.1L4.5 4.5L6.1 3.6Z' +
|
|
64
|
+
'M5 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z';
|
|
65
|
+
export const LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
|
|
66
|
+
export const LEGEND_TOGGLE_OFF_OPACITY = 0.4;
|
|
67
|
+
export const LEGEND_GEAR_PILL_W = 14 + LEGEND_PILL_PAD; // gear icon (14) + padding
|
package/src/utils/legend-d3.ts
CHANGED
|
@@ -9,6 +9,9 @@ import {
|
|
|
9
9
|
LEGEND_DOT_R,
|
|
10
10
|
LEGEND_ENTRY_FONT_SIZE,
|
|
11
11
|
LEGEND_ENTRY_DOT_GAP,
|
|
12
|
+
LEGEND_TOGGLE_DOT_R,
|
|
13
|
+
LEGEND_TOGGLE_OFF_OPACITY,
|
|
14
|
+
CONTROLS_ICON_PATH,
|
|
12
15
|
measureLegendText,
|
|
13
16
|
} from './legend-constants';
|
|
14
17
|
import { computeLegendLayout } from './legend-layout';
|
|
@@ -24,6 +27,7 @@ import type {
|
|
|
24
27
|
LegendPillLayout,
|
|
25
28
|
LegendCapsuleLayout,
|
|
26
29
|
LegendControlLayout,
|
|
30
|
+
ControlsGroupLayout,
|
|
27
31
|
D3Sel,
|
|
28
32
|
} from './legend-types';
|
|
29
33
|
|
|
@@ -83,6 +87,19 @@ export function renderLegendD3(
|
|
|
83
87
|
renderPill(legendG, pill, palette, groupBg, callbacks);
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// Render controls group (gear pill / capsule)
|
|
91
|
+
if (currentLayout.controlsGroup) {
|
|
92
|
+
renderControlsGroup(
|
|
93
|
+
legendG,
|
|
94
|
+
currentLayout.controlsGroup,
|
|
95
|
+
palette,
|
|
96
|
+
groupBg,
|
|
97
|
+
pillBorder,
|
|
98
|
+
callbacks,
|
|
99
|
+
config
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
86
103
|
// Render controls
|
|
87
104
|
for (const ctrl of currentLayout.controls) {
|
|
88
105
|
renderControl(
|
|
@@ -398,3 +415,157 @@ function renderControl(
|
|
|
398
415
|
g.on('click', () => onClick());
|
|
399
416
|
}
|
|
400
417
|
}
|
|
418
|
+
|
|
419
|
+
// ── Controls group (gear pill / capsule) ───────────────────
|
|
420
|
+
|
|
421
|
+
function renderControlsGroup(
|
|
422
|
+
parent: D3Sel,
|
|
423
|
+
layout: ControlsGroupLayout,
|
|
424
|
+
palette: LegendPalette,
|
|
425
|
+
groupBg: string,
|
|
426
|
+
pillBorder: string,
|
|
427
|
+
callbacks?: LegendCallbacks,
|
|
428
|
+
config?: LegendConfig
|
|
429
|
+
): void {
|
|
430
|
+
const g = parent
|
|
431
|
+
.append('g')
|
|
432
|
+
.attr('transform', `translate(${layout.x},${layout.y})`)
|
|
433
|
+
.attr('data-legend-controls', layout.expanded ? 'expanded' : 'collapsed')
|
|
434
|
+
.attr('data-export-ignore', 'true')
|
|
435
|
+
.style('cursor', 'pointer');
|
|
436
|
+
|
|
437
|
+
if (!layout.expanded) {
|
|
438
|
+
// Collapsed: gear pill
|
|
439
|
+
g.append('rect')
|
|
440
|
+
.attr('width', layout.width)
|
|
441
|
+
.attr('height', layout.height)
|
|
442
|
+
.attr('rx', layout.height / 2)
|
|
443
|
+
.attr('fill', groupBg);
|
|
444
|
+
|
|
445
|
+
// Gear icon centered
|
|
446
|
+
const iconSize = 14;
|
|
447
|
+
const iconX = (layout.width - iconSize) / 2;
|
|
448
|
+
const iconY = (layout.height - iconSize) / 2;
|
|
449
|
+
g.append('path')
|
|
450
|
+
.attr('d', CONTROLS_ICON_PATH)
|
|
451
|
+
.attr('transform', `translate(${iconX},${iconY})`)
|
|
452
|
+
.attr('fill', palette.textMuted)
|
|
453
|
+
.attr('fill-rule', 'evenodd')
|
|
454
|
+
.attr('pointer-events', 'none');
|
|
455
|
+
|
|
456
|
+
if (callbacks?.onControlsExpand) {
|
|
457
|
+
const cb = callbacks.onControlsExpand;
|
|
458
|
+
g.on('click', () => cb());
|
|
459
|
+
}
|
|
460
|
+
} else {
|
|
461
|
+
// Expanded: capsule with gear pill + toggle entries
|
|
462
|
+
const pill = layout.pill;
|
|
463
|
+
|
|
464
|
+
// Outer capsule background
|
|
465
|
+
g.append('rect')
|
|
466
|
+
.attr('width', layout.width)
|
|
467
|
+
.attr('height', layout.height)
|
|
468
|
+
.attr('rx', LEGEND_HEIGHT / 2)
|
|
469
|
+
.attr('fill', groupBg);
|
|
470
|
+
|
|
471
|
+
// Inner gear pill
|
|
472
|
+
const pillG = g
|
|
473
|
+
.append('g')
|
|
474
|
+
.attr('class', 'controls-gear-pill')
|
|
475
|
+
.style('cursor', 'pointer');
|
|
476
|
+
|
|
477
|
+
pillG
|
|
478
|
+
.append('rect')
|
|
479
|
+
.attr('x', pill.x)
|
|
480
|
+
.attr('y', pill.y)
|
|
481
|
+
.attr('width', pill.width)
|
|
482
|
+
.attr('height', pill.height)
|
|
483
|
+
.attr('rx', pill.height / 2)
|
|
484
|
+
.attr('fill', palette.bg);
|
|
485
|
+
|
|
486
|
+
pillG
|
|
487
|
+
.append('rect')
|
|
488
|
+
.attr('x', pill.x)
|
|
489
|
+
.attr('y', pill.y)
|
|
490
|
+
.attr('width', pill.width)
|
|
491
|
+
.attr('height', pill.height)
|
|
492
|
+
.attr('rx', pill.height / 2)
|
|
493
|
+
.attr('fill', 'none')
|
|
494
|
+
.attr('stroke', pillBorder)
|
|
495
|
+
.attr('stroke-width', 0.75);
|
|
496
|
+
|
|
497
|
+
// Gear icon inside pill
|
|
498
|
+
const iconSize = 14;
|
|
499
|
+
const iconX = pill.x + (pill.width - iconSize) / 2;
|
|
500
|
+
const iconY = pill.y + (pill.height - iconSize) / 2;
|
|
501
|
+
pillG
|
|
502
|
+
.append('path')
|
|
503
|
+
.attr('d', CONTROLS_ICON_PATH)
|
|
504
|
+
.attr('transform', `translate(${iconX},${iconY})`)
|
|
505
|
+
.attr('fill', palette.text)
|
|
506
|
+
.attr('fill-rule', 'evenodd')
|
|
507
|
+
.attr('pointer-events', 'none');
|
|
508
|
+
|
|
509
|
+
// Click on gear pill collapses
|
|
510
|
+
if (callbacks?.onControlsExpand) {
|
|
511
|
+
const cb = callbacks.onControlsExpand;
|
|
512
|
+
pillG.on('click', (event: Event) => {
|
|
513
|
+
event.stopPropagation();
|
|
514
|
+
cb();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Toggle entries
|
|
519
|
+
const toggles = config?.controlsGroup?.toggles ?? [];
|
|
520
|
+
for (const tl of layout.toggles) {
|
|
521
|
+
const toggle = toggles.find((t) => t.id === tl.id);
|
|
522
|
+
const entryG = g
|
|
523
|
+
.append('g')
|
|
524
|
+
.attr('data-controls-toggle', tl.id)
|
|
525
|
+
.style('cursor', 'pointer');
|
|
526
|
+
|
|
527
|
+
if (tl.active) {
|
|
528
|
+
// Filled dot
|
|
529
|
+
entryG
|
|
530
|
+
.append('circle')
|
|
531
|
+
.attr('cx', tl.dotCx)
|
|
532
|
+
.attr('cy', tl.dotCy)
|
|
533
|
+
.attr('r', LEGEND_TOGGLE_DOT_R)
|
|
534
|
+
.attr('fill', palette.primary ?? palette.text);
|
|
535
|
+
} else {
|
|
536
|
+
// Hollow dot
|
|
537
|
+
entryG
|
|
538
|
+
.append('circle')
|
|
539
|
+
.attr('cx', tl.dotCx)
|
|
540
|
+
.attr('cy', tl.dotCy)
|
|
541
|
+
.attr('r', LEGEND_TOGGLE_DOT_R)
|
|
542
|
+
.attr('fill', 'none')
|
|
543
|
+
.attr('stroke', palette.textMuted)
|
|
544
|
+
.attr('stroke-width', 1);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Label
|
|
548
|
+
entryG
|
|
549
|
+
.append('text')
|
|
550
|
+
.attr('x', tl.textX)
|
|
551
|
+
.attr('y', tl.textY)
|
|
552
|
+
.attr('dominant-baseline', 'central')
|
|
553
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
554
|
+
.attr('fill', palette.textMuted)
|
|
555
|
+
.attr('opacity', tl.active ? 1 : LEGEND_TOGGLE_OFF_OPACITY)
|
|
556
|
+
.attr('font-family', FONT_FAMILY)
|
|
557
|
+
.text(tl.label);
|
|
558
|
+
|
|
559
|
+
// Click on toggle entry
|
|
560
|
+
if (callbacks?.onControlsToggle && toggle) {
|
|
561
|
+
const cb = callbacks.onControlsToggle;
|
|
562
|
+
const id = tl.id;
|
|
563
|
+
const newActive = !tl.active;
|
|
564
|
+
entryG.on('click', (event: Event) => {
|
|
565
|
+
event.stopPropagation();
|
|
566
|
+
cb(id, newActive);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|