@diagrammo/dgmo 0.8.8 → 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/src/index.ts CHANGED
@@ -356,8 +356,28 @@ export {
356
356
  computeScatterLabelGraphics,
357
357
  } from './echarts';
358
358
  export type { ScatterLabelPoint } from './echarts';
359
- export { renderLegendSvg, type LegendGroupData } from './utils/legend-svg';
359
+ export {
360
+ renderLegendSvg,
361
+ renderLegendSvgFromConfig,
362
+ type LegendGroupData,
363
+ } from './utils/legend-svg';
360
364
  export { LEGEND_HEIGHT } from './utils/legend-constants';
365
+ export { renderLegendD3 } from './utils/legend-d3';
366
+ export {
367
+ computeLegendLayout,
368
+ getLegendReservedHeight,
369
+ } from './utils/legend-layout';
370
+ export type {
371
+ LegendConfig,
372
+ LegendState,
373
+ LegendCallbacks,
374
+ LegendPosition,
375
+ LegendMode,
376
+ LegendControl,
377
+ LegendLayout,
378
+ LegendHandle,
379
+ LegendPalette,
380
+ } from './utils/legend-types';
361
381
  export { buildMermaidQuadrant } from './dgmo-mermaid';
362
382
 
363
383
  // ============================================================
@@ -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
- // Compute centered positions
2027
- const effectiveW = (g: InfraLegendGroup) =>
2028
- activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase()
2029
- ? g.width
2030
- : g.minifiedWidth;
2031
- const playbackW = computePlaybackWidth(playback);
2032
- const trailingGaps =
2033
- legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
2034
- const totalLegendW =
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
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
2072
- const pillYOff = LEGEND_CAPSULE_PAD;
2073
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
2074
-
2075
- // Pill background
2076
- gEl
2077
- .append('rect')
2078
- .attr('x', pillXOff)
2079
- .attr('y', pillYOff)
2080
- .attr('width', pillWidth)
2081
- .attr('height', pillH)
2082
- .attr('rx', pillH / 2)
2083
- .attr('fill', isActive ? palette.bg : groupBg);
2084
-
2085
- // Active pill border
2086
- if (isActive) {
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
- // Pill text
2100
- gEl
2101
- .append('text')
2102
- .attr('x', pillXOff + pillWidth / 2)
2103
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
2104
- .attr('font-family', FONT_FAMILY)
2105
- .attr('font-size', LEGEND_PILL_FONT_SIZE)
2106
- .attr('font-weight', '500')
2107
- .attr('fill', isActive ? palette.text : palette.textMuted)
2108
- .attr('text-anchor', 'middle')
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
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
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
- // Playback pill speed + pause only
2155
- if (playback) {
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
- const pillLabel = 'Playback';
2064
+ // Inject speed badges into the Playback pill when expanded
2065
+ if (playback && playback.expanded && !playbackEl.empty()) {
2162
2066
  const pillWidth =
2163
- measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
2164
- const fullW = computePlaybackWidth(playback);
2067
+ measureLegendText('Playback', LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
2165
2068
 
2166
- const pbG = legendG
2167
- .append('g')
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
- pbG
2072
+ const ppLabel = playback.paused ? '▶' : '⏸';
2073
+ playbackEl
2208
2074
  .append('text')
2209
- .attr('x', pillXOff + pillWidth / 2)
2210
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
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('font-weight', '500')
2214
- .attr('fill', isExpanded ? palette.text : palette.textMuted)
2215
- .attr('text-anchor', 'middle')
2216
- .text(pillLabel);
2217
-
2218
- if (isExpanded) {
2219
- let entryX = pillXOff + pillWidth + 8;
2220
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
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
- const ppLabel = playback.paused ? '▶' : '⏸';
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', LEGEND_PILL_FONT_SIZE)
2229
- .attr('fill', palette.textMuted)
2230
- .attr('data-playback-action', 'toggle-pause')
2231
- .style('cursor', 'pointer')
2232
- .text(ppLabel);
2233
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
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
 
@@ -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
- LEGEND_HEIGHT,
20
- LEGEND_PILL_FONT_SIZE,
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
- let legendX = DIAGRAM_PADDING;
260
- const groupBg = isDark
261
- ? mix(palette.surface, palette.bg, 50)
262
- : mix(palette.surface, palette.bg, 30);
263
- const capsulePad = LEGEND_CAPSULE_PAD;
264
-
265
- const legendContainer = svg.append('g').attr('class', 'kanban-legend');
266
- if (activeTagGroup) {
267
- legendContainer.attr('data-legend-active', activeTagGroup.toLowerCase());
268
- }
269
-
270
- for (const group of parsed.tagGroups) {
271
- const isActive =
272
- activeTagGroup?.toLowerCase() === group.name.toLowerCase();
273
-
274
- // When a group is active, skip all other groups entirely
275
- if (activeTagGroup != null && !isActive) continue;
276
-
277
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
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