@diagrammo/dgmo 0.8.9 → 0.8.10
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 +191 -191
- package/dist/index.cjs +1318 -724
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -1
- package/dist/index.d.ts +147 -1
- package/dist/index.js +1314 -724
- 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 +1 -1
- 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 +33 -137
- package/src/c4/renderer.ts +25 -138
- package/src/class/renderer.ts +185 -186
- package/src/d3.ts +114 -191
- package/src/er/renderer.ts +52 -245
- package/src/gantt/renderer.ts +140 -182
- package/src/index.ts +21 -1
- package/src/infra/renderer.ts +91 -244
- package/src/kanban/renderer.ts +22 -129
- package/src/org/renderer.ts +103 -170
- package/src/render.ts +39 -9
- package/src/sequence/renderer.ts +31 -151
- package/src/sitemap/layout.ts +180 -38
- package/src/sitemap/parser.ts +64 -23
- package/src/sitemap/renderer.ts +73 -161
- package/src/utils/legend-constants.ts +6 -0
- package/src/utils/legend-d3.ts +400 -0
- package/src/utils/legend-layout.ts +495 -0
- package/src/utils/legend-svg.ts +26 -0
- package/src/utils/legend-types.ts +169 -0
package/src/infra/renderer.ts
CHANGED
|
@@ -33,9 +33,10 @@ import {
|
|
|
33
33
|
LEGEND_ENTRY_FONT_SIZE,
|
|
34
34
|
LEGEND_ENTRY_DOT_GAP,
|
|
35
35
|
LEGEND_ENTRY_TRAIL,
|
|
36
|
-
LEGEND_GROUP_GAP,
|
|
37
36
|
measureLegendText,
|
|
38
37
|
} from '../utils/legend-constants';
|
|
38
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
39
|
+
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
39
40
|
import {
|
|
40
41
|
TITLE_FONT_SIZE,
|
|
41
42
|
TITLE_FONT_WEIGHT,
|
|
@@ -1983,26 +1984,6 @@ export function computeInfraLegendGroups(
|
|
|
1983
1984
|
return groups;
|
|
1984
1985
|
}
|
|
1985
1986
|
|
|
1986
|
-
/** Compute total width for the playback pill (speed only). */
|
|
1987
|
-
function computePlaybackWidth(
|
|
1988
|
-
playback: InfraPlaybackState | undefined
|
|
1989
|
-
): number {
|
|
1990
|
-
if (!playback) return 0;
|
|
1991
|
-
const pillWidth =
|
|
1992
|
-
measureLegendText('Playback', LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
1993
|
-
if (!playback.expanded) return pillWidth;
|
|
1994
|
-
|
|
1995
|
-
let entriesW = 8; // gap after pill
|
|
1996
|
-
entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6; // play/pause
|
|
1997
|
-
for (const s of playback.speedOptions) {
|
|
1998
|
-
entriesW +=
|
|
1999
|
-
measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) +
|
|
2000
|
-
SPEED_BADGE_H_PAD * 2 +
|
|
2001
|
-
SPEED_BADGE_GAP;
|
|
2002
|
-
}
|
|
2003
|
-
return LEGEND_CAPSULE_PAD * 2 + pillWidth + entriesW;
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
1987
|
function renderLegend(
|
|
2007
1988
|
rootSvg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>,
|
|
2008
1989
|
legendGroups: InfraLegendGroup[],
|
|
@@ -2023,254 +2004,120 @@ function renderLegend(
|
|
|
2023
2004
|
legendG.attr('data-legend-active', activeGroup.toLowerCase());
|
|
2024
2005
|
}
|
|
2025
2006
|
|
|
2026
|
-
//
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
legendGroups.reduce((s, g) => s + effectiveW(g), 0) +
|
|
2036
|
-
(legendGroups.length - 1) * LEGEND_GROUP_GAP +
|
|
2037
|
-
trailingGaps +
|
|
2038
|
-
playbackW;
|
|
2039
|
-
let cursorX = (totalWidth - totalLegendW) / 2;
|
|
2040
|
-
|
|
2041
|
-
for (const group of legendGroups) {
|
|
2042
|
-
const isActive =
|
|
2043
|
-
activeGroup != null &&
|
|
2044
|
-
group.name.toLowerCase() === activeGroup.toLowerCase();
|
|
2045
|
-
|
|
2046
|
-
const groupBg = isDark
|
|
2047
|
-
? mix(palette.surface, palette.bg, 50)
|
|
2048
|
-
: mix(palette.surface, palette.bg, 30);
|
|
2049
|
-
|
|
2050
|
-
const pillLabel = group.name;
|
|
2051
|
-
const pillWidth =
|
|
2052
|
-
measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
2053
|
-
|
|
2054
|
-
const gEl = legendG
|
|
2055
|
-
.append('g')
|
|
2056
|
-
.attr('transform', `translate(${cursorX}, 0)`)
|
|
2057
|
-
.attr('class', 'infra-legend-group')
|
|
2058
|
-
.attr('data-legend-group', group.name.toLowerCase())
|
|
2059
|
-
.style('cursor', 'pointer');
|
|
2060
|
-
|
|
2061
|
-
// Outer capsule background (active only)
|
|
2062
|
-
if (isActive) {
|
|
2063
|
-
gEl
|
|
2064
|
-
.append('rect')
|
|
2065
|
-
.attr('width', group.width)
|
|
2066
|
-
.attr('height', LEGEND_HEIGHT)
|
|
2067
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
2068
|
-
.attr('fill', groupBg);
|
|
2069
|
-
}
|
|
2007
|
+
// Build all groups including Playback as a regular group for consistent rendering
|
|
2008
|
+
const allGroups = legendGroups.map((g) => ({
|
|
2009
|
+
name: g.name,
|
|
2010
|
+
entries: g.entries.map((e) => ({ value: e.value, color: e.color })),
|
|
2011
|
+
}));
|
|
2012
|
+
// Add Playback as a group with empty entries (collapsed pill) or dummy entries (expanded)
|
|
2013
|
+
if (playback) {
|
|
2014
|
+
allGroups.push({ name: 'Playback', entries: [] });
|
|
2015
|
+
}
|
|
2070
2016
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
gEl
|
|
2088
|
-
.append('rect')
|
|
2089
|
-
.attr('x', pillXOff)
|
|
2090
|
-
.attr('y', pillYOff)
|
|
2091
|
-
.attr('width', pillWidth)
|
|
2092
|
-
.attr('height', pillH)
|
|
2093
|
-
.attr('rx', pillH / 2)
|
|
2094
|
-
.attr('fill', 'none')
|
|
2095
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
2096
|
-
.attr('stroke-width', 0.75);
|
|
2097
|
-
}
|
|
2017
|
+
const legendConfig: LegendConfig = {
|
|
2018
|
+
groups: allGroups,
|
|
2019
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
2020
|
+
mode: 'fixed',
|
|
2021
|
+
showEmptyGroups: true,
|
|
2022
|
+
};
|
|
2023
|
+
const legendState: LegendState = { activeGroup };
|
|
2024
|
+
renderLegendD3(
|
|
2025
|
+
legendG,
|
|
2026
|
+
legendConfig,
|
|
2027
|
+
legendState,
|
|
2028
|
+
palette,
|
|
2029
|
+
isDark,
|
|
2030
|
+
undefined,
|
|
2031
|
+
totalWidth
|
|
2032
|
+
);
|
|
2098
2033
|
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
.
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
.
|
|
2108
|
-
|
|
2109
|
-
.text(pillLabel);
|
|
2110
|
-
|
|
2111
|
-
// Entries inside capsule (active only)
|
|
2112
|
-
if (isActive) {
|
|
2113
|
-
let entryX = pillXOff + pillWidth + 4;
|
|
2114
|
-
for (const entry of group.entries) {
|
|
2115
|
-
const entryG = gEl
|
|
2116
|
-
.append('g')
|
|
2117
|
-
.attr('class', 'infra-legend-entry')
|
|
2034
|
+
// Add infra-specific classes and data attributes for app interactivity
|
|
2035
|
+
legendG.selectAll('[data-legend-group]').classed('infra-legend-group', true);
|
|
2036
|
+
for (const group of legendGroups) {
|
|
2037
|
+
const groupKey = group.name.toLowerCase();
|
|
2038
|
+
for (const entry of group.entries) {
|
|
2039
|
+
const entryEl = legendG.select(
|
|
2040
|
+
`[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
|
|
2041
|
+
);
|
|
2042
|
+
if (!entryEl.empty()) {
|
|
2043
|
+
entryEl
|
|
2118
2044
|
.attr('data-legend-entry', entry.key.toLowerCase())
|
|
2119
2045
|
.attr('data-legend-color', entry.color)
|
|
2120
2046
|
.attr('data-legend-type', group.type)
|
|
2121
2047
|
.attr(
|
|
2122
2048
|
'data-legend-tag-group',
|
|
2123
2049
|
group.type === 'tag' ? (group.tagKey ?? '') : null
|
|
2124
|
-
)
|
|
2125
|
-
.style('cursor', 'pointer');
|
|
2126
|
-
|
|
2127
|
-
entryG
|
|
2128
|
-
.append('circle')
|
|
2129
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
2130
|
-
.attr('cy', LEGEND_HEIGHT / 2)
|
|
2131
|
-
.attr('r', LEGEND_DOT_R)
|
|
2132
|
-
.attr('fill', entry.color);
|
|
2133
|
-
|
|
2134
|
-
const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
|
|
2135
|
-
entryG
|
|
2136
|
-
.append('text')
|
|
2137
|
-
.attr('x', textX)
|
|
2138
|
-
.attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1)
|
|
2139
|
-
.attr('font-family', FONT_FAMILY)
|
|
2140
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
2141
|
-
.attr('fill', palette.textMuted)
|
|
2142
|
-
.text(entry.value);
|
|
2143
|
-
|
|
2144
|
-
entryX =
|
|
2145
|
-
textX +
|
|
2146
|
-
measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
|
|
2147
|
-
LEGEND_ENTRY_TRAIL;
|
|
2050
|
+
);
|
|
2148
2051
|
}
|
|
2149
2052
|
}
|
|
2053
|
+
}
|
|
2150
2054
|
|
|
2151
|
-
|
|
2055
|
+
// Mark the Playback pill with infra-playback-pill class for app interactivity
|
|
2056
|
+
const playbackEl = legendG.select('[data-legend-group="playback"]');
|
|
2057
|
+
if (!playbackEl.empty()) {
|
|
2058
|
+
playbackEl.classed('infra-playback-pill', true);
|
|
2152
2059
|
}
|
|
2153
2060
|
|
|
2154
|
-
//
|
|
2155
|
-
|
|
2156
|
-
const isExpanded = playback.expanded;
|
|
2157
|
-
const groupBg = isDark
|
|
2158
|
-
? mix(palette.bg, palette.text, 85)
|
|
2159
|
-
: mix(palette.bg, palette.text, 92);
|
|
2061
|
+
// Inject speed badges into the Playback pill when expanded
|
|
2062
|
+
// Speed badges are injected into the Playback pill below
|
|
2160
2063
|
|
|
2161
|
-
|
|
2064
|
+
// Inject speed badges into the Playback pill when expanded
|
|
2065
|
+
if (playback && playback.expanded && !playbackEl.empty()) {
|
|
2162
2066
|
const pillWidth =
|
|
2163
|
-
measureLegendText(
|
|
2164
|
-
const fullW = computePlaybackWidth(playback);
|
|
2067
|
+
measureLegendText('Playback', LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
|
|
2165
2068
|
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
.attr('transform', `translate(${cursorX}, 0)`)
|
|
2169
|
-
.attr('class', 'infra-legend-group infra-playback-pill')
|
|
2170
|
-
.style('cursor', 'pointer');
|
|
2171
|
-
|
|
2172
|
-
if (isExpanded) {
|
|
2173
|
-
pbG
|
|
2174
|
-
.append('rect')
|
|
2175
|
-
.attr('width', fullW)
|
|
2176
|
-
.attr('height', LEGEND_HEIGHT)
|
|
2177
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
2178
|
-
.attr('fill', groupBg);
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
2182
|
-
const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
|
|
2183
|
-
const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
|
|
2184
|
-
|
|
2185
|
-
pbG
|
|
2186
|
-
.append('rect')
|
|
2187
|
-
.attr('x', pillXOff)
|
|
2188
|
-
.attr('y', pillYOff)
|
|
2189
|
-
.attr('width', pillWidth)
|
|
2190
|
-
.attr('height', pillH)
|
|
2191
|
-
.attr('rx', pillH / 2)
|
|
2192
|
-
.attr('fill', isExpanded ? palette.bg : groupBg);
|
|
2193
|
-
|
|
2194
|
-
if (isExpanded) {
|
|
2195
|
-
pbG
|
|
2196
|
-
.append('rect')
|
|
2197
|
-
.attr('x', pillXOff)
|
|
2198
|
-
.attr('y', pillYOff)
|
|
2199
|
-
.attr('width', pillWidth)
|
|
2200
|
-
.attr('height', pillH)
|
|
2201
|
-
.attr('rx', pillH / 2)
|
|
2202
|
-
.attr('fill', 'none')
|
|
2203
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
2204
|
-
.attr('stroke-width', 0.75);
|
|
2205
|
-
}
|
|
2069
|
+
let entryX = pillWidth + 8;
|
|
2070
|
+
const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
|
|
2206
2071
|
|
|
2207
|
-
|
|
2072
|
+
const ppLabel = playback.paused ? '▶' : '⏸';
|
|
2073
|
+
playbackEl
|
|
2208
2074
|
.append('text')
|
|
2209
|
-
.attr('x',
|
|
2210
|
-
.attr('y',
|
|
2075
|
+
.attr('x', entryX)
|
|
2076
|
+
.attr('y', entryY)
|
|
2211
2077
|
.attr('font-family', FONT_FAMILY)
|
|
2212
2078
|
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
2213
|
-
.attr('
|
|
2214
|
-
.attr('
|
|
2215
|
-
.
|
|
2216
|
-
.text(
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
const
|
|
2079
|
+
.attr('fill', palette.textMuted)
|
|
2080
|
+
.attr('data-playback-action', 'toggle-pause')
|
|
2081
|
+
.style('cursor', 'pointer')
|
|
2082
|
+
.text(ppLabel);
|
|
2083
|
+
entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
|
|
2084
|
+
|
|
2085
|
+
for (const s of playback.speedOptions) {
|
|
2086
|
+
const label = `${s}x`;
|
|
2087
|
+
const isSpeedActive = playback.speed === s;
|
|
2088
|
+
const slotW =
|
|
2089
|
+
measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) +
|
|
2090
|
+
SPEED_BADGE_H_PAD * 2;
|
|
2091
|
+
const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
|
|
2092
|
+
const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
|
|
2093
|
+
|
|
2094
|
+
const speedG = playbackEl
|
|
2095
|
+
.append('g')
|
|
2096
|
+
.attr('data-playback-action', 'set-speed')
|
|
2097
|
+
.attr('data-playback-value', String(s))
|
|
2098
|
+
.style('cursor', 'pointer');
|
|
2099
|
+
|
|
2100
|
+
speedG
|
|
2101
|
+
.append('rect')
|
|
2102
|
+
.attr('x', entryX)
|
|
2103
|
+
.attr('y', badgeY)
|
|
2104
|
+
.attr('width', slotW)
|
|
2105
|
+
.attr('height', badgeH)
|
|
2106
|
+
.attr('rx', badgeH / 2)
|
|
2107
|
+
.attr('fill', isSpeedActive ? palette.primary : 'transparent');
|
|
2221
2108
|
|
|
2222
|
-
|
|
2223
|
-
pbG
|
|
2109
|
+
speedG
|
|
2224
2110
|
.append('text')
|
|
2225
|
-
.attr('x', entryX)
|
|
2111
|
+
.attr('x', entryX + slotW / 2)
|
|
2226
2112
|
.attr('y', entryY)
|
|
2227
2113
|
.attr('font-family', FONT_FAMILY)
|
|
2228
|
-
.attr('font-size',
|
|
2229
|
-
.attr('
|
|
2230
|
-
.attr('
|
|
2231
|
-
.
|
|
2232
|
-
.text(
|
|
2233
|
-
entryX +=
|
|
2234
|
-
|
|
2235
|
-
for (const s of playback.speedOptions) {
|
|
2236
|
-
const label = `${s}x`;
|
|
2237
|
-
const isActive = playback.speed === s;
|
|
2238
|
-
const slotW =
|
|
2239
|
-
measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) +
|
|
2240
|
-
SPEED_BADGE_H_PAD * 2;
|
|
2241
|
-
const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
|
|
2242
|
-
const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
|
|
2243
|
-
|
|
2244
|
-
const speedG = pbG
|
|
2245
|
-
.append('g')
|
|
2246
|
-
.attr('data-playback-action', 'set-speed')
|
|
2247
|
-
.attr('data-playback-value', String(s))
|
|
2248
|
-
.style('cursor', 'pointer');
|
|
2249
|
-
|
|
2250
|
-
speedG
|
|
2251
|
-
.append('rect')
|
|
2252
|
-
.attr('x', entryX)
|
|
2253
|
-
.attr('y', badgeY)
|
|
2254
|
-
.attr('width', slotW)
|
|
2255
|
-
.attr('height', badgeH)
|
|
2256
|
-
.attr('rx', badgeH / 2)
|
|
2257
|
-
.attr('fill', isActive ? palette.primary : 'transparent');
|
|
2258
|
-
|
|
2259
|
-
speedG
|
|
2260
|
-
.append('text')
|
|
2261
|
-
.attr('x', entryX + slotW / 2)
|
|
2262
|
-
.attr('y', entryY)
|
|
2263
|
-
.attr('font-family', FONT_FAMILY)
|
|
2264
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
2265
|
-
.attr('font-weight', isActive ? '600' : '400')
|
|
2266
|
-
.attr('fill', isActive ? palette.bg : palette.textMuted)
|
|
2267
|
-
.attr('text-anchor', 'middle')
|
|
2268
|
-
.text(label);
|
|
2269
|
-
entryX += slotW + SPEED_BADGE_GAP;
|
|
2270
|
-
}
|
|
2114
|
+
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
2115
|
+
.attr('font-weight', isSpeedActive ? '600' : '400')
|
|
2116
|
+
.attr('fill', isSpeedActive ? palette.bg : palette.textMuted)
|
|
2117
|
+
.attr('text-anchor', 'middle')
|
|
2118
|
+
.text(label);
|
|
2119
|
+
entryX += slotW + SPEED_BADGE_GAP;
|
|
2271
2120
|
}
|
|
2272
|
-
|
|
2273
|
-
cursorX += fullW + LEGEND_GROUP_GAP; // eslint-disable-line no-useless-assignment
|
|
2274
2121
|
}
|
|
2275
2122
|
}
|
|
2276
2123
|
|
package/src/kanban/renderer.ts
CHANGED
|
@@ -15,13 +15,9 @@ import type {
|
|
|
15
15
|
} from './types';
|
|
16
16
|
import { parseKanban } from './parser';
|
|
17
17
|
import { isArchiveColumn } from './mutations';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
LEGEND_DOT_R,
|
|
22
|
-
LEGEND_ENTRY_FONT_SIZE,
|
|
23
|
-
LEGEND_CAPSULE_PAD,
|
|
24
|
-
} from '../utils/legend-constants';
|
|
18
|
+
import { LEGEND_HEIGHT } from '../utils/legend-constants';
|
|
19
|
+
import { renderLegendD3 } from '../utils/legend-d3';
|
|
20
|
+
import type { LegendConfig, LegendState } from '../utils/legend-types';
|
|
25
21
|
|
|
26
22
|
// ============================================================
|
|
27
23
|
// Constants
|
|
@@ -256,128 +252,25 @@ export function renderKanban(
|
|
|
256
252
|
// Legend (bottom of diagram)
|
|
257
253
|
if (parsed.tagGroups.length > 0) {
|
|
258
254
|
const legendY = height - LEGEND_HEIGHT;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const pillWidth = pillTextWidth + 16;
|
|
279
|
-
|
|
280
|
-
// Measure total capsule width for active groups (pill + entries)
|
|
281
|
-
let capsuleContentWidth = pillWidth;
|
|
282
|
-
if (isActive) {
|
|
283
|
-
capsuleContentWidth += 4; // gap after pill
|
|
284
|
-
for (const entry of group.entries) {
|
|
285
|
-
capsuleContentWidth +=
|
|
286
|
-
LEGEND_DOT_R * 2 +
|
|
287
|
-
4 +
|
|
288
|
-
entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 +
|
|
289
|
-
8;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const capsuleWidth = capsuleContentWidth + capsulePad * 2;
|
|
293
|
-
|
|
294
|
-
// Outer capsule background for active group
|
|
295
|
-
if (isActive) {
|
|
296
|
-
legendContainer
|
|
297
|
-
.append('rect')
|
|
298
|
-
.attr('x', legendX)
|
|
299
|
-
.attr('y', legendY)
|
|
300
|
-
.attr('width', capsuleWidth)
|
|
301
|
-
.attr('height', LEGEND_HEIGHT)
|
|
302
|
-
.attr('rx', LEGEND_HEIGHT / 2)
|
|
303
|
-
.attr('fill', groupBg);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const pillX = legendX + (isActive ? capsulePad : 0);
|
|
307
|
-
|
|
308
|
-
// Pill background
|
|
309
|
-
const pillBg = isActive ? palette.bg : groupBg;
|
|
310
|
-
legendContainer
|
|
311
|
-
.append('rect')
|
|
312
|
-
.attr('x', pillX)
|
|
313
|
-
.attr('y', legendY + capsulePad)
|
|
314
|
-
.attr('width', pillWidth)
|
|
315
|
-
.attr('height', LEGEND_HEIGHT - capsulePad * 2)
|
|
316
|
-
.attr('rx', (LEGEND_HEIGHT - capsulePad * 2) / 2)
|
|
317
|
-
.attr('fill', pillBg)
|
|
318
|
-
.attr('class', 'kanban-legend-group')
|
|
319
|
-
.attr('data-legend-group', group.name.toLowerCase());
|
|
320
|
-
|
|
321
|
-
if (isActive) {
|
|
322
|
-
legendContainer
|
|
323
|
-
.append('rect')
|
|
324
|
-
.attr('x', pillX)
|
|
325
|
-
.attr('y', legendY + capsulePad)
|
|
326
|
-
.attr('width', pillWidth)
|
|
327
|
-
.attr('height', LEGEND_HEIGHT - capsulePad * 2)
|
|
328
|
-
.attr('rx', (LEGEND_HEIGHT - capsulePad * 2) / 2)
|
|
329
|
-
.attr('fill', 'none')
|
|
330
|
-
.attr('stroke', mix(palette.textMuted, palette.bg, 50))
|
|
331
|
-
.attr('stroke-width', 0.75);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Pill text
|
|
335
|
-
legendContainer
|
|
336
|
-
.append('text')
|
|
337
|
-
.attr('x', pillX + pillWidth / 2)
|
|
338
|
-
.attr('y', legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
|
|
339
|
-
.attr('font-size', LEGEND_PILL_FONT_SIZE)
|
|
340
|
-
.attr('font-weight', '500')
|
|
341
|
-
.attr('fill', isActive ? palette.text : palette.textMuted)
|
|
342
|
-
.attr('text-anchor', 'middle')
|
|
343
|
-
.text(group.name);
|
|
344
|
-
|
|
345
|
-
// Show entries inside capsule when active
|
|
346
|
-
if (isActive) {
|
|
347
|
-
let entryX = pillX + pillWidth + 4;
|
|
348
|
-
for (const entry of group.entries) {
|
|
349
|
-
const entryG = legendContainer
|
|
350
|
-
.append('g')
|
|
351
|
-
.attr('data-legend-entry', entry.value.toLowerCase())
|
|
352
|
-
.style('cursor', 'pointer');
|
|
353
|
-
|
|
354
|
-
entryG
|
|
355
|
-
.append('circle')
|
|
356
|
-
.attr('cx', entryX + LEGEND_DOT_R)
|
|
357
|
-
.attr('cy', legendY + LEGEND_HEIGHT / 2)
|
|
358
|
-
.attr('r', LEGEND_DOT_R)
|
|
359
|
-
.attr('fill', entry.color);
|
|
360
|
-
|
|
361
|
-
const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
|
|
362
|
-
entryG
|
|
363
|
-
.append('text')
|
|
364
|
-
.attr('x', entryTextX)
|
|
365
|
-
.attr(
|
|
366
|
-
'y',
|
|
367
|
-
legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
|
|
368
|
-
)
|
|
369
|
-
.attr('font-size', LEGEND_ENTRY_FONT_SIZE)
|
|
370
|
-
.attr('fill', palette.textMuted)
|
|
371
|
-
.text(entry.value);
|
|
372
|
-
|
|
373
|
-
entryX =
|
|
374
|
-
entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
|
|
375
|
-
}
|
|
376
|
-
legendX += capsuleWidth + 12;
|
|
377
|
-
} else {
|
|
378
|
-
legendX += pillWidth + 12;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
255
|
+
const legendConfig: LegendConfig = {
|
|
256
|
+
groups: parsed.tagGroups,
|
|
257
|
+
position: { placement: 'top-center', titleRelation: 'below-title' },
|
|
258
|
+
mode: exportDims ? 'inline' : 'fixed',
|
|
259
|
+
};
|
|
260
|
+
const legendState: LegendState = { activeGroup: activeTagGroup ?? null };
|
|
261
|
+
const legendG = svg
|
|
262
|
+
.append('g')
|
|
263
|
+
.attr('class', 'kanban-legend')
|
|
264
|
+
.attr('transform', `translate(${DIAGRAM_PADDING},${legendY})`);
|
|
265
|
+
renderLegendD3(
|
|
266
|
+
legendG,
|
|
267
|
+
legendConfig,
|
|
268
|
+
legendState,
|
|
269
|
+
palette,
|
|
270
|
+
isDark,
|
|
271
|
+
undefined,
|
|
272
|
+
width - DIAGRAM_PADDING * 2
|
|
273
|
+
);
|
|
381
274
|
}
|
|
382
275
|
|
|
383
276
|
// Columns
|