@diagrammo/dgmo 0.8.18 → 0.8.20
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 +89 -130
- package/dist/index.cjs +1202 -993
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +216 -114
- package/dist/index.d.ts +216 -114
- package/dist/index.js +1211 -985
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +73 -0
- package/package.json +22 -9
- package/src/boxes-and-lines/parser.ts +8 -3
- package/src/c4/parser.ts +8 -7
- package/src/class/parser.ts +6 -0
- package/src/cli.ts +1 -9
- package/src/d3.ts +16 -234
- package/src/dgmo-router.ts +97 -5
- package/src/diagnostics.ts +16 -6
- package/src/echarts.ts +43 -10
- package/src/er/parser.ts +22 -2
- package/src/gantt/renderer.ts +153 -91
- package/src/graph/flowchart-parser.ts +89 -52
- package/src/graph/state-parser.ts +60 -35
- package/src/index.ts +23 -18
- package/src/infra/parser.ts +9 -2
- package/src/kanban/renderer.ts +2 -2
- package/src/palettes/color-utils.ts +4 -12
- package/src/palettes/index.ts +0 -4
- package/src/render.ts +30 -16
- package/src/sequence/collapse.ts +169 -0
- package/src/sequence/parser.ts +21 -4
- package/src/sequence/renderer.ts +198 -52
- package/src/sharing.ts +86 -49
- package/src/sitemap/renderer.ts +1 -6
- package/src/utils/arrows.ts +180 -11
- package/src/utils/d3-types.ts +4 -0
- package/src/utils/legend-constants.ts +11 -4
- package/src/utils/legend-d3.ts +171 -0
- package/src/utils/legend-layout.ts +140 -13
- package/src/utils/legend-types.ts +45 -0
- package/src/utils/time-ticks.ts +213 -0
- package/src/branding.ts +0 -67
- package/src/dgmo-mermaid.ts +0 -262
- package/src/palettes/mermaid-bridge.ts +0 -220
package/src/utils/arrows.ts
CHANGED
|
@@ -5,6 +5,24 @@
|
|
|
5
5
|
// Labeled arrow syntax (always left-to-right):
|
|
6
6
|
// Sync: `-label->`
|
|
7
7
|
// Async: `~label~>`
|
|
8
|
+
//
|
|
9
|
+
// In-arrow label character-set contract (see docs/dgmo-language-spec.md
|
|
10
|
+
// §"In-Arrow Message Labels"):
|
|
11
|
+
// - Allowed: any codepoint except the forbidden substrings and forbidden
|
|
12
|
+
// control characters below.
|
|
13
|
+
// - Forbidden substrings: "->", "~>" (arrow-token lookalikes inside labels).
|
|
14
|
+
// Use the post-colon form for labels that need these symbols:
|
|
15
|
+
// `A -> B: uses -> to chain`
|
|
16
|
+
// - Forbidden characters: C0 control chars U+0000–U+001F EXCEPT U+0009 (tab),
|
|
17
|
+
// and U+007F (DEL).
|
|
18
|
+
// - Whitespace: leading/trailing trimmed; internal runs (incl. tab, NBSP,
|
|
19
|
+
// ZWSP) preserved — never collapsed.
|
|
20
|
+
// - Plain text only: no markdown interpretation. `*`, `_`, backticks,
|
|
21
|
+
// `[`, `]`, `{`, `}` are literal characters.
|
|
22
|
+
|
|
23
|
+
import type { DgmoError } from '../diagnostics';
|
|
24
|
+
import { makeDgmoError } from '../diagnostics';
|
|
25
|
+
import { RECOGNIZED_COLOR_NAMES } from '../colors';
|
|
8
26
|
|
|
9
27
|
interface ParsedArrow {
|
|
10
28
|
from: string;
|
|
@@ -13,6 +31,160 @@ interface ParsedArrow {
|
|
|
13
31
|
async: boolean;
|
|
14
32
|
}
|
|
15
33
|
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Diagnostic codes (TD-16)
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stable diagnostic codes for in-arrow label parsing errors.
|
|
40
|
+
*
|
|
41
|
+
* **Active codes** — emitted by the parser pipeline today:
|
|
42
|
+
* - `ARROW_SUBSTRING_IN_LABEL` (TD-13)
|
|
43
|
+
* - `CONTROL_CHAR_IN_LABEL` (TD-14)
|
|
44
|
+
*
|
|
45
|
+
* **Reserved codes** — declared but NOT currently emitted. These are
|
|
46
|
+
* placeholders for future tightening of the arrow-tokenization rules
|
|
47
|
+
* described in TD-9. Today's chart parsers catch these cases through
|
|
48
|
+
* their own regex machinery with different diagnostics. A follow-up
|
|
49
|
+
* spec that introduces a dedicated tokenizer can start emitting them
|
|
50
|
+
* without changing the public code shape:
|
|
51
|
+
* - `TRAILING_ARROW_TEXT` — extra `->`/`~>` after the primary arrow
|
|
52
|
+
* - `MIXED_ARROW_DELIMITERS` — opening delim type doesn't match arrow
|
|
53
|
+
*
|
|
54
|
+
* See `docs/dgmo-language-spec-decisions.md` → TD-16 for the rationale.
|
|
55
|
+
*/
|
|
56
|
+
export const ARROW_DIAGNOSTIC_CODES = {
|
|
57
|
+
/** Active: label contains `->` or `~>` substring (TD-13). */
|
|
58
|
+
ARROW_SUBSTRING_IN_LABEL: 'E_ARROW_SUBSTRING_IN_LABEL',
|
|
59
|
+
/** Active: label contains a forbidden control character (TD-14). */
|
|
60
|
+
CONTROL_CHAR_IN_LABEL: 'E_CONTROL_CHAR_IN_LABEL',
|
|
61
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
62
|
+
TRAILING_ARROW_TEXT: 'E_TRAILING_ARROW_TEXT',
|
|
63
|
+
/** Reserved: not currently emitted by any parser. See JSDoc above. */
|
|
64
|
+
MIXED_ARROW_DELIMITERS: 'E_MIXED_ARROW_DELIMITERS',
|
|
65
|
+
} as const;
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// validateLabelCharacters (TD-13, TD-14)
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate an in-arrow label against the TD-13 and TD-14 character-set
|
|
73
|
+
* contract. Returns diagnostics (possibly empty). Does NOT mutate the label —
|
|
74
|
+
* callers that want a normalized label should trim before calling.
|
|
75
|
+
*
|
|
76
|
+
* TD-13: label must not contain the substrings "->" or "~>".
|
|
77
|
+
* TD-14: label must not contain C0 control chars other than tab, and no DEL.
|
|
78
|
+
*/
|
|
79
|
+
export function validateLabelCharacters(
|
|
80
|
+
label: string,
|
|
81
|
+
lineNumber: number
|
|
82
|
+
): DgmoError[] {
|
|
83
|
+
const out: DgmoError[] = [];
|
|
84
|
+
|
|
85
|
+
// TD-13: forbidden substrings
|
|
86
|
+
if (label.includes('->') || label.includes('~>')) {
|
|
87
|
+
out.push(
|
|
88
|
+
makeDgmoError(
|
|
89
|
+
lineNumber,
|
|
90
|
+
'Arrow symbols (-> or ~>) are not allowed inside a label. ' +
|
|
91
|
+
'Move the label after the arrow: "A -> B: uses -> to chain". ' +
|
|
92
|
+
'See "In-Arrow Message Labels" → Forbidden.',
|
|
93
|
+
'error',
|
|
94
|
+
ARROW_DIAGNOSTIC_CODES.ARROW_SUBSTRING_IN_LABEL
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// TD-14: control chars (iterate codepoints to handle surrogate pairs)
|
|
100
|
+
for (const ch of label) {
|
|
101
|
+
const cp = ch.codePointAt(0)!;
|
|
102
|
+
const isC0 = cp >= 0x00 && cp <= 0x1f && cp !== 0x09; // allow tab
|
|
103
|
+
const isDel = cp === 0x7f;
|
|
104
|
+
if (isC0 || isDel) {
|
|
105
|
+
const hex = cp.toString(16).toUpperCase().padStart(4, '0');
|
|
106
|
+
out.push(
|
|
107
|
+
makeDgmoError(
|
|
108
|
+
lineNumber,
|
|
109
|
+
`Label contains a control character (U+${hex}). ` +
|
|
110
|
+
'Remove it and use plain text.',
|
|
111
|
+
'error',
|
|
112
|
+
ARROW_DIAGNOSTIC_CODES.CONTROL_CHAR_IN_LABEL
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
break; // one diagnostic per label is enough
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================
|
|
123
|
+
// parseInArrowLabel (TD-1, TD-8, TD-10, TD-13, TD-14)
|
|
124
|
+
// ============================================================
|
|
125
|
+
|
|
126
|
+
export interface ParseInArrowLabelResult {
|
|
127
|
+
/** Cleaned label (trimmed; `undefined` if empty after trim per TD-10). */
|
|
128
|
+
label: string | undefined;
|
|
129
|
+
diagnostics: DgmoError[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Normalize and validate a raw in-arrow label.
|
|
134
|
+
*
|
|
135
|
+
* Behavior:
|
|
136
|
+
* - Trims leading/trailing whitespace (TD-8: internal whitespace preserved).
|
|
137
|
+
* - Empty-after-trim → `{ label: undefined }` (TD-10 normalization).
|
|
138
|
+
* - TD-13: emits `E_ARROW_SUBSTRING_IN_LABEL` if `->` or `~>` is present.
|
|
139
|
+
* - TD-14: emits `E_CONTROL_CHAR_IN_LABEL` for forbidden control chars.
|
|
140
|
+
*
|
|
141
|
+
* This helper is intentionally chart-agnostic: it operates on an already
|
|
142
|
+
* extracted label string, leaving each chart's existing arrow-finding
|
|
143
|
+
* tokenization in place. TD-11 color-parens is handled inside the
|
|
144
|
+
* flowchart and state `parseArrowToken` functions because those are the
|
|
145
|
+
* only charts that interpret `-(color)->` as a colored edge; they use
|
|
146
|
+
* `matchColorParens()` from this module for the shared lookup.
|
|
147
|
+
*/
|
|
148
|
+
export function parseInArrowLabel(
|
|
149
|
+
rawLabel: string,
|
|
150
|
+
lineNumber: number
|
|
151
|
+
): ParseInArrowLabelResult {
|
|
152
|
+
const trimmed = rawLabel.trim();
|
|
153
|
+
|
|
154
|
+
// TD-10: empty/whitespace-only label normalizes to undefined
|
|
155
|
+
if (trimmed.length === 0) {
|
|
156
|
+
return { label: undefined, diagnostics: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// TD-13 / TD-14 validation
|
|
160
|
+
const diagnostics = validateLabelCharacters(trimmed, lineNumber);
|
|
161
|
+
|
|
162
|
+
return { label: trimmed, diagnostics };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================
|
|
166
|
+
// matchColorParens — shared TD-11 helper for flowchart and state
|
|
167
|
+
// ============================================================
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Test whether a string matches the TD-11 color-parens form `(colorName)`
|
|
171
|
+
* where `colorName` is one of the 11 recognized palette color names from
|
|
172
|
+
* `src/colors.ts:RECOGNIZED_COLOR_NAMES`. Returns the lowercase color name
|
|
173
|
+
* on a match, or `null` on fall-through (whole string becomes a label).
|
|
174
|
+
*
|
|
175
|
+
* Used by flowchart and state parsers to keep the color-parens recognition
|
|
176
|
+
* rule in one place — do NOT re-implement the regex in chart parsers.
|
|
177
|
+
*/
|
|
178
|
+
export function matchColorParens(content: string): string | null {
|
|
179
|
+
const m = content.match(/^\(([A-Za-z]+)\)$/);
|
|
180
|
+
if (!m) return null;
|
|
181
|
+
const candidate = m[1].toLowerCase();
|
|
182
|
+
if ((RECOGNIZED_COLOR_NAMES as readonly string[]).includes(candidate)) {
|
|
183
|
+
return candidate;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
16
188
|
// Forward (call) patterns — participant names may contain spaces, so use non-greedy (.+?)
|
|
17
189
|
const SYNC_LABELED_RE = /^(.+?)\s*-(.+)->\s*(.+)$/;
|
|
18
190
|
const ASYNC_LABELED_RE = /^(.+?)\s*~(.+)~>\s*(.+)$/;
|
|
@@ -23,8 +195,6 @@ const RETURN_ASYNC_LABELED_RE = /^(.+?)\s*<~(.+)~\s*(.+)$/;
|
|
|
23
195
|
const BIDI_SYNC_RE = /^(.+?)\s*<-(.+)->\s*(.+)$/;
|
|
24
196
|
const BIDI_ASYNC_RE = /^(.+?)\s*<~(.+)~>\s*(.+)$/;
|
|
25
197
|
|
|
26
|
-
const ARROW_CHARS = ['->', '~>'];
|
|
27
|
-
|
|
28
198
|
/**
|
|
29
199
|
* Try to parse a labeled arrow from a trimmed line.
|
|
30
200
|
*
|
|
@@ -32,6 +202,14 @@ const ARROW_CHARS = ['->', '~>'];
|
|
|
32
202
|
* - `ParsedArrow` if matched and valid
|
|
33
203
|
* - `{ error: string }` if matched but invalid (deprecated syntax)
|
|
34
204
|
* - `null` if not a labeled arrow (caller should fall through to bare patterns)
|
|
205
|
+
*
|
|
206
|
+
* Note: arrow-char-in-label validation (TD-13) is NOT performed here —
|
|
207
|
+
* callers must route the returned `label` through `parseInArrowLabel` or
|
|
208
|
+
* `validateLabelCharacters` to get the unified `E_ARROW_SUBSTRING_IN_LABEL`
|
|
209
|
+
* diagnostic with the correct code. In practice this path is unreachable
|
|
210
|
+
* because arrow regexes are greedy enough to absorb inner `->`/`~>` tokens
|
|
211
|
+
* into the source/destination captures, but the check remains at the
|
|
212
|
+
* validator level for defense in depth.
|
|
35
213
|
*/
|
|
36
214
|
export function parseArrow(
|
|
37
215
|
line: string
|
|
@@ -73,15 +251,6 @@ export function parseArrow(
|
|
|
73
251
|
// Empty label (e.g. `--> B`) — fall through to plain arrow handling
|
|
74
252
|
if (!label) return null;
|
|
75
253
|
|
|
76
|
-
// Validate: no arrow chars inside label
|
|
77
|
-
for (const arrow of ARROW_CHARS) {
|
|
78
|
-
if (label.includes(arrow)) {
|
|
79
|
-
return {
|
|
80
|
-
error: 'Arrow characters (->, ~>) are not allowed inside labels',
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
254
|
return {
|
|
86
255
|
from: m[1],
|
|
87
256
|
to: m[3],
|
|
@@ -16,10 +16,6 @@ export const LEGEND_EYE_SIZE = 14;
|
|
|
16
16
|
export const LEGEND_EYE_GAP = 6;
|
|
17
17
|
export const LEGEND_ICON_W = 20;
|
|
18
18
|
|
|
19
|
-
// ── Spacing constants (centralized legend system) ───────────
|
|
20
|
-
export const LEGEND_TOP_PAD = 12;
|
|
21
|
-
export const LEGEND_TITLE_GAP = 8;
|
|
22
|
-
export const LEGEND_CONTENT_GAP = 12;
|
|
23
19
|
export const LEGEND_MAX_ENTRY_ROWS = 3;
|
|
24
20
|
|
|
25
21
|
// ── Proportional text measurement ────────────────────────────
|
|
@@ -54,3 +50,14 @@ export const EYE_OPEN_PATH =
|
|
|
54
50
|
'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
51
|
export const EYE_CLOSED_PATH =
|
|
56
52
|
'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';
|
|
53
|
+
|
|
54
|
+
// ── Controls group constants ────────────────────────────────
|
|
55
|
+
// Gear/cog icon (14×14 viewBox) — 6 flat teeth with center hole
|
|
56
|
+
// Computed from polar coordinates: outerR=5.5, innerR=3.5, holeR=2, center=(7,7)
|
|
57
|
+
// Uses evenodd fill-rule for the center hole
|
|
58
|
+
export const CONTROLS_ICON_PATH =
|
|
59
|
+
'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' +
|
|
60
|
+
'M5 7a2 2 0 1 0 4 0a2 2 0 1 0-4 0Z';
|
|
61
|
+
export const LEGEND_TOGGLE_DOT_R = LEGEND_DOT_R;
|
|
62
|
+
export const LEGEND_TOGGLE_OFF_OPACITY = 0.4;
|
|
63
|
+
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
|
+
}
|