@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/dist/index.cjs CHANGED
@@ -1923,6 +1923,493 @@ var init_legend_constants = __esm({
1923
1923
  }
1924
1924
  });
1925
1925
 
1926
+ // src/utils/legend-layout.ts
1927
+ function pillWidth(name) {
1928
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1929
+ }
1930
+ function entriesWidth(entries) {
1931
+ let w = 0;
1932
+ for (const e of entries) {
1933
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1934
+ }
1935
+ return w;
1936
+ }
1937
+ function entryWidth(value) {
1938
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1939
+ }
1940
+ function controlWidth(control) {
1941
+ let w = CONTROL_PILL_PAD;
1942
+ if (control.label) {
1943
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
1944
+ if (control.icon) w += CONTROL_ICON_GAP;
1945
+ }
1946
+ if (control.icon) w += 14;
1947
+ if (control.children) {
1948
+ for (const child of control.children) {
1949
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
1950
+ }
1951
+ }
1952
+ return w;
1953
+ }
1954
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
1955
+ const pw = pillWidth(name);
1956
+ const maxCapsuleW = containerWidth;
1957
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
1958
+ const ew = entriesWidth(entries);
1959
+ const singleRowW = baseW + ew;
1960
+ if (singleRowW <= maxCapsuleW) {
1961
+ return {
1962
+ width: singleRowW,
1963
+ entryRows: 1,
1964
+ moreCount: 0,
1965
+ visibleEntries: entries.length
1966
+ };
1967
+ }
1968
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
1969
+ let row = 1;
1970
+ let rowX = pw + 4;
1971
+ let visible = 0;
1972
+ for (let i = 0; i < entries.length; i++) {
1973
+ const ew2 = entryWidth(entries[i].value);
1974
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
1975
+ row++;
1976
+ rowX = 0;
1977
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
1978
+ return {
1979
+ width: maxCapsuleW,
1980
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
1981
+ moreCount: entries.length - visible,
1982
+ visibleEntries: visible
1983
+ };
1984
+ }
1985
+ }
1986
+ rowX += ew2;
1987
+ visible++;
1988
+ }
1989
+ return {
1990
+ width: maxCapsuleW,
1991
+ entryRows: row,
1992
+ moreCount: 0,
1993
+ visibleEntries: entries.length
1994
+ };
1995
+ }
1996
+ function computeLegendLayout(config, state, containerWidth) {
1997
+ const { groups, controls: configControls, mode } = config;
1998
+ const isExport = mode === "inline";
1999
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
2000
+ if (isExport && !activeGroupName) {
2001
+ return {
2002
+ height: 0,
2003
+ width: 0,
2004
+ rows: [],
2005
+ controls: [],
2006
+ pills: [],
2007
+ activeCapsule: void 0
2008
+ };
2009
+ }
2010
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2011
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2012
+ return {
2013
+ height: 0,
2014
+ width: 0,
2015
+ rows: [],
2016
+ controls: [],
2017
+ pills: [],
2018
+ activeCapsule: void 0
2019
+ };
2020
+ }
2021
+ const controlLayouts = [];
2022
+ let totalControlsW = 0;
2023
+ if (configControls && !isExport) {
2024
+ for (const ctrl of configControls) {
2025
+ const w = controlWidth(ctrl);
2026
+ controlLayouts.push({
2027
+ id: ctrl.id,
2028
+ x: 0,
2029
+ // positioned later
2030
+ y: 0,
2031
+ width: w,
2032
+ height: LEGEND_HEIGHT,
2033
+ icon: ctrl.icon,
2034
+ label: ctrl.label,
2035
+ exportBehavior: ctrl.exportBehavior,
2036
+ children: ctrl.children?.map((c) => ({
2037
+ id: c.id,
2038
+ label: c.label,
2039
+ x: 0,
2040
+ y: 0,
2041
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2042
+ isActive: c.isActive
2043
+ }))
2044
+ });
2045
+ totalControlsW += w + CONTROL_GAP;
2046
+ }
2047
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2048
+ } else if (configControls && isExport) {
2049
+ for (const ctrl of configControls) {
2050
+ if (ctrl.exportBehavior === "strip") continue;
2051
+ const w = controlWidth(ctrl);
2052
+ controlLayouts.push({
2053
+ id: ctrl.id,
2054
+ x: 0,
2055
+ y: 0,
2056
+ width: w,
2057
+ height: LEGEND_HEIGHT,
2058
+ icon: ctrl.icon,
2059
+ label: ctrl.label,
2060
+ exportBehavior: ctrl.exportBehavior,
2061
+ children: ctrl.children?.map((c) => ({
2062
+ id: c.id,
2063
+ label: c.label,
2064
+ x: 0,
2065
+ y: 0,
2066
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2067
+ isActive: c.isActive
2068
+ }))
2069
+ });
2070
+ totalControlsW += w + CONTROL_GAP;
2071
+ }
2072
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2073
+ }
2074
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2075
+ const groupAvailW = containerWidth - controlsSpace;
2076
+ const pills = [];
2077
+ let activeCapsule;
2078
+ for (const g of visibleGroups) {
2079
+ const isActive = activeGroupName === g.name.toLowerCase();
2080
+ if (isExport && !isActive) continue;
2081
+ if (isActive) {
2082
+ activeCapsule = buildCapsuleLayout(
2083
+ g,
2084
+ containerWidth,
2085
+ config.capsulePillAddonWidth ?? 0
2086
+ );
2087
+ } else {
2088
+ const pw = pillWidth(g.name);
2089
+ pills.push({
2090
+ groupName: g.name,
2091
+ x: 0,
2092
+ y: 0,
2093
+ width: pw,
2094
+ height: LEGEND_HEIGHT,
2095
+ isActive: false
2096
+ });
2097
+ }
2098
+ }
2099
+ const rows = layoutRows(
2100
+ activeCapsule,
2101
+ pills,
2102
+ controlLayouts,
2103
+ groupAvailW,
2104
+ containerWidth,
2105
+ totalControlsW
2106
+ );
2107
+ const height = rows.length * LEGEND_HEIGHT;
2108
+ const width = containerWidth;
2109
+ return {
2110
+ height,
2111
+ width,
2112
+ rows,
2113
+ activeCapsule,
2114
+ controls: controlLayouts,
2115
+ pills
2116
+ };
2117
+ }
2118
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2119
+ const pw = pillWidth(group.name);
2120
+ const info = capsuleWidth(
2121
+ group.name,
2122
+ group.entries,
2123
+ containerWidth,
2124
+ addonWidth
2125
+ );
2126
+ const pill = {
2127
+ groupName: group.name,
2128
+ x: LEGEND_CAPSULE_PAD,
2129
+ y: LEGEND_CAPSULE_PAD,
2130
+ width: pw,
2131
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2132
+ isActive: true
2133
+ };
2134
+ const entries = [];
2135
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2136
+ let ey = 0;
2137
+ let rowX = ex;
2138
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2139
+ let currentRow = 0;
2140
+ for (let i = 0; i < info.visibleEntries; i++) {
2141
+ const entry = group.entries[i];
2142
+ const ew = entryWidth(entry.value);
2143
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2144
+ currentRow++;
2145
+ rowX = 0;
2146
+ ey = currentRow * LEGEND_HEIGHT;
2147
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2148
+ }
2149
+ const dotCx = rowX + LEGEND_DOT_R;
2150
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2151
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2152
+ const textY = ey + LEGEND_HEIGHT / 2;
2153
+ entries.push({
2154
+ value: entry.value,
2155
+ color: entry.color,
2156
+ x: rowX,
2157
+ y: ey,
2158
+ dotCx,
2159
+ dotCy,
2160
+ textX,
2161
+ textY
2162
+ });
2163
+ rowX += ew;
2164
+ }
2165
+ const totalRows = info.entryRows;
2166
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2167
+ return {
2168
+ groupName: group.name,
2169
+ x: 0,
2170
+ y: 0,
2171
+ width: info.width,
2172
+ height: capsuleH,
2173
+ pill,
2174
+ entries,
2175
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2176
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2177
+ };
2178
+ }
2179
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2180
+ const rows = [];
2181
+ const groupItems = [];
2182
+ if (activeCapsule) groupItems.push(activeCapsule);
2183
+ groupItems.push(...pills);
2184
+ let currentRowItems = [];
2185
+ let currentRowW = 0;
2186
+ let rowY = 0;
2187
+ for (const item of groupItems) {
2188
+ const itemW = item.width + LEGEND_GROUP_GAP;
2189
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2190
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2191
+ rows.push({ y: rowY, items: currentRowItems });
2192
+ rowY += LEGEND_HEIGHT;
2193
+ currentRowItems = [];
2194
+ currentRowW = 0;
2195
+ }
2196
+ item.x = currentRowW;
2197
+ item.y = rowY;
2198
+ currentRowItems.push(item);
2199
+ currentRowW += itemW;
2200
+ }
2201
+ if (controls.length > 0) {
2202
+ let cx = containerWidth;
2203
+ for (let i = controls.length - 1; i >= 0; i--) {
2204
+ cx -= controls[i].width;
2205
+ controls[i].x = cx;
2206
+ controls[i].y = 0;
2207
+ cx -= CONTROL_GAP;
2208
+ }
2209
+ if (rows.length > 0) {
2210
+ rows[0].items.push(...controls);
2211
+ } else if (currentRowItems.length > 0) {
2212
+ currentRowItems.push(...controls);
2213
+ } else {
2214
+ currentRowItems.push(...controls);
2215
+ }
2216
+ }
2217
+ if (currentRowItems.length > 0) {
2218
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2219
+ rows.push({ y: rowY, items: currentRowItems });
2220
+ }
2221
+ if (rows.length === 0) {
2222
+ rows.push({ y: 0, items: [] });
2223
+ }
2224
+ return rows;
2225
+ }
2226
+ function centerRowItems(items, containerWidth, totalControlsW) {
2227
+ const groupItems = items.filter((it) => "groupName" in it);
2228
+ if (groupItems.length === 0) return;
2229
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2230
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2231
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2232
+ let x = offset;
2233
+ for (const item of groupItems) {
2234
+ item.x = x;
2235
+ x += item.width + LEGEND_GROUP_GAP;
2236
+ }
2237
+ }
2238
+ function getLegendReservedHeight(config, state, containerWidth) {
2239
+ const layout = computeLegendLayout(config, state, containerWidth);
2240
+ return layout.height;
2241
+ }
2242
+ var LEGEND_MAX_ENTRY_ROWS, CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2243
+ var init_legend_layout = __esm({
2244
+ "src/utils/legend-layout.ts"() {
2245
+ "use strict";
2246
+ init_legend_constants();
2247
+ LEGEND_MAX_ENTRY_ROWS = 3;
2248
+ CONTROL_PILL_PAD = 16;
2249
+ CONTROL_FONT_SIZE = 11;
2250
+ CONTROL_ICON_GAP = 4;
2251
+ CONTROL_GAP = 8;
2252
+ }
2253
+ });
2254
+
2255
+ // src/utils/legend-d3.ts
2256
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2257
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2258
+ let currentState = { ...state };
2259
+ let currentLayout;
2260
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2261
+ function render2() {
2262
+ currentLayout = computeLegendLayout(config, currentState, width);
2263
+ legendG.selectAll("*").remove();
2264
+ if (currentLayout.height === 0) return;
2265
+ if (currentState.activeGroup) {
2266
+ legendG.attr(
2267
+ "data-legend-active",
2268
+ currentState.activeGroup.toLowerCase()
2269
+ );
2270
+ } else {
2271
+ legendG.attr("data-legend-active", null);
2272
+ }
2273
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2274
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2275
+ if (currentLayout.activeCapsule) {
2276
+ renderCapsule(
2277
+ legendG,
2278
+ currentLayout.activeCapsule,
2279
+ palette,
2280
+ groupBg,
2281
+ pillBorder,
2282
+ isDark,
2283
+ callbacks
2284
+ );
2285
+ }
2286
+ for (const pill of currentLayout.pills) {
2287
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2288
+ }
2289
+ for (const ctrl of currentLayout.controls) {
2290
+ renderControl(
2291
+ legendG,
2292
+ ctrl,
2293
+ palette,
2294
+ groupBg,
2295
+ pillBorder,
2296
+ isDark,
2297
+ config.controls
2298
+ );
2299
+ }
2300
+ }
2301
+ render2();
2302
+ return {
2303
+ setState(newState) {
2304
+ currentState = { ...newState };
2305
+ render2();
2306
+ },
2307
+ destroy() {
2308
+ legendG.remove();
2309
+ },
2310
+ getHeight() {
2311
+ return currentLayout?.height ?? 0;
2312
+ },
2313
+ getLayout() {
2314
+ return currentLayout;
2315
+ }
2316
+ };
2317
+ }
2318
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2319
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2320
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2321
+ const pill = capsule.pill;
2322
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
2323
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2324
+ g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
2325
+ for (const entry of capsule.entries) {
2326
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2327
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2328
+ entryG.append("text").attr("x", entry.textX).attr("y", entry.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.value);
2329
+ if (callbacks?.onEntryHover) {
2330
+ const groupName = capsule.groupName;
2331
+ const entryValue = entry.value;
2332
+ const onHover = callbacks.onEntryHover;
2333
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2334
+ }
2335
+ }
2336
+ if (capsule.moreCount) {
2337
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2338
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2339
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2340
+ g.append("text").attr("x", moreX).attr("y", moreY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-style", "italic").attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(`+${capsule.moreCount} more`);
2341
+ }
2342
+ if (callbacks?.onGroupToggle) {
2343
+ const cb = callbacks.onGroupToggle;
2344
+ const name = capsule.groupName;
2345
+ g.on("click", () => cb(name));
2346
+ }
2347
+ if (callbacks?.onGroupRendered) {
2348
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2349
+ }
2350
+ }
2351
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2352
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2353
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2354
+ g.append("text").attr("x", pill.width / 2).attr("y", pill.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(pill.groupName);
2355
+ if (callbacks?.onGroupToggle) {
2356
+ const cb = callbacks.onGroupToggle;
2357
+ const name = pill.groupName;
2358
+ g.on("click", () => cb(name));
2359
+ }
2360
+ if (callbacks?.onGroupRendered) {
2361
+ callbacks.onGroupRendered(pill.groupName, g, false);
2362
+ }
2363
+ }
2364
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2365
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2366
+ if (ctrl.exportBehavior === "strip") {
2367
+ g.attr("data-export-ignore", "true");
2368
+ }
2369
+ g.append("rect").attr("width", ctrl.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2370
+ let textX = ctrl.width / 2;
2371
+ if (ctrl.icon && ctrl.label) {
2372
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2373
+ iconG.html(ctrl.icon);
2374
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2375
+ }
2376
+ if (ctrl.label) {
2377
+ g.append("text").attr("x", textX).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(ctrl.label);
2378
+ }
2379
+ if (ctrl.children) {
2380
+ let cx = ctrl.width + 4;
2381
+ for (const child of ctrl.children) {
2382
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2383
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2384
+ "fill",
2385
+ child.isActive ? palette.primary ?? palette.text : "none"
2386
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2387
+ childG.append("text").attr("x", child.width / 2).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", child.isActive ? palette.bg : palette.textMuted).attr("font-family", FONT_FAMILY).text(child.label);
2388
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2389
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2390
+ if (configChild?.onClick) {
2391
+ const onClick = configChild.onClick;
2392
+ childG.on("click", () => onClick());
2393
+ }
2394
+ cx += child.width + 4;
2395
+ }
2396
+ }
2397
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2398
+ if (configCtrl?.onClick) {
2399
+ const onClick = configCtrl.onClick;
2400
+ g.on("click", () => onClick());
2401
+ }
2402
+ }
2403
+ var init_legend_d3 = __esm({
2404
+ "src/utils/legend-d3.ts"() {
2405
+ "use strict";
2406
+ init_legend_constants();
2407
+ init_legend_layout();
2408
+ init_color_utils();
2409
+ init_fonts();
2410
+ }
2411
+ });
2412
+
1926
2413
  // src/utils/title-constants.ts
1927
2414
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1928
2415
  var init_title_constants = __esm({
@@ -4852,10 +5339,10 @@ var init_chart = __esm({
4852
5339
  function esc(s) {
4853
5340
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4854
5341
  }
4855
- function pillWidth(name) {
5342
+ function pillWidth2(name) {
4856
5343
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4857
5344
  }
4858
- function entriesWidth(entries) {
5345
+ function entriesWidth2(entries) {
4859
5346
  let w = 0;
4860
5347
  for (const e of entries) {
4861
5348
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4863,9 +5350,9 @@ function entriesWidth(entries) {
4863
5350
  return w;
4864
5351
  }
4865
5352
  function groupTotalWidth(name, entries, isActive) {
4866
- const pw = pillWidth(name);
5353
+ const pw = pillWidth2(name);
4867
5354
  if (!isActive) return pw;
4868
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5355
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4869
5356
  }
4870
5357
  function renderLegendSvg(groups, options) {
4871
5358
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4873,7 +5360,7 @@ function renderLegendSvg(groups, options) {
4873
5360
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4874
5361
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4875
5362
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4876
- const pw = pillWidth(g.name);
5363
+ const pw = pillWidth2(g.name);
4877
5364
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4878
5365
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4879
5366
  });
@@ -4927,6 +5414,19 @@ function renderLegendSvg(groups, options) {
4927
5414
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4928
5415
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4929
5416
  }
5417
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5418
+ return renderLegendSvg(config.groups, {
5419
+ palette: {
5420
+ bg: palette.bg,
5421
+ surface: palette.surface,
5422
+ text: palette.text,
5423
+ textMuted: palette.textMuted
5424
+ },
5425
+ isDark: palette.isDark,
5426
+ containerWidth,
5427
+ activeGroup: state.activeGroup
5428
+ });
5429
+ }
4930
5430
  var init_legend_svg = __esm({
4931
5431
  "src/utils/legend-svg.ts"() {
4932
5432
  "use strict";
@@ -8379,7 +8879,12 @@ __export(parser_exports7, {
8379
8879
  function parseArrowLine(trimmed, palette) {
8380
8880
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8381
8881
  if (bareMatch) {
8382
- return { target: bareMatch[1].trim() };
8882
+ const rawTarget = bareMatch[1].trim();
8883
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8884
+ return {
8885
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8886
+ targetIsGroup: !!groupMatch
8887
+ };
8383
8888
  }
8384
8889
  const arrowMatch = trimmed.match(ARROW_RE);
8385
8890
  if (arrowMatch) {
@@ -8388,8 +8893,14 @@ function parseArrowLine(trimmed, palette) {
8388
8893
  if (label && !color) {
8389
8894
  color = inferArrowColor(label);
8390
8895
  }
8391
- const target = arrowMatch[3].trim();
8392
- return { label, color, target };
8896
+ const rawTarget = arrowMatch[3].trim();
8897
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8898
+ return {
8899
+ label,
8900
+ color,
8901
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8902
+ targetIsGroup: !!groupMatch
8903
+ };
8393
8904
  }
8394
8905
  return null;
8395
8906
  }
@@ -8451,6 +8962,7 @@ function parseSitemap(content, palette) {
8451
8962
  const aliasMap = /* @__PURE__ */ new Map();
8452
8963
  const indentStack = [];
8453
8964
  const labelToNode = /* @__PURE__ */ new Map();
8965
+ const labelToContainer = /* @__PURE__ */ new Map();
8454
8966
  const deferredArrows = [];
8455
8967
  for (let i = 0; i < lines.length; i++) {
8456
8968
  const line10 = lines[i];
@@ -8552,6 +9064,7 @@ function parseSitemap(content, palette) {
8552
9064
  deferredArrows.push({
8553
9065
  sourceNode: source,
8554
9066
  targetLabel: arrowInfo.target,
9067
+ targetIsGroup: arrowInfo.targetIsGroup,
8555
9068
  label: arrowInfo.label,
8556
9069
  color: arrowInfo.color,
8557
9070
  lineNumber
@@ -8585,6 +9098,7 @@ function parseSitemap(content, palette) {
8585
9098
  color
8586
9099
  };
8587
9100
  attachNode2(node, indent, indentStack, result);
9101
+ labelToContainer.set(label.toLowerCase(), node);
8588
9102
  } else if (metadataMatch && indentStack.length > 0) {
8589
9103
  const rawKey = metadataMatch[1].trim().toLowerCase();
8590
9104
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8625,22 +9139,41 @@ function parseSitemap(content, palette) {
8625
9139
  }
8626
9140
  for (const arrow of deferredArrows) {
8627
9141
  const targetKey = arrow.targetLabel.toLowerCase();
8628
- const targetNode = labelToNode.get(targetKey);
8629
- if (!targetNode) {
8630
- const allLabels = Array.from(labelToNode.keys());
8631
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8632
- const hint = suggest(targetKey, allLabels);
8633
- if (hint) msg += `. ${hint}`;
8634
- pushError(arrow.lineNumber, msg);
8635
- continue;
9142
+ if (arrow.targetIsGroup) {
9143
+ const targetContainer = labelToContainer.get(targetKey);
9144
+ if (!targetContainer) {
9145
+ const allLabels = Array.from(labelToContainer.keys());
9146
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9147
+ const hint = suggest(targetKey, allLabels);
9148
+ if (hint) msg += `. ${hint}`;
9149
+ pushError(arrow.lineNumber, msg);
9150
+ continue;
9151
+ }
9152
+ result.edges.push({
9153
+ sourceId: arrow.sourceNode.id,
9154
+ targetId: targetContainer.id,
9155
+ label: arrow.label,
9156
+ color: arrow.color,
9157
+ lineNumber: arrow.lineNumber
9158
+ });
9159
+ } else {
9160
+ const targetNode = labelToNode.get(targetKey);
9161
+ if (!targetNode) {
9162
+ const allLabels = Array.from(labelToNode.keys());
9163
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9164
+ const hint = suggest(targetKey, allLabels);
9165
+ if (hint) msg += `. ${hint}`;
9166
+ pushError(arrow.lineNumber, msg);
9167
+ continue;
9168
+ }
9169
+ result.edges.push({
9170
+ sourceId: arrow.sourceNode.id,
9171
+ targetId: targetNode.id,
9172
+ label: arrow.label,
9173
+ color: arrow.color,
9174
+ lineNumber: arrow.lineNumber
9175
+ });
8636
9176
  }
8637
- result.edges.push({
8638
- sourceId: arrow.sourceNode.id,
8639
- targetId: targetNode.id,
8640
- label: arrow.label,
8641
- color: arrow.color,
8642
- lineNumber: arrow.lineNumber
8643
- });
8644
9177
  }
8645
9178
  if (result.tagGroups.length > 0) {
8646
9179
  const allNodes = [];
@@ -10354,6 +10887,7 @@ function parseBoxesAndLines(content) {
10354
10887
  const nodeLabels = /* @__PURE__ */ new Set();
10355
10888
  const groupLabels = /* @__PURE__ */ new Set();
10356
10889
  let lastNodeLabel = null;
10890
+ let lastSourceIsGroup = false;
10357
10891
  const groupStack = [];
10358
10892
  let contentStarted = false;
10359
10893
  let currentTagGroup = null;
@@ -10592,6 +11126,8 @@ function parseBoxesAndLines(content) {
10592
11126
  };
10593
11127
  groupLabels.add(label);
10594
11128
  groupStack.push({ group, indent, depth: currentDepth });
11129
+ lastNodeLabel = label;
11130
+ lastSourceIsGroup = true;
10595
11131
  continue;
10596
11132
  }
10597
11133
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10609,7 +11145,8 @@ function parseBoxesAndLines(content) {
10609
11145
  );
10610
11146
  continue;
10611
11147
  }
10612
- edgeText = `${lastNodeLabel} ${trimmed}`;
11148
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11149
+ edgeText = `${sourcePrefix} ${trimmed}`;
10613
11150
  }
10614
11151
  const edge = parseEdgeLine(
10615
11152
  edgeText,
@@ -10632,6 +11169,7 @@ function parseBoxesAndLines(content) {
10632
11169
  continue;
10633
11170
  }
10634
11171
  lastNodeLabel = node.label;
11172
+ lastSourceIsGroup = false;
10635
11173
  const gs = currentGroupState();
10636
11174
  const isGroupChild = gs && indent > gs.indent;
10637
11175
  if (nodeLabels.has(node.label)) {
@@ -10659,14 +11197,42 @@ function parseBoxesAndLines(content) {
10659
11197
  const gs = groupStack.pop();
10660
11198
  result.groups.push(gs.group);
10661
11199
  }
11200
+ const validEdges = [];
10662
11201
  for (const edge of result.edges) {
10663
- if (!edge.source.startsWith("__group_")) {
11202
+ let valid = true;
11203
+ if (edge.source.startsWith("__group_")) {
11204
+ const label = edge.source.slice("__group_".length);
11205
+ const found = [...groupLabels].some(
11206
+ (g) => g.toLowerCase() === label.toLowerCase()
11207
+ );
11208
+ if (!found) {
11209
+ result.diagnostics.push(
11210
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11211
+ );
11212
+ valid = false;
11213
+ }
11214
+ } else {
10664
11215
  ensureNode(edge.source, edge.lineNumber);
10665
11216
  }
10666
- if (!edge.target.startsWith("__group_")) {
11217
+ if (edge.target.startsWith("__group_")) {
11218
+ const label = edge.target.slice("__group_".length);
11219
+ const found = [...groupLabels].some(
11220
+ (g) => g.toLowerCase() === label.toLowerCase()
11221
+ );
11222
+ if (!found) {
11223
+ result.diagnostics.push(
11224
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11225
+ );
11226
+ valid = false;
11227
+ }
11228
+ } else {
10667
11229
  ensureNode(edge.target, edge.lineNumber);
10668
11230
  }
11231
+ if (valid) {
11232
+ validEdges.push(edge);
11233
+ }
10669
11234
  }
11235
+ result.edges = validEdges;
10670
11236
  if (result.tagGroups.length > 0) {
10671
11237
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10672
11238
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10695,10 +11261,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10695
11261
  description
10696
11262
  };
10697
11263
  }
11264
+ function resolveEndpoint(name) {
11265
+ const m = name.match(/^\[(.+)\]$/);
11266
+ return m ? groupId2(m[1].trim()) : name;
11267
+ }
10698
11268
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10699
11269
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10700
11270
  if (biLabeledMatch) {
10701
- const source2 = biLabeledMatch[1].trim();
11271
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10702
11272
  const label = biLabeledMatch[2].trim();
10703
11273
  let rest2 = biLabeledMatch[3].trim();
10704
11274
  let metadata2 = {};
@@ -10719,7 +11289,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10719
11289
  }
10720
11290
  return {
10721
11291
  source: source2,
10722
- target: rest2,
11292
+ target: resolveEndpoint(rest2),
10723
11293
  label: label || void 0,
10724
11294
  bidirectional: true,
10725
11295
  lineNumber: lineNum,
@@ -10728,7 +11298,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10728
11298
  }
10729
11299
  const biIdx = trimmed.indexOf("<->");
10730
11300
  if (biIdx >= 0) {
10731
- const source2 = trimmed.slice(0, biIdx).trim();
11301
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10732
11302
  let rest2 = trimmed.slice(biIdx + 3).trim();
10733
11303
  let metadata2 = {};
10734
11304
  const pipeIdx2 = rest2.indexOf("|");
@@ -10748,7 +11318,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10748
11318
  }
10749
11319
  return {
10750
11320
  source: source2,
10751
- target: rest2,
11321
+ target: resolveEndpoint(rest2),
10752
11322
  bidirectional: true,
10753
11323
  lineNumber: lineNum,
10754
11324
  metadata: metadata2
@@ -10756,7 +11326,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10756
11326
  }
10757
11327
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10758
11328
  if (labeledMatch) {
10759
- const source2 = labeledMatch[1].trim();
11329
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10760
11330
  const label = labeledMatch[2].trim();
10761
11331
  let rest2 = labeledMatch[3].trim();
10762
11332
  if (label) {
@@ -10778,7 +11348,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10778
11348
  }
10779
11349
  return {
10780
11350
  source: source2,
10781
- target: rest2,
11351
+ target: resolveEndpoint(rest2),
10782
11352
  label,
10783
11353
  bidirectional: false,
10784
11354
  lineNumber: lineNum,
@@ -10788,7 +11358,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10788
11358
  }
10789
11359
  const arrowIdx = trimmed.indexOf("->");
10790
11360
  if (arrowIdx < 0) return null;
10791
- const source = trimmed.slice(0, arrowIdx).trim();
11361
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10792
11362
  let rest = trimmed.slice(arrowIdx + 2).trim();
10793
11363
  if (!source || !rest) {
10794
11364
  diagnostics.push(
@@ -10809,7 +11379,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10809
11379
  }
10810
11380
  return {
10811
11381
  source,
10812
- target: rest,
11382
+ target: resolveEndpoint(rest),
10813
11383
  bidirectional: false,
10814
11384
  lineNumber: lineNum,
10815
11385
  metadata
@@ -11123,14 +11693,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11123
11693
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11124
11694
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11125
11695
  if (visibleEntries.length === 0) continue;
11126
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11127
- const minPillWidth = pillWidth2;
11128
- let entriesWidth2 = 0;
11696
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11697
+ const minPillWidth = pillWidth3;
11698
+ let entriesWidth3 = 0;
11129
11699
  for (const entry of visibleEntries) {
11130
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11700
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11131
11701
  }
11132
11702
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11133
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11703
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11134
11704
  groups.push({
11135
11705
  name: group.name,
11136
11706
  alias: group.alias,
@@ -11140,7 +11710,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11140
11710
  })),
11141
11711
  x: 0,
11142
11712
  y: 0,
11143
- width: capsuleWidth,
11713
+ width: capsuleWidth2,
11144
11714
  height: LEGEND_HEIGHT2,
11145
11715
  minifiedWidth: minPillWidth,
11146
11716
  minifiedHeight: LEGEND_HEIGHT2
@@ -12081,66 +12651,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12081
12651
  }
12082
12652
  }
12083
12653
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12084
- const visibleGroups = layout.legend.filter((group) => {
12085
- if (legendOnly) return true;
12086
- if (activeTagGroup == null) return true;
12087
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12088
- });
12089
- let fixedPositions;
12090
- if (fixedLegend && visibleGroups.length > 0) {
12091
- fixedPositions = /* @__PURE__ */ new Map();
12092
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12093
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12094
- let cx = (width - totalW) / 2;
12095
- for (const g of visibleGroups) {
12096
- fixedPositions.set(g.name, cx);
12097
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12098
- }
12099
- }
12100
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12101
- const legendParent = legendParentBase;
12102
- if (fixedLegend && activeTagGroup) {
12103
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12654
+ const groups = layout.legend.map((g) => ({
12655
+ name: g.name,
12656
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12657
+ }));
12658
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12659
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12660
+ let legendHandle;
12661
+ if (legendOnly) {
12662
+ for (const lg of layout.legend) {
12663
+ const singleConfig = {
12664
+ groups: [
12665
+ {
12666
+ name: lg.name,
12667
+ entries: lg.entries.map((e) => ({
12668
+ value: e.value,
12669
+ color: e.color
12670
+ }))
12671
+ }
12672
+ ],
12673
+ position: { placement: "top-center", titleRelation: "below-title" },
12674
+ mode: "fixed"
12675
+ };
12676
+ const singleState = { activeGroup: lg.name };
12677
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12678
+ renderLegendD3(
12679
+ groupG,
12680
+ singleConfig,
12681
+ singleState,
12682
+ palette,
12683
+ isDark,
12684
+ void 0,
12685
+ lg.width
12686
+ );
12687
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12688
+ }
12689
+ legendHandle = null;
12690
+ } else {
12691
+ const legendConfig = {
12692
+ groups,
12693
+ position: { placement: "top-center", titleRelation: "below-title" },
12694
+ mode: "fixed",
12695
+ capsulePillAddonWidth: eyeAddonWidth
12696
+ };
12697
+ const legendState = { activeGroup: activeTagGroup ?? null };
12698
+ legendHandle = renderLegendD3(
12699
+ legendParentBase,
12700
+ legendConfig,
12701
+ legendState,
12702
+ palette,
12703
+ isDark,
12704
+ void 0,
12705
+ fixedLegend ? width : layout.width
12706
+ );
12707
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12104
12708
  }
12105
- for (const group of visibleGroups) {
12106
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12107
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12108
- const pillLabel = group.name;
12109
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12110
- const gX = fixedPositions?.get(group.name) ?? group.x;
12111
- const gY = fixedPositions ? 0 : group.y;
12112
- const gEl = legendParent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "org-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", legendOnly ? "default" : "pointer");
12113
- if (isActive) {
12114
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12115
- }
12116
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12117
- const pillYOff = LEGEND_CAPSULE_PAD;
12118
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12119
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
12120
- if (isActive) {
12121
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
12122
- }
12123
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
12124
- if (isActive && fixedLegend) {
12125
- const groupKey = group.name.toLowerCase();
12709
+ if (fixedLegend && legendHandle) {
12710
+ const computedLayout = legendHandle.getLayout();
12711
+ if (computedLayout.activeCapsule?.addonX != null) {
12712
+ const capsule = computedLayout.activeCapsule;
12713
+ const groupKey = capsule.groupName.toLowerCase();
12126
12714
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12127
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12128
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12129
- const hitPad = 6;
12130
- const eyeG = gEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12131
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12132
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12133
- }
12134
- if (isActive) {
12135
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12136
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12137
- for (const entry of group.entries) {
12138
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12139
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12140
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12141
- const entryLabel = entry.value;
12142
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entryLabel);
12143
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12715
+ const activeGroupEl = legendParentBase.select(
12716
+ `[data-legend-group="${groupKey}"]`
12717
+ );
12718
+ if (!activeGroupEl.empty()) {
12719
+ const eyeX = capsule.addonX;
12720
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12721
+ const hitPad = 6;
12722
+ const eyeG = activeGroupEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12723
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12724
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12144
12725
  }
12145
12726
  }
12146
12727
  }
@@ -12175,6 +12756,7 @@ var init_renderer = __esm({
12175
12756
  init_parser4();
12176
12757
  init_layout();
12177
12758
  init_legend_constants();
12759
+ init_legend_d3();
12178
12760
  init_title_constants();
12179
12761
  DIAGRAM_PADDING = 20;
12180
12762
  MAX_SCALE = 3;
@@ -12203,6 +12785,17 @@ var layout_exports2 = {};
12203
12785
  __export(layout_exports2, {
12204
12786
  layoutSitemap: () => layoutSitemap
12205
12787
  });
12788
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12789
+ const dx = tx - cx;
12790
+ const dy = ty - cy;
12791
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12792
+ const hw = w / 2;
12793
+ const hh = h / 2;
12794
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12795
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12796
+ const s = Math.min(sx, sy);
12797
+ return { x: cx + dx * s, y: cy + dy * s };
12798
+ }
12206
12799
  function filterMetadata2(metadata, hiddenAttributes) {
12207
12800
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12208
12801
  const filtered = {};
@@ -12219,7 +12812,10 @@ function computeCardWidth2(label, meta) {
12219
12812
  const lineChars = key.length + 2 + value.length;
12220
12813
  if (lineChars > maxChars) maxChars = lineChars;
12221
12814
  }
12222
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12815
+ return Math.max(
12816
+ MIN_CARD_WIDTH2,
12817
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
12818
+ );
12223
12819
  }
12224
12820
  function computeCardHeight2(meta) {
12225
12821
  const metaCount = Object.keys(meta).length;
@@ -12228,7 +12824,12 @@ function computeCardHeight2(meta) {
12228
12824
  }
12229
12825
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12230
12826
  if (node.color) return node.color;
12231
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
12827
+ return resolveTagColor(
12828
+ node.metadata,
12829
+ tagGroups,
12830
+ activeGroupName,
12831
+ node.isContainer
12832
+ );
12232
12833
  }
12233
12834
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12234
12835
  const groups = [];
@@ -12237,21 +12838,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12237
12838
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12238
12839
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12239
12840
  if (visibleEntries.length === 0) continue;
12240
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12241
- const minPillWidth = pillWidth2;
12242
- let entriesWidth2 = 0;
12841
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12842
+ const minPillWidth = pillWidth3;
12843
+ let entriesWidth3 = 0;
12243
12844
  for (const entry of visibleEntries) {
12244
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12845
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12245
12846
  }
12246
12847
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12247
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
12848
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12248
12849
  groups.push({
12249
12850
  name: group.name,
12250
12851
  alias: group.alias,
12251
12852
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12252
12853
  x: 0,
12253
12854
  y: 0,
12254
- width: capsuleWidth,
12855
+ width: capsuleWidth2,
12255
12856
  height: LEGEND_HEIGHT3,
12256
12857
  minifiedWidth: minPillWidth,
12257
12858
  minifiedHeight: LEGEND_HEIGHT3
@@ -12271,10 +12872,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12271
12872
  parentPageId,
12272
12873
  meta,
12273
12874
  fullMeta: { ...node.metadata },
12274
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
12875
+ width: Math.max(
12876
+ MIN_CARD_WIDTH2,
12877
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
12878
+ ),
12275
12879
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12276
12880
  });
12277
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
12881
+ flattenNodes(
12882
+ node.children,
12883
+ node.id,
12884
+ parentPageId,
12885
+ hiddenCounts,
12886
+ hiddenAttributes,
12887
+ result
12888
+ );
12278
12889
  } else {
12279
12890
  result.push({
12280
12891
  sitemapNode: node,
@@ -12286,14 +12897,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12286
12897
  height: computeCardHeight2(meta)
12287
12898
  });
12288
12899
  if (node.children.length > 0) {
12289
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
12900
+ flattenNodes(
12901
+ node.children,
12902
+ parentContainerId,
12903
+ node.id,
12904
+ hiddenCounts,
12905
+ hiddenAttributes,
12906
+ result
12907
+ );
12290
12908
  }
12291
12909
  }
12292
12910
  }
12293
12911
  }
12294
12912
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12295
12913
  if (parsed.roots.length === 0) {
12296
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
12914
+ return {
12915
+ nodes: [],
12916
+ edges: [],
12917
+ containers: [],
12918
+ legend: [],
12919
+ width: 0,
12920
+ height: 0
12921
+ };
12297
12922
  }
12298
12923
  const allNodes = [];
12299
12924
  const collect = (node) => {
@@ -12301,9 +12926,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12301
12926
  for (const child of node.children) collect(child);
12302
12927
  };
12303
12928
  for (const root of parsed.roots) collect(root);
12304
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
12929
+ injectDefaultTagMetadata(
12930
+ allNodes,
12931
+ parsed.tagGroups,
12932
+ (e) => e.isContainer
12933
+ );
12305
12934
  const flatNodes = [];
12306
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
12935
+ flattenNodes(
12936
+ parsed.roots,
12937
+ null,
12938
+ null,
12939
+ hiddenCounts,
12940
+ hiddenAttributes,
12941
+ flatNodes
12942
+ );
12307
12943
  const nodeMap = /* @__PURE__ */ new Map();
12308
12944
  for (const flat of flatNodes) {
12309
12945
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12365,14 +13001,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12365
13001
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12366
13002
  }
12367
13003
  }
13004
+ const expandedContainerIds = /* @__PURE__ */ new Set();
13005
+ for (const cid of containerIds) {
13006
+ if (!collapsedContainerIds.has(cid)) {
13007
+ expandedContainerIds.add(cid);
13008
+ }
13009
+ }
13010
+ const deferredEdgeIndices = [];
12368
13011
  for (let i = 0; i < parsed.edges.length; i++) {
12369
13012
  const edge = parsed.edges[i];
12370
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12371
- g.setEdge(edge.sourceId, edge.targetId, {
13013
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
13014
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
13015
+ deferredEdgeIndices.push(i);
13016
+ continue;
13017
+ }
13018
+ g.setEdge(
13019
+ edge.sourceId,
13020
+ edge.targetId,
13021
+ {
12372
13022
  label: edge.label ?? "",
12373
13023
  minlen: 1
12374
- }, `e${i}`);
12375
- }
13024
+ },
13025
+ `e${i}`
13026
+ );
12376
13027
  }
12377
13028
  import_dagre.default.layout(g);
12378
13029
  const layoutNodes = [];
@@ -12440,19 +13091,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12440
13091
  });
12441
13092
  }
12442
13093
  }
13094
+ const deferredSet = new Set(deferredEdgeIndices);
12443
13095
  const layoutEdges = [];
12444
13096
  for (let i = 0; i < parsed.edges.length; i++) {
12445
13097
  const edge = parsed.edges[i];
12446
13098
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12447
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12448
- if (!edgeData) continue;
13099
+ let points;
13100
+ if (deferredSet.has(i)) {
13101
+ const srcNode = g.node(edge.sourceId);
13102
+ const tgtNode = g.node(edge.targetId);
13103
+ if (!srcNode || !tgtNode) continue;
13104
+ const srcPt = clipToRectBorder(
13105
+ srcNode.x,
13106
+ srcNode.y,
13107
+ srcNode.width,
13108
+ srcNode.height,
13109
+ tgtNode.x,
13110
+ tgtNode.y
13111
+ );
13112
+ const tgtPt = clipToRectBorder(
13113
+ tgtNode.x,
13114
+ tgtNode.y,
13115
+ tgtNode.width,
13116
+ tgtNode.height,
13117
+ srcNode.x,
13118
+ srcNode.y
13119
+ );
13120
+ const midX = (srcPt.x + tgtPt.x) / 2;
13121
+ const midY = (srcPt.y + tgtPt.y) / 2;
13122
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13123
+ } else {
13124
+ const edgeData = g.edge({
13125
+ v: edge.sourceId,
13126
+ w: edge.targetId,
13127
+ name: `e${i}`
13128
+ });
13129
+ if (!edgeData) continue;
13130
+ points = edgeData.points ?? [];
13131
+ }
12449
13132
  layoutEdges.push({
12450
13133
  sourceId: edge.sourceId,
12451
13134
  targetId: edge.targetId,
12452
- points: edgeData.points ?? [],
13135
+ points,
12453
13136
  label: edge.label,
12454
13137
  color: edge.color,
12455
- lineNumber: edge.lineNumber
13138
+ lineNumber: edge.lineNumber,
13139
+ deferred: deferredSet.has(i) || void 0
12456
13140
  });
12457
13141
  }
12458
13142
  {
@@ -12613,7 +13297,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12613
13297
  usedValuesByGroup.set(key, used);
12614
13298
  }
12615
13299
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12616
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13300
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13301
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13302
+ ) : legendGroups;
12617
13303
  const allExpanded = expandAllLegend && activeTagGroup == null;
12618
13304
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12619
13305
  if (visibleGroups.length > 0) {
@@ -12928,7 +13614,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12928
13614
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
12929
13615
  const edgeColor3 = edge.color ?? palette.textMuted;
12930
13616
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
12931
- const pathD = lineGenerator(edge.points);
13617
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13618
+ const pathD = gen(edge.points);
12932
13619
  if (pathD) {
12933
13620
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor3).attr("stroke-width", EDGE_STROKE_WIDTH2).attr("marker-end", `url(#${markerId})`).attr("class", "sitemap-edge");
12934
13621
  }
@@ -13019,57 +13706,44 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13019
13706
  }
13020
13707
  function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13021
13708
  if (legendGroups.length === 0) return;
13022
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13023
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13024
- ) : legendGroups;
13025
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13026
- let fixedPositions;
13027
- if (fixedWidth != null && visibleGroups.length > 0) {
13028
- fixedPositions = /* @__PURE__ */ new Map();
13029
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13030
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13031
- let cx = (fixedWidth - totalW) / 2;
13032
- for (const g of visibleGroups) {
13033
- fixedPositions.set(g.name, cx);
13034
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13035
- }
13036
- }
13037
- for (const group of visibleGroups) {
13038
- const isActive = activeTagGroup != null;
13039
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13040
- const gX = fixedPositions?.get(group.name) ?? group.x;
13041
- const gY = fixedPositions ? 0 : group.y;
13042
- const legendG = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "sitemap-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13043
- if (isActive) {
13044
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13045
- }
13046
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13047
- const pillYOff = LEGEND_CAPSULE_PAD;
13048
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13049
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
13050
- if (isActive) {
13051
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13052
- }
13053
- legendG.append("text").attr("x", pillXOff + pillW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13054
- if (isActive && fixedWidth != null) {
13055
- const groupKey = group.name.toLowerCase();
13709
+ const groups = legendGroups.map((g) => ({
13710
+ name: g.name,
13711
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13712
+ }));
13713
+ const isFixedMode = fixedWidth != null;
13714
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13715
+ const legendConfig = {
13716
+ groups,
13717
+ position: { placement: "top-center", titleRelation: "below-title" },
13718
+ mode: "fixed",
13719
+ capsulePillAddonWidth: eyeAddonWidth
13720
+ };
13721
+ const legendState = { activeGroup: activeTagGroup ?? null };
13722
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13723
+ const legendHandle = renderLegendD3(
13724
+ parent,
13725
+ legendConfig,
13726
+ legendState,
13727
+ palette,
13728
+ isDark,
13729
+ void 0,
13730
+ containerWidth
13731
+ );
13732
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13733
+ if (isFixedMode) {
13734
+ const computedLayout = legendHandle.getLayout();
13735
+ if (computedLayout.activeCapsule?.addonX != null) {
13736
+ const capsule = computedLayout.activeCapsule;
13737
+ const groupKey = capsule.groupName.toLowerCase();
13056
13738
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13057
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13058
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13059
- const hitPad = 6;
13060
- const eyeG = legendG.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13061
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13062
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13063
- }
13064
- if (isActive) {
13065
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13066
- let entryX = pillXOff + pillW + 4 + eyeShift;
13067
- for (const entry of group.entries) {
13068
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13069
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13070
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13071
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13072
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13739
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13740
+ if (!activeGroupEl.empty()) {
13741
+ const eyeX = capsule.addonX;
13742
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13743
+ const hitPad = 6;
13744
+ const eyeG = activeGroupEl.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13745
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13746
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13073
13747
  }
13074
13748
  }
13075
13749
  }
@@ -13123,7 +13797,7 @@ async function renderSitemapForExport(content, theme, palette) {
13123
13797
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13124
13798
  return injectBranding2(svgHtml, brandColor);
13125
13799
  }
13126
- var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
13800
+ var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator, lineGeneratorLinear;
13127
13801
  var init_renderer2 = __esm({
13128
13802
  "src/sitemap/renderer.ts"() {
13129
13803
  "use strict";
@@ -13132,6 +13806,7 @@ var init_renderer2 = __esm({
13132
13806
  init_fonts();
13133
13807
  init_color_utils();
13134
13808
  init_legend_constants();
13809
+ init_legend_d3();
13135
13810
  init_title_constants();
13136
13811
  DIAGRAM_PADDING2 = 20;
13137
13812
  MAX_SCALE2 = 3;
@@ -13155,6 +13830,7 @@ var init_renderer2 = __esm({
13155
13830
  COLLAPSE_BAR_HEIGHT2 = 6;
13156
13831
  LEGEND_FIXED_GAP2 = 8;
13157
13832
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
13833
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13158
13834
  }
13159
13835
  });
13160
13836
 
@@ -13431,53 +14107,22 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13431
14107
  }
13432
14108
  if (parsed.tagGroups.length > 0) {
13433
14109
  const legendY = height - LEGEND_HEIGHT;
13434
- let legendX = DIAGRAM_PADDING3;
13435
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13436
- const capsulePad = LEGEND_CAPSULE_PAD;
13437
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13438
- if (activeTagGroup) {
13439
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13440
- }
13441
- for (const group of parsed.tagGroups) {
13442
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13443
- if (activeTagGroup != null && !isActive) continue;
13444
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13445
- const pillWidth2 = pillTextWidth + 16;
13446
- let capsuleContentWidth = pillWidth2;
13447
- if (isActive) {
13448
- capsuleContentWidth += 4;
13449
- for (const entry of group.entries) {
13450
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13451
- }
13452
- }
13453
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13454
- if (isActive) {
13455
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13456
- }
13457
- const pillX = legendX + (isActive ? capsulePad : 0);
13458
- const pillBg = isActive ? palette.bg : groupBg;
13459
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", pillBg).attr("class", "kanban-legend-group").attr("data-legend-group", group.name.toLowerCase());
13460
- if (isActive) {
13461
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13462
- }
13463
- legendContainer.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13464
- if (isActive) {
13465
- let entryX = pillX + pillWidth2 + 4;
13466
- for (const entry of group.entries) {
13467
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13468
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13469
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13470
- entryG.append("text").attr("x", entryTextX).attr(
13471
- "y",
13472
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13473
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13474
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13475
- }
13476
- legendX += capsuleWidth + 12;
13477
- } else {
13478
- legendX += pillWidth2 + 12;
13479
- }
13480
- }
14110
+ const legendConfig = {
14111
+ groups: parsed.tagGroups,
14112
+ position: { placement: "top-center", titleRelation: "below-title" },
14113
+ mode: exportDims ? "inline" : "fixed"
14114
+ };
14115
+ const legendState = { activeGroup: activeTagGroup ?? null };
14116
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${DIAGRAM_PADDING3},${legendY})`);
14117
+ renderLegendD3(
14118
+ legendG,
14119
+ legendConfig,
14120
+ legendState,
14121
+ palette,
14122
+ isDark,
14123
+ void 0,
14124
+ width - DIAGRAM_PADDING3 * 2
14125
+ );
13481
14126
  }
13482
14127
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13483
14128
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13573,6 +14218,7 @@ var init_renderer3 = __esm({
13573
14218
  init_parser5();
13574
14219
  init_mutations();
13575
14220
  init_legend_constants();
14221
+ init_legend_d3();
13576
14222
  init_title_constants();
13577
14223
  DIAGRAM_PADDING3 = 20;
13578
14224
  COLUMN_GAP = 16;
@@ -13766,14 +14412,9 @@ function collectClassTypes(parsed) {
13766
14412
  if (c.color) continue;
13767
14413
  present.add(c.modifier ?? "class");
13768
14414
  }
13769
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13770
- }
13771
- function legendEntriesWidth(entries) {
13772
- let w = 0;
13773
- for (const e of entries) {
13774
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13775
- }
13776
- return w;
14415
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14416
+ (k) => CLASS_TYPE_MAP[k]
14417
+ );
13777
14418
  }
13778
14419
  function classTypeKey(modifier) {
13779
14420
  return modifier ?? "class";
@@ -13842,7 +14483,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13842
14483
  defs.append("marker").attr("id", "cd-arrow-depend").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13843
14484
  defs.append("marker").attr("id", "cd-arrow-assoc").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13844
14485
  if (parsed.title) {
13845
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
14486
+ const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style(
14487
+ "cursor",
14488
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14489
+ ).text(parsed.title);
13846
14490
  if (parsed.titleLineNumber) {
13847
14491
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13848
14492
  if (onClickItem) {
@@ -13856,32 +14500,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13856
14500
  }
13857
14501
  const isLegendExpanded = legendActive !== false;
13858
14502
  if (hasLegend) {
13859
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13860
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13861
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13862
- const entriesW = legendEntriesWidth(legendEntries);
13863
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13864
- const legendX = (width - totalW) / 2;
13865
- const legendY = titleHeight;
13866
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13867
- if (isLegendExpanded) {
13868
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13869
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
13870
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13871
- legendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13872
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13873
- for (const entry of legendEntries) {
13874
- const color = palette.colors[entry.colorKey];
13875
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13876
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13877
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13878
- entryG.append("text").attr("x", entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.label);
13879
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14503
+ const legendGroups = [
14504
+ {
14505
+ name: LEGEND_GROUP_NAME,
14506
+ entries: legendEntries.map((entry) => ({
14507
+ value: entry.label,
14508
+ color: palette.colors[entry.colorKey]
14509
+ }))
13880
14510
  }
13881
- } else {
13882
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13883
- legendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13884
- }
14511
+ ];
14512
+ const legendConfig = {
14513
+ groups: legendGroups,
14514
+ position: { placement: "top-center", titleRelation: "below-title" },
14515
+ mode: "fixed"
14516
+ };
14517
+ const legendState = {
14518
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14519
+ };
14520
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14521
+ renderLegendD3(
14522
+ legendG,
14523
+ legendConfig,
14524
+ legendState,
14525
+ palette,
14526
+ isDark,
14527
+ void 0,
14528
+ width
14529
+ );
13885
14530
  }
13886
14531
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13887
14532
  for (const edge of layout.edges) {
@@ -13925,7 +14570,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13925
14570
  const colorOff = !!parsed.options?.["no-auto-color"];
13926
14571
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
13927
14572
  const effectiveColor = neutralize ? palette.primary : node.color;
13928
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14573
+ const fill2 = nodeFill3(
14574
+ palette,
14575
+ isDark,
14576
+ node.modifier,
14577
+ effectiveColor,
14578
+ colorOff
14579
+ );
13929
14580
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
13930
14581
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", 3).attr("ry", 3).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", NODE_STROKE_WIDTH3);
13931
14582
  let yPos = -h / 2;
@@ -13994,15 +14645,10 @@ function renderClassDiagramForExport(content, theme, palette) {
13994
14645
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
13995
14646
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
13996
14647
  return runInExportContainer(exportWidth, exportHeight, (container) => {
13997
- renderClassDiagram(
13998
- container,
13999
- parsed,
14000
- layout,
14001
- palette,
14002
- isDark,
14003
- void 0,
14004
- { width: exportWidth, height: exportHeight }
14005
- );
14648
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14649
+ width: exportWidth,
14650
+ height: exportHeight
14651
+ });
14006
14652
  return extractExportSvg(container, theme);
14007
14653
  });
14008
14654
  }
@@ -14015,6 +14661,7 @@ var init_renderer4 = __esm({
14015
14661
  init_fonts();
14016
14662
  init_export_container();
14017
14663
  init_legend_constants();
14664
+ init_legend_d3();
14018
14665
  init_title_constants();
14019
14666
  init_color_utils();
14020
14667
  init_parser2();
@@ -14621,35 +15268,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14621
15268
  }
14622
15269
  }
14623
15270
  if (parsed.tagGroups.length > 0) {
14624
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14625
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14626
- const LEGEND_GAP = 8;
14627
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14628
- if (activeTagGroup) {
14629
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14630
- }
14631
- let legendX = DIAGRAM_PADDING5;
14632
15271
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14633
- for (const group of parsed.tagGroups) {
14634
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14635
- const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
14636
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14637
- legendX += labelWidth;
14638
- for (const entry of group.entries) {
14639
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14640
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14641
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14642
- tmpText.remove();
14643
- const pillW = textW + LEGEND_PILL_PAD * 2;
14644
- pillG.append("rect").attr("x", legendX).attr("y", legendY).attr("width", pillW).attr("height", LEGEND_PILL_H).attr("rx", LEGEND_PILL_RX).attr("ry", LEGEND_PILL_RX).attr(
14645
- "fill",
14646
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14647
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14648
- pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14649
- legendX += pillW + LEGEND_GAP;
14650
- }
14651
- legendX += LEGEND_GROUP_GAP;
14652
- }
15272
+ const legendConfig = {
15273
+ groups: parsed.tagGroups,
15274
+ position: { placement: "top-center", titleRelation: "below-title" },
15275
+ mode: "fixed"
15276
+ };
15277
+ const legendState = { activeGroup: activeTagGroup ?? null };
15278
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15279
+ renderLegendD3(
15280
+ legendG,
15281
+ legendConfig,
15282
+ legendState,
15283
+ palette,
15284
+ isDark,
15285
+ void 0,
15286
+ viewW
15287
+ );
15288
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14653
15289
  }
14654
15290
  if (semanticRoles) {
14655
15291
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14659,55 +15295,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14659
15295
  return false;
14660
15296
  });
14661
15297
  if (presentRoles.length > 0) {
14662
- const measureLabelW = (text, fontSize) => {
14663
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14664
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14665
- dummy.remove();
14666
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14667
- };
14668
- const labelWidths = /* @__PURE__ */ new Map();
14669
- for (const role of presentRoles) {
14670
- labelWidths.set(
14671
- role,
14672
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14673
- );
14674
- }
14675
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14676
- const groupName = "Role";
14677
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14678
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14679
- let totalWidth;
14680
- let entriesWidth2 = 0;
14681
- if (semanticActive) {
14682
- for (const role of presentRoles) {
14683
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14684
- }
14685
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14686
- } else {
14687
- totalWidth = pillWidth2;
14688
- }
14689
- const legendX = (viewW - totalWidth) / 2;
14690
15298
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14691
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14692
- if (semanticActive) {
14693
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14694
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
14695
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
14696
- semanticLegendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14697
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14698
- for (const role of presentRoles) {
14699
- const label = ROLE_LABELS[role];
14700
- const roleColor = palette.colors[ROLE_COLORS[role]];
14701
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14702
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14703
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14704
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(label);
14705
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15299
+ const semanticGroups = [
15300
+ {
15301
+ name: "Role",
15302
+ entries: presentRoles.map((role) => ({
15303
+ value: ROLE_LABELS[role],
15304
+ color: palette.colors[ROLE_COLORS[role]]
15305
+ }))
14706
15306
  }
14707
- } else {
14708
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14709
- semanticLegendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14710
- }
15307
+ ];
15308
+ const legendConfig = {
15309
+ groups: semanticGroups,
15310
+ position: { placement: "top-center", titleRelation: "below-title" },
15311
+ mode: "fixed"
15312
+ };
15313
+ const legendState = {
15314
+ activeGroup: semanticActive ? "Role" : null
15315
+ };
15316
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15317
+ renderLegendD3(
15318
+ legendG,
15319
+ legendConfig,
15320
+ legendState,
15321
+ palette,
15322
+ isDark,
15323
+ void 0,
15324
+ viewW
15325
+ );
15326
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14711
15327
  }
14712
15328
  }
14713
15329
  }
@@ -14752,6 +15368,7 @@ var init_renderer5 = __esm({
14752
15368
  init_palettes();
14753
15369
  init_tag_groups();
14754
15370
  init_legend_constants();
15371
+ init_legend_d3();
14755
15372
  init_title_constants();
14756
15373
  init_parser3();
14757
15374
  init_layout4();
@@ -14775,6 +15392,17 @@ var layout_exports5 = {};
14775
15392
  __export(layout_exports5, {
14776
15393
  layoutBoxesAndLines: () => layoutBoxesAndLines
14777
15394
  });
15395
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15396
+ const dx = tx - cx;
15397
+ const dy = ty - cy;
15398
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15399
+ const hw = w / 2;
15400
+ const hh = h / 2;
15401
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15402
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15403
+ const s = Math.min(sx, sy);
15404
+ return { x: cx + dx * s, y: cy + dy * s };
15405
+ }
14778
15406
  function computeNodeSize(_node) {
14779
15407
  const PHI = 1.618;
14780
15408
  const NODE_HEIGHT = 60;
@@ -14927,13 +15555,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14927
15555
  const srcNode = g.node(edge.source);
14928
15556
  const tgtNode = g.node(edge.target);
14929
15557
  if (!srcNode || !tgtNode) continue;
14930
- const midX = (srcNode.x + tgtNode.x) / 2;
14931
- const midY = (srcNode.y + tgtNode.y) / 2;
14932
- points = [
14933
- { x: srcNode.x, y: srcNode.y },
14934
- { x: midX, y: midY },
14935
- { x: tgtNode.x, y: tgtNode.y }
14936
- ];
15558
+ const srcPt = clipToRectBorder2(
15559
+ srcNode.x,
15560
+ srcNode.y,
15561
+ srcNode.width,
15562
+ srcNode.height,
15563
+ tgtNode.x,
15564
+ tgtNode.y
15565
+ );
15566
+ const tgtPt = clipToRectBorder2(
15567
+ tgtNode.x,
15568
+ tgtNode.y,
15569
+ tgtNode.width,
15570
+ tgtNode.height,
15571
+ srcNode.x,
15572
+ srcNode.y
15573
+ );
15574
+ const midX = (srcPt.x + tgtPt.x) / 2;
15575
+ const midY = (srcPt.y + tgtPt.y) / 2;
15576
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
14937
15577
  } else {
14938
15578
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
14939
15579
  points = dagreEdge?.points ?? [];
@@ -14956,7 +15596,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14956
15596
  labelY,
14957
15597
  yOffset: edgeYOffsets[i],
14958
15598
  parallelCount: edgeParallelCounts[i],
14959
- metadata: edge.metadata
15599
+ metadata: edge.metadata,
15600
+ deferred: deferredSet.has(i) || void 0
14960
15601
  });
14961
15602
  }
14962
15603
  let maxX = 0;
@@ -15225,12 +15866,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15225
15866
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15226
15867
  edgeGroups.set(i, edgeG);
15227
15868
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15228
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15229
- "d",
15230
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15231
- points
15232
- ) ?? ""
15233
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15869
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
15870
+ const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15234
15871
  if (le.bidirectional) {
15235
15872
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15236
15873
  path.attr("marker-start", `url(#${revId})`);
@@ -15311,50 +15948,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15311
15948
  }
15312
15949
  }
15313
15950
  if (parsed.tagGroups.length > 0) {
15314
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15315
- }
15316
- }
15317
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15318
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15319
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15320
- let totalW = 0;
15321
- for (const tg of parsed.tagGroups) {
15322
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15323
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15324
- if (isActive) {
15325
- totalW += 6;
15326
- for (const entry of tg.entries) {
15327
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15328
- }
15329
- }
15330
- totalW += LEGEND_GROUP_GAP;
15331
- }
15332
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15333
- const legendY = titleOffset + 4;
15334
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15335
- let x = 0;
15336
- for (const tg of parsed.tagGroups) {
15337
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15338
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15339
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15340
- const tagPill = groupG.append("rect").attr("x", x).attr("y", 0).attr("width", nameW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
15341
- if (isActiveGroup) {
15342
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15343
- }
15344
- groupG.append("text").attr("x", x + nameW / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", isActiveGroup ? palette.text : palette.textMuted).attr("pointer-events", "none").text(tg.name);
15345
- x += nameW;
15346
- if (isActiveGroup) {
15347
- x += 6;
15348
- for (const entry of tg.entries) {
15349
- const entryColor = entry.color || palette.textMuted;
15350
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15351
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15352
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15353
- entryG.append("text").attr("x", x + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
15354
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15355
- }
15356
- }
15357
- x += LEGEND_GROUP_GAP;
15951
+ const legendConfig = {
15952
+ groups: parsed.tagGroups,
15953
+ position: { placement: "top-center", titleRelation: "below-title" },
15954
+ mode: "fixed"
15955
+ };
15956
+ const legendState = { activeGroup };
15957
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
15958
+ renderLegendD3(
15959
+ legendG,
15960
+ legendConfig,
15961
+ legendState,
15962
+ palette,
15963
+ isDark,
15964
+ void 0,
15965
+ width
15966
+ );
15967
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15358
15968
  }
15359
15969
  }
15360
15970
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15362,7 +15972,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15362
15972
  exportDims: options?.exportDims
15363
15973
  });
15364
15974
  }
15365
- var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
15975
+ var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB, lineGeneratorLinear2;
15366
15976
  var init_renderer6 = __esm({
15367
15977
  "src/boxes-and-lines/renderer.ts"() {
15368
15978
  "use strict";
@@ -15370,6 +15980,7 @@ var init_renderer6 = __esm({
15370
15980
  d3Shape4 = __toESM(require("d3-shape"), 1);
15371
15981
  init_fonts();
15372
15982
  init_legend_constants();
15983
+ init_legend_d3();
15373
15984
  init_title_constants();
15374
15985
  init_color_utils();
15375
15986
  init_tag_groups();
@@ -15390,6 +16001,7 @@ var init_renderer6 = __esm({
15390
16001
  GROUP_LABEL_FONT_SIZE = 14;
15391
16002
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15392
16003
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
16004
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15393
16005
  }
15394
16006
  });
15395
16007
 
@@ -17296,7 +17908,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17296
17908
  if (activeTagGroup) {
17297
17909
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17298
17910
  }
17299
- renderLegend3(
17911
+ renderLegend2(
17300
17912
  legendParent,
17301
17913
  layout,
17302
17914
  palette,
@@ -17657,52 +18269,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17657
18269
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17658
18270
  }
17659
18271
  }
17660
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17661
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17662
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17663
- ) : layout.legend;
17664
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17665
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17666
- let fixedPositions = null;
17667
- if (fixedWidth != null && visibleGroups.length > 0) {
17668
- fixedPositions = /* @__PURE__ */ new Map();
17669
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17670
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17671
- for (const g of visibleGroups) {
17672
- fixedPositions.set(g.name, cx);
17673
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17674
- }
17675
- }
17676
- for (const group of visibleGroups) {
17677
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17678
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17679
- const pillLabel = group.name;
17680
- const pillWidth2 = pillWidthOf(group);
17681
- const gX = fixedPositions?.get(group.name) ?? group.x;
17682
- const gY = fixedPositions != null ? 0 : group.y;
17683
- const gEl = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
17684
- if (isActive) {
17685
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17686
- }
17687
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17688
- const pillY = LEGEND_CAPSULE_PAD;
17689
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17690
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
17691
- if (isActive) {
17692
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
17693
- }
17694
- gEl.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
17695
- if (isActive) {
17696
- let entryX = pillX + pillWidth2 + 4;
17697
- for (const entry of group.entries) {
17698
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17699
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17700
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17701
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
17702
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17703
- }
17704
- }
17705
- }
18272
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18273
+ const groups = layout.legend.map((g) => ({
18274
+ name: g.name,
18275
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18276
+ }));
18277
+ const legendConfig = {
18278
+ groups,
18279
+ position: { placement: "top-center", titleRelation: "below-title" },
18280
+ mode: "fixed"
18281
+ };
18282
+ const legendState = { activeGroup: activeTagGroup ?? null };
18283
+ const containerWidth = fixedWidth ?? layout.width;
18284
+ renderLegendD3(
18285
+ parent,
18286
+ legendConfig,
18287
+ legendState,
18288
+ palette,
18289
+ isDark,
18290
+ void 0,
18291
+ containerWidth
18292
+ );
18293
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17706
18294
  }
17707
18295
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17708
18296
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -17913,7 +18501,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
17913
18501
  if (activeTagGroup) {
17914
18502
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17915
18503
  }
17916
- renderLegend3(
18504
+ renderLegend2(
17917
18505
  legendParent,
17918
18506
  layout,
17919
18507
  palette,
@@ -18043,6 +18631,7 @@ var init_renderer7 = __esm({
18043
18631
  init_parser6();
18044
18632
  init_layout6();
18045
18633
  init_legend_constants();
18634
+ init_legend_d3();
18046
18635
  init_title_constants();
18047
18636
  DIAGRAM_PADDING7 = 20;
18048
18637
  MAX_SCALE5 = 3;
@@ -20930,17 +21519,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20930
21519
  color: r.color,
20931
21520
  key: r.name.toLowerCase().replace(/\s+/g, "-")
20932
21521
  }));
20933
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20934
- let entriesWidth2 = 0;
21522
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21523
+ let entriesWidth3 = 0;
20935
21524
  for (const e of entries) {
20936
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21525
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20937
21526
  }
20938
21527
  groups.push({
20939
21528
  name: "Capabilities",
20940
21529
  type: "role",
20941
21530
  entries,
20942
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20943
- minifiedWidth: pillWidth2
21531
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21532
+ minifiedWidth: pillWidth3
20944
21533
  });
20945
21534
  }
20946
21535
  for (const tg of tagGroups) {
@@ -20955,113 +21544,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20955
21544
  }
20956
21545
  }
20957
21546
  if (entries.length === 0) continue;
20958
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20959
- let entriesWidth2 = 0;
21547
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21548
+ let entriesWidth3 = 0;
20960
21549
  for (const e of entries) {
20961
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21550
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20962
21551
  }
20963
21552
  groups.push({
20964
21553
  name: tg.name,
20965
21554
  type: "tag",
20966
21555
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
20967
21556
  entries,
20968
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20969
- minifiedWidth: pillWidth2
21557
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21558
+ minifiedWidth: pillWidth3
20970
21559
  });
20971
21560
  }
20972
21561
  return groups;
20973
21562
  }
20974
- function computePlaybackWidth(playback) {
20975
- if (!playback) return 0;
20976
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20977
- if (!playback.expanded) return pillWidth2;
20978
- let entriesW = 8;
20979
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
20980
- for (const s of playback.speedOptions) {
20981
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
20982
- }
20983
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
20984
- }
20985
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21563
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
20986
21564
  if (legendGroups.length === 0 && !playback) return;
20987
21565
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
20988
21566
  if (activeGroup) {
20989
21567
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
20990
21568
  }
20991
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
20992
- const playbackW = computePlaybackWidth(playback);
20993
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
20994
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
20995
- let cursorX = (totalWidth - totalLegendW) / 2;
21569
+ const allGroups = legendGroups.map((g) => ({
21570
+ name: g.name,
21571
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21572
+ }));
21573
+ if (playback) {
21574
+ allGroups.push({ name: "Playback", entries: [] });
21575
+ }
21576
+ const legendConfig = {
21577
+ groups: allGroups,
21578
+ position: { placement: "top-center", titleRelation: "below-title" },
21579
+ mode: "fixed",
21580
+ showEmptyGroups: true
21581
+ };
21582
+ const legendState = { activeGroup };
21583
+ renderLegendD3(
21584
+ legendG,
21585
+ legendConfig,
21586
+ legendState,
21587
+ palette,
21588
+ isDark,
21589
+ void 0,
21590
+ totalWidth
21591
+ );
21592
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
20996
21593
  for (const group of legendGroups) {
20997
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
20998
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20999
- const pillLabel = group.name;
21000
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21001
- const gEl = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
21002
- if (isActive) {
21003
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21004
- }
21005
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
21006
- const pillYOff = LEGEND_CAPSULE_PAD;
21007
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
21008
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
21009
- if (isActive) {
21010
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
21011
- }
21012
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
21013
- if (isActive) {
21014
- let entryX = pillXOff + pillWidth2 + 4;
21015
- for (const entry of group.entries) {
21016
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21594
+ const groupKey = group.name.toLowerCase();
21595
+ for (const entry of group.entries) {
21596
+ const entryEl = legendG.select(
21597
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21598
+ );
21599
+ if (!entryEl.empty()) {
21600
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21017
21601
  "data-legend-tag-group",
21018
21602
  group.type === "tag" ? group.tagKey ?? "" : null
21019
- ).style("cursor", "pointer");
21020
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21021
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21022
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
21023
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21603
+ );
21024
21604
  }
21025
21605
  }
21026
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21027
21606
  }
21028
- if (playback) {
21029
- const isExpanded = playback.expanded;
21030
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21031
- const pillLabel = "Playback";
21032
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21033
- const fullW = computePlaybackWidth(playback);
21034
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21035
- if (isExpanded) {
21036
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21037
- }
21038
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21039
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21040
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21041
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isExpanded ? palette.bg : groupBg);
21042
- if (isExpanded) {
21043
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
21044
- }
21045
- pbG.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isExpanded ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
21046
- if (isExpanded) {
21047
- let entryX = pillXOff + pillWidth2 + 8;
21048
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21049
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21050
- pbG.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21051
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21052
- for (const s of playback.speedOptions) {
21053
- const label = `${s}x`;
21054
- const isActive = playback.speed === s;
21055
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21056
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21057
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21058
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21059
- speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isActive ? palette.primary : "transparent");
21060
- speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21061
- entryX += slotW + SPEED_BADGE_GAP;
21062
- }
21063
- }
21064
- cursorX += fullW + LEGEND_GROUP_GAP;
21607
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21608
+ if (!playbackEl.empty()) {
21609
+ playbackEl.classed("infra-playback-pill", true);
21610
+ }
21611
+ if (playback && playback.expanded && !playbackEl.empty()) {
21612
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21613
+ let entryX = pillWidth3 + 8;
21614
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21615
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21616
+ playbackEl.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21617
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21618
+ for (const s of playback.speedOptions) {
21619
+ const label = `${s}x`;
21620
+ const isSpeedActive = playback.speed === s;
21621
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21622
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21623
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21624
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21625
+ speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isSpeedActive ? palette.primary : "transparent");
21626
+ speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isSpeedActive ? "600" : "400").attr("fill", isSpeedActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21627
+ entryX += slotW + SPEED_BADGE_GAP;
21628
+ }
21065
21629
  }
21066
21630
  }
21067
21631
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21192,7 +21756,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21192
21756
  "viewBox",
21193
21757
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21194
21758
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21195
- renderLegend4(
21759
+ renderLegend3(
21196
21760
  legendSvg,
21197
21761
  legendGroups,
21198
21762
  containerWidth,
@@ -21204,7 +21768,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21204
21768
  );
21205
21769
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21206
21770
  } else {
21207
- renderLegend4(
21771
+ renderLegend3(
21208
21772
  rootSvg,
21209
21773
  legendGroups,
21210
21774
  totalWidth,
@@ -21238,6 +21802,7 @@ var init_renderer8 = __esm({
21238
21802
  init_compute();
21239
21803
  init_layout8();
21240
21804
  init_legend_constants();
21805
+ init_legend_d3();
21241
21806
  init_title_constants();
21242
21807
  NODE_FONT_SIZE3 = 13;
21243
21808
  META_FONT_SIZE5 = 10;
@@ -22863,7 +23428,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22863
23428
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22864
23429
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22865
23430
  const showIcon = !legendViewMode && tagGroups.length > 0;
22866
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23431
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22867
23432
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22868
23433
  let groupW = pillW;
22869
23434
  if (isActive) {
@@ -22890,83 +23455,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22890
23455
  const legendX = (containerWidth - totalW) / 2;
22891
23456
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22892
23457
  let cursorX = 0;
22893
- for (let i = 0; i < visibleGroups.length; i++) {
22894
- const group = visibleGroups[i];
22895
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22896
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23458
+ if (visibleGroups.length > 0) {
22897
23459
  const showIcon = !legendViewMode && tagGroups.length > 0;
22898
23460
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
22899
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22900
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
22901
- const groupW = groupWidths[i];
22902
- const gEl = legendRow.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "gantt-tag-legend-group").attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber)).style("cursor", "pointer").on("click", () => {
22903
- if (onToggle) onToggle(group.name);
23461
+ const legendGroups = visibleGroups.map((g) => {
23462
+ const key = g.name.toLowerCase();
23463
+ const entries = filteredEntries.get(key) ?? g.entries;
23464
+ return {
23465
+ name: g.name,
23466
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23467
+ };
22904
23468
  });
22905
- if (isActive) {
22906
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
22907
- }
22908
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
22909
- const pillYOff = LEGEND_CAPSULE_PAD;
22910
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
22911
- if (isActive) {
22912
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
22913
- }
22914
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
22915
- gEl.append("text").attr("x", pillXOff + textW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("text-anchor", "middle").attr("font-size", `${LEGEND_PILL_FONT_SIZE}px`).attr("font-weight", "500").attr("fill", isActive || isSwimlane ? palette.text : palette.textMuted).text(group.name);
22916
- if (showIcon) {
22917
- const iconX = pillXOff + textW + 3;
22918
- const iconY = (LEGEND_HEIGHT - 10) / 2;
22919
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
22920
- iconEl.append("title").text(`Group by ${group.name}`);
22921
- iconEl.style("cursor", "pointer").on("click", (event) => {
22922
- event.stopPropagation();
22923
- if (onSwimlaneChange) {
22924
- onSwimlaneChange(
22925
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
22926
- );
22927
- }
22928
- });
22929
- }
22930
- if (isActive) {
22931
- const tagKey = group.name.toLowerCase();
22932
- const entries = filteredEntries.get(tagKey) ?? group.entries;
22933
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
22934
- for (const entry of entries) {
22935
- const entryValue = entry.value.toLowerCase();
22936
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
22937
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
22938
- entryG.append("text").attr("x", ex + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 2).attr("text-anchor", "start").attr("font-size", `${LEGEND_ENTRY_FONT_SIZE}px`).attr("fill", palette.textMuted).text(entry.value);
22939
- entryG.on("mouseenter", () => {
23469
+ const legendConfig = {
23470
+ groups: legendGroups,
23471
+ position: {
23472
+ placement: "top-center",
23473
+ titleRelation: "below-title"
23474
+ },
23475
+ mode: "fixed",
23476
+ capsulePillAddonWidth: iconReserve
23477
+ };
23478
+ const legendState = { activeGroup: activeGroupName };
23479
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23480
+ const tagGroupG = legendRow.append("g");
23481
+ const legendCallbacks = {
23482
+ onGroupToggle: onToggle,
23483
+ onEntryHover: (groupName, entryValue) => {
23484
+ const tagKey = groupName.toLowerCase();
23485
+ if (entryValue) {
23486
+ const ev = entryValue.toLowerCase();
22940
23487
  chartG.selectAll(".gantt-task").each(function() {
22941
23488
  const el = d3Selection10.select(this);
22942
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22943
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23489
+ el.attr(
23490
+ "opacity",
23491
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23492
+ );
22944
23493
  });
22945
23494
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
22946
23495
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
22947
23496
  svg.selectAll(".gantt-task-label").each(function() {
22948
23497
  const el = d3Selection10.select(this);
22949
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22950
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23498
+ el.attr(
23499
+ "opacity",
23500
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23501
+ );
22951
23502
  });
22952
23503
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
22953
23504
  svg.selectAll(".gantt-lane-header").each(function() {
22954
23505
  const el = d3Selection10.select(this);
22955
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22956
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23506
+ el.attr(
23507
+ "opacity",
23508
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23509
+ );
22957
23510
  });
22958
23511
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
22959
- }).on("mouseleave", () => {
23512
+ } else {
22960
23513
  if (criticalPathActive) {
22961
23514
  applyCriticalPathHighlight(svg, chartG);
22962
23515
  } else {
22963
23516
  resetHighlightAll(svg, chartG);
22964
23517
  }
22965
- });
22966
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23518
+ }
23519
+ },
23520
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23521
+ const group = visibleGroups.find((g) => g.name === groupName);
23522
+ if (group) {
23523
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23524
+ }
23525
+ if (showIcon && _isActive) {
23526
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23527
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23528
+ const pillXOff = LEGEND_CAPSULE_PAD;
23529
+ const iconX = pillXOff + textW + 3;
23530
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23531
+ const iconEl = drawSwimlaneIcon(
23532
+ groupEl,
23533
+ iconX,
23534
+ iconY,
23535
+ isSwimlane,
23536
+ palette
23537
+ );
23538
+ iconEl.append("title").text(`Group by ${groupName}`);
23539
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23540
+ event.stopPropagation();
23541
+ if (onSwimlaneChange) {
23542
+ onSwimlaneChange(
23543
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23544
+ );
23545
+ }
23546
+ });
23547
+ }
22967
23548
  }
23549
+ };
23550
+ renderLegendD3(
23551
+ tagGroupG,
23552
+ legendConfig,
23553
+ legendState,
23554
+ palette,
23555
+ isDark,
23556
+ legendCallbacks,
23557
+ tagGroupsW
23558
+ );
23559
+ for (let i = 0; i < visibleGroups.length; i++) {
23560
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
22968
23561
  }
22969
- cursorX += groupW + LEGEND_GROUP_GAP;
22970
23562
  }
22971
23563
  if (hasCriticalPath) {
22972
23564
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23595,6 +24187,7 @@ var init_renderer9 = __esm({
23595
24187
  init_tag_groups();
23596
24188
  init_d3();
23597
24189
  init_legend_constants();
24190
+ init_legend_d3();
23598
24191
  init_title_constants();
23599
24192
  BAR_H = 22;
23600
24193
  ROW_GAP = 6;
@@ -24764,57 +25357,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24764
25357
  }
24765
25358
  if (parsed.tagGroups.length > 0) {
24766
25359
  const legendY = TOP_MARGIN + titleOffset;
24767
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24768
- const legendItems = [];
24769
- for (const tg of parsed.tagGroups) {
24770
- if (tg.entries.length === 0) continue;
24771
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24772
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24773
- const entries = tg.entries.map((e) => ({
25360
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25361
+ name: tg.name,
25362
+ entries: tg.entries.map((e) => ({
24774
25363
  value: e.value,
24775
25364
  color: resolveColor(e.color) ?? e.color
24776
- }));
24777
- let totalWidth2 = pillWidth2;
24778
- if (isActive) {
24779
- let entriesWidth2 = 0;
24780
- for (const entry of entries) {
24781
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24782
- }
24783
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24784
- }
24785
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24786
- }
24787
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24788
- let legendX = (svgWidth - totalLegendWidth) / 2;
24789
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24790
- if (activeTagGroup) {
24791
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24792
- }
24793
- for (const item of legendItems) {
24794
- const gEl = legendContainer.append("g").attr("transform", `translate(${legendX}, ${legendY})`).attr("class", "sequence-legend-group").attr("data-legend-group", item.group.name.toLowerCase()).style("cursor", "pointer");
24795
- if (item.isActive) {
24796
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24797
- }
24798
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24799
- const pillYOff = LEGEND_CAPSULE_PAD;
24800
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24801
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", item.isActive ? palette.bg : groupBg);
24802
- if (item.isActive) {
24803
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
24804
- }
24805
- gEl.append("text").attr("x", pillXOff + item.pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", item.isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(item.group.name);
24806
- if (item.isActive) {
24807
- let entryX = pillXOff + item.pillWidth + 4;
24808
- for (const entry of item.entries) {
24809
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24810
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24811
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24812
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
24813
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24814
- }
24815
- }
24816
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24817
- }
25365
+ }))
25366
+ }));
25367
+ const legendConfig = {
25368
+ groups: resolvedGroups,
25369
+ position: { placement: "top-center", titleRelation: "below-title" },
25370
+ mode: "fixed"
25371
+ };
25372
+ const legendState = { activeGroup: activeTagGroup ?? null };
25373
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25374
+ renderLegendD3(
25375
+ legendG,
25376
+ legendConfig,
25377
+ legendState,
25378
+ palette,
25379
+ isDark,
25380
+ void 0,
25381
+ svgWidth
25382
+ );
24818
25383
  }
24819
25384
  for (const group of groups) {
24820
25385
  if (group.participantIds.length === 0) continue;
@@ -25374,6 +25939,7 @@ var init_renderer10 = __esm({
25374
25939
  init_parser();
25375
25940
  init_tag_resolution();
25376
25941
  init_legend_constants();
25942
+ init_legend_d3();
25377
25943
  init_title_constants();
25378
25944
  PARTICIPANT_GAP = 160;
25379
25945
  PARTICIPANT_BOX_WIDTH = 120;
@@ -28029,7 +28595,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28029
28595
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28030
28596
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28031
28597
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28032
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28033
28598
  const LG_ICON_W = 20;
28034
28599
  const mainSvg = d3Selection13.select(container).select("svg");
28035
28600
  const mainG = mainSvg.select("g");
@@ -28068,11 +28633,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28068
28633
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28069
28634
  ) : legendGroups;
28070
28635
  if (visibleGroups.length === 0) return;
28071
- const totalW = visibleGroups.reduce((s, lg) => {
28072
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28073
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28074
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28075
- let cx = (width - totalW) / 2;
28076
28636
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28077
28637
  if (currentActiveGroup) {
28078
28638
  legendContainer.attr(
@@ -28080,82 +28640,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28080
28640
  currentActiveGroup.toLowerCase()
28081
28641
  );
28082
28642
  }
28083
- for (const lg of visibleGroups) {
28084
- const groupKey = lg.group.name.toLowerCase();
28085
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28086
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28087
- const pillLabel = lg.group.name;
28088
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28089
- const gEl = legendContainer.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", groupKey).attr("data-tag-group", groupKey).attr("data-legend-entry", "__group__");
28090
- if (!viewMode) {
28091
- gEl.style("cursor", "pointer").on("click", () => {
28092
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28093
- drawLegend2();
28094
- recolorEvents2();
28095
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28096
- });
28097
- }
28098
- if (isActive) {
28099
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28100
- }
28101
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28102
- const pillYOff = LG_CAPSULE_PAD;
28103
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28104
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
28105
- if (isActive) {
28106
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
28107
- }
28108
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2).attr("font-size", LG_PILL_FONT_SIZE).attr("font-weight", "500").attr("font-family", FONT_FAMILY).attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
28109
- if (isActive) {
28110
- let entryX;
28111
- if (!viewMode) {
28112
- const iconX = pillXOff + pillWidth2 + 5;
28643
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28644
+ const centralGroups = visibleGroups.map((lg) => ({
28645
+ name: lg.group.name,
28646
+ entries: lg.group.entries.map((e) => ({
28647
+ value: e.value,
28648
+ color: e.color
28649
+ }))
28650
+ }));
28651
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28652
+ const centralConfig = {
28653
+ groups: centralGroups,
28654
+ position: { placement: "top-center", titleRelation: "below-title" },
28655
+ mode: "fixed",
28656
+ capsulePillAddonWidth: iconAddon
28657
+ };
28658
+ const centralState = { activeGroup: centralActive };
28659
+ const centralCallbacks = viewMode ? {} : {
28660
+ onGroupToggle: (groupName) => {
28661
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28662
+ drawLegend2();
28663
+ recolorEvents2();
28664
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28665
+ },
28666
+ onEntryHover: (groupName, entryValue) => {
28667
+ const tagKey = groupName.toLowerCase();
28668
+ if (entryValue) {
28669
+ const tagVal = entryValue.toLowerCase();
28670
+ fadeToTagValue(mainG, tagKey, tagVal);
28671
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28672
+ const el = d3Selection13.select(this);
28673
+ const ev = el.attr("data-legend-entry");
28674
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28675
+ el.attr(
28676
+ "opacity",
28677
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28678
+ );
28679
+ });
28680
+ } else {
28681
+ fadeReset(mainG);
28682
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28683
+ }
28684
+ },
28685
+ onGroupRendered: (groupName, groupEl, isActive) => {
28686
+ const groupKey = groupName.toLowerCase();
28687
+ groupEl.attr("data-tag-group", groupKey);
28688
+ if (isActive && !viewMode) {
28689
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28690
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28691
+ const pillXOff = LG_CAPSULE_PAD;
28692
+ const iconX = pillXOff + pillWidth3 + 5;
28113
28693
  const iconY = (LG_HEIGHT - 10) / 2;
28114
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28694
+ const iconEl = drawSwimlaneIcon3(
28695
+ groupEl,
28696
+ iconX,
28697
+ iconY,
28698
+ isSwimActive
28699
+ );
28115
28700
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28116
28701
  event.stopPropagation();
28117
28702
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28118
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28703
+ onTagStateChange?.(
28704
+ currentActiveGroup,
28705
+ currentSwimlaneGroup
28706
+ );
28119
28707
  relayout2();
28120
28708
  });
28121
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28122
- } else {
28123
- entryX = pillXOff + pillWidth2 + 8;
28124
- }
28125
- for (const entry of lg.group.entries) {
28126
- const tagKey = lg.group.name.toLowerCase();
28127
- const tagVal = entry.value.toLowerCase();
28128
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28129
- if (!viewMode) {
28130
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28131
- event.stopPropagation();
28132
- fadeToTagValue(mainG, tagKey, tagVal);
28133
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28134
- const el = d3Selection13.select(this);
28135
- const ev = el.attr("data-legend-entry");
28136
- if (ev === "__group__") return;
28137
- const eg = el.attr("data-tag-group");
28138
- el.attr(
28139
- "opacity",
28140
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28141
- );
28142
- });
28143
- }).on("mouseleave", (event) => {
28144
- event.stopPropagation();
28145
- fadeReset(mainG);
28146
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28147
- }).on("click", (event) => {
28148
- event.stopPropagation();
28149
- });
28150
- }
28151
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28152
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28153
- entryG.append("text").attr("x", textX).attr("y", LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LG_ENTRY_FONT_SIZE).attr("font-family", FONT_FAMILY).attr("fill", palette.textMuted).text(entry.value);
28154
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28155
28709
  }
28156
28710
  }
28157
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28158
- }
28711
+ };
28712
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28713
+ renderLegendD3(
28714
+ legendInnerG,
28715
+ centralConfig,
28716
+ centralState,
28717
+ palette,
28718
+ isDark,
28719
+ centralCallbacks,
28720
+ width
28721
+ );
28159
28722
  }, recolorEvents2 = function() {
28160
28723
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28161
28724
  mainG.selectAll(".tl-event").each(function() {
@@ -28180,7 +28743,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28180
28743
  };
28181
28744
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28182
28745
  const legendY = title ? 50 : 10;
28183
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28184
28746
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28185
28747
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28186
28748
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -29427,6 +29989,7 @@ var init_d3 = __esm({
29427
29989
  init_parsing();
29428
29990
  init_tag_groups();
29429
29991
  init_legend_constants();
29992
+ init_legend_d3();
29430
29993
  init_title_constants();
29431
29994
  DEFAULT_CLOUD_OPTIONS = {
29432
29995
  rotate: "none",
@@ -29605,6 +30168,7 @@ __export(index_exports, {
29605
30168
  computeCardMove: () => computeCardMove,
29606
30169
  computeInfra: () => computeInfra,
29607
30170
  computeInfraLegendGroups: () => computeInfraLegendGroups,
30171
+ computeLegendLayout: () => computeLegendLayout,
29608
30172
  computeScatterLabelGraphics: () => computeScatterLabelGraphics,
29609
30173
  computeTimeTicks: () => computeTimeTicks,
29610
30174
  contrastText: () => contrastText,
@@ -29616,6 +30180,7 @@ __export(index_exports, {
29616
30180
  formatDgmoError: () => formatDgmoError,
29617
30181
  getAvailablePalettes: () => getAvailablePalettes,
29618
30182
  getExtendedChartLegendGroups: () => getExtendedChartLegendGroups,
30183
+ getLegendReservedHeight: () => getLegendReservedHeight,
29619
30184
  getPalette: () => getPalette,
29620
30185
  getRenderCategory: () => getRenderCategory,
29621
30186
  getSeriesColors: () => getSeriesColors,
@@ -29704,7 +30269,9 @@ __export(index_exports, {
29704
30269
  renderInfra: () => renderInfra,
29705
30270
  renderKanban: () => renderKanban,
29706
30271
  renderKanbanForExport: () => renderKanbanForExport,
30272
+ renderLegendD3: () => renderLegendD3,
29707
30273
  renderLegendSvg: () => renderLegendSvg,
30274
+ renderLegendSvgFromConfig: () => renderLegendSvgFromConfig,
29708
30275
  renderOrg: () => renderOrg,
29709
30276
  renderOrgForExport: () => renderOrgForExport,
29710
30277
  renderQuadrant: () => renderQuadrant,
@@ -29744,11 +30311,26 @@ async function ensureDom() {
29744
30311
  const { JSDOM } = await import("jsdom");
29745
30312
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29746
30313
  const win = dom.window;
29747
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29748
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29749
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29750
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29751
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30314
+ Object.defineProperty(globalThis, "document", {
30315
+ value: win.document,
30316
+ configurable: true
30317
+ });
30318
+ Object.defineProperty(globalThis, "window", {
30319
+ value: win,
30320
+ configurable: true
30321
+ });
30322
+ Object.defineProperty(globalThis, "navigator", {
30323
+ value: win.navigator,
30324
+ configurable: true
30325
+ });
30326
+ Object.defineProperty(globalThis, "HTMLElement", {
30327
+ value: win.HTMLElement,
30328
+ configurable: true
30329
+ });
30330
+ Object.defineProperty(globalThis, "SVGElement", {
30331
+ value: win.SVGElement,
30332
+ configurable: true
30333
+ });
29752
30334
  }
29753
30335
  async function render(content, options) {
29754
30336
  const theme = options?.theme ?? "light";
@@ -29757,11 +30339,17 @@ async function render(content, options) {
29757
30339
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29758
30340
  const chartType = parseDgmoChartType(content);
29759
30341
  const category = chartType ? getRenderCategory(chartType) : null;
30342
+ const legendExportState = options?.legendState ? {
30343
+ activeTagGroup: options.legendState.activeGroup ?? null,
30344
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30345
+ } : void 0;
29760
30346
  if (category === "data-chart") {
29761
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30347
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30348
+ branding
30349
+ });
29762
30350
  }
29763
30351
  await ensureDom();
29764
- return renderForExport(content, theme, paletteColors, void 0, {
30352
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29765
30353
  branding,
29766
30354
  c4Level: options?.c4Level,
29767
30355
  c4System: options?.c4System,
@@ -30484,6 +31072,8 @@ init_flowchart_renderer();
30484
31072
  init_echarts();
30485
31073
  init_legend_svg();
30486
31074
  init_legend_constants();
31075
+ init_legend_d3();
31076
+ init_legend_layout();
30487
31077
  init_d3();
30488
31078
  init_renderer10();
30489
31079
  init_colors();
@@ -31328,6 +31918,7 @@ init_branding();
31328
31918
  computeCardMove,
31329
31919
  computeInfra,
31330
31920
  computeInfraLegendGroups,
31921
+ computeLegendLayout,
31331
31922
  computeScatterLabelGraphics,
31332
31923
  computeTimeTicks,
31333
31924
  contrastText,
@@ -31339,6 +31930,7 @@ init_branding();
31339
31930
  formatDgmoError,
31340
31931
  getAvailablePalettes,
31341
31932
  getExtendedChartLegendGroups,
31933
+ getLegendReservedHeight,
31342
31934
  getPalette,
31343
31935
  getRenderCategory,
31344
31936
  getSeriesColors,
@@ -31427,7 +32019,9 @@ init_branding();
31427
32019
  renderInfra,
31428
32020
  renderKanban,
31429
32021
  renderKanbanForExport,
32022
+ renderLegendD3,
31430
32023
  renderLegendSvg,
32024
+ renderLegendSvgFromConfig,
31431
32025
  renderOrg,
31432
32026
  renderOrgForExport,
31433
32027
  renderQuadrant,