@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.js CHANGED
@@ -1901,6 +1901,493 @@ var init_legend_constants = __esm({
1901
1901
  }
1902
1902
  });
1903
1903
 
1904
+ // src/utils/legend-layout.ts
1905
+ function pillWidth(name) {
1906
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1907
+ }
1908
+ function entriesWidth(entries) {
1909
+ let w = 0;
1910
+ for (const e of entries) {
1911
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1912
+ }
1913
+ return w;
1914
+ }
1915
+ function entryWidth(value) {
1916
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1917
+ }
1918
+ function controlWidth(control) {
1919
+ let w = CONTROL_PILL_PAD;
1920
+ if (control.label) {
1921
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
1922
+ if (control.icon) w += CONTROL_ICON_GAP;
1923
+ }
1924
+ if (control.icon) w += 14;
1925
+ if (control.children) {
1926
+ for (const child of control.children) {
1927
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
1928
+ }
1929
+ }
1930
+ return w;
1931
+ }
1932
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
1933
+ const pw = pillWidth(name);
1934
+ const maxCapsuleW = containerWidth;
1935
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
1936
+ const ew = entriesWidth(entries);
1937
+ const singleRowW = baseW + ew;
1938
+ if (singleRowW <= maxCapsuleW) {
1939
+ return {
1940
+ width: singleRowW,
1941
+ entryRows: 1,
1942
+ moreCount: 0,
1943
+ visibleEntries: entries.length
1944
+ };
1945
+ }
1946
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
1947
+ let row = 1;
1948
+ let rowX = pw + 4;
1949
+ let visible = 0;
1950
+ for (let i = 0; i < entries.length; i++) {
1951
+ const ew2 = entryWidth(entries[i].value);
1952
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
1953
+ row++;
1954
+ rowX = 0;
1955
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
1956
+ return {
1957
+ width: maxCapsuleW,
1958
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
1959
+ moreCount: entries.length - visible,
1960
+ visibleEntries: visible
1961
+ };
1962
+ }
1963
+ }
1964
+ rowX += ew2;
1965
+ visible++;
1966
+ }
1967
+ return {
1968
+ width: maxCapsuleW,
1969
+ entryRows: row,
1970
+ moreCount: 0,
1971
+ visibleEntries: entries.length
1972
+ };
1973
+ }
1974
+ function computeLegendLayout(config, state, containerWidth) {
1975
+ const { groups, controls: configControls, mode } = config;
1976
+ const isExport = mode === "inline";
1977
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
1978
+ if (isExport && !activeGroupName) {
1979
+ return {
1980
+ height: 0,
1981
+ width: 0,
1982
+ rows: [],
1983
+ controls: [],
1984
+ pills: [],
1985
+ activeCapsule: void 0
1986
+ };
1987
+ }
1988
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
1989
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
1990
+ return {
1991
+ height: 0,
1992
+ width: 0,
1993
+ rows: [],
1994
+ controls: [],
1995
+ pills: [],
1996
+ activeCapsule: void 0
1997
+ };
1998
+ }
1999
+ const controlLayouts = [];
2000
+ let totalControlsW = 0;
2001
+ if (configControls && !isExport) {
2002
+ for (const ctrl of configControls) {
2003
+ const w = controlWidth(ctrl);
2004
+ controlLayouts.push({
2005
+ id: ctrl.id,
2006
+ x: 0,
2007
+ // positioned later
2008
+ y: 0,
2009
+ width: w,
2010
+ height: LEGEND_HEIGHT,
2011
+ icon: ctrl.icon,
2012
+ label: ctrl.label,
2013
+ exportBehavior: ctrl.exportBehavior,
2014
+ children: ctrl.children?.map((c) => ({
2015
+ id: c.id,
2016
+ label: c.label,
2017
+ x: 0,
2018
+ y: 0,
2019
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2020
+ isActive: c.isActive
2021
+ }))
2022
+ });
2023
+ totalControlsW += w + CONTROL_GAP;
2024
+ }
2025
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2026
+ } else if (configControls && isExport) {
2027
+ for (const ctrl of configControls) {
2028
+ if (ctrl.exportBehavior === "strip") continue;
2029
+ const w = controlWidth(ctrl);
2030
+ controlLayouts.push({
2031
+ id: ctrl.id,
2032
+ x: 0,
2033
+ y: 0,
2034
+ width: w,
2035
+ height: LEGEND_HEIGHT,
2036
+ icon: ctrl.icon,
2037
+ label: ctrl.label,
2038
+ exportBehavior: ctrl.exportBehavior,
2039
+ children: ctrl.children?.map((c) => ({
2040
+ id: c.id,
2041
+ label: c.label,
2042
+ x: 0,
2043
+ y: 0,
2044
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2045
+ isActive: c.isActive
2046
+ }))
2047
+ });
2048
+ totalControlsW += w + CONTROL_GAP;
2049
+ }
2050
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2051
+ }
2052
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2053
+ const groupAvailW = containerWidth - controlsSpace;
2054
+ const pills = [];
2055
+ let activeCapsule;
2056
+ for (const g of visibleGroups) {
2057
+ const isActive = activeGroupName === g.name.toLowerCase();
2058
+ if (isExport && !isActive) continue;
2059
+ if (isActive) {
2060
+ activeCapsule = buildCapsuleLayout(
2061
+ g,
2062
+ containerWidth,
2063
+ config.capsulePillAddonWidth ?? 0
2064
+ );
2065
+ } else {
2066
+ const pw = pillWidth(g.name);
2067
+ pills.push({
2068
+ groupName: g.name,
2069
+ x: 0,
2070
+ y: 0,
2071
+ width: pw,
2072
+ height: LEGEND_HEIGHT,
2073
+ isActive: false
2074
+ });
2075
+ }
2076
+ }
2077
+ const rows = layoutRows(
2078
+ activeCapsule,
2079
+ pills,
2080
+ controlLayouts,
2081
+ groupAvailW,
2082
+ containerWidth,
2083
+ totalControlsW
2084
+ );
2085
+ const height = rows.length * LEGEND_HEIGHT;
2086
+ const width = containerWidth;
2087
+ return {
2088
+ height,
2089
+ width,
2090
+ rows,
2091
+ activeCapsule,
2092
+ controls: controlLayouts,
2093
+ pills
2094
+ };
2095
+ }
2096
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2097
+ const pw = pillWidth(group.name);
2098
+ const info = capsuleWidth(
2099
+ group.name,
2100
+ group.entries,
2101
+ containerWidth,
2102
+ addonWidth
2103
+ );
2104
+ const pill = {
2105
+ groupName: group.name,
2106
+ x: LEGEND_CAPSULE_PAD,
2107
+ y: LEGEND_CAPSULE_PAD,
2108
+ width: pw,
2109
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2110
+ isActive: true
2111
+ };
2112
+ const entries = [];
2113
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2114
+ let ey = 0;
2115
+ let rowX = ex;
2116
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2117
+ let currentRow = 0;
2118
+ for (let i = 0; i < info.visibleEntries; i++) {
2119
+ const entry = group.entries[i];
2120
+ const ew = entryWidth(entry.value);
2121
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2122
+ currentRow++;
2123
+ rowX = 0;
2124
+ ey = currentRow * LEGEND_HEIGHT;
2125
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2126
+ }
2127
+ const dotCx = rowX + LEGEND_DOT_R;
2128
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2129
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2130
+ const textY = ey + LEGEND_HEIGHT / 2;
2131
+ entries.push({
2132
+ value: entry.value,
2133
+ color: entry.color,
2134
+ x: rowX,
2135
+ y: ey,
2136
+ dotCx,
2137
+ dotCy,
2138
+ textX,
2139
+ textY
2140
+ });
2141
+ rowX += ew;
2142
+ }
2143
+ const totalRows = info.entryRows;
2144
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2145
+ return {
2146
+ groupName: group.name,
2147
+ x: 0,
2148
+ y: 0,
2149
+ width: info.width,
2150
+ height: capsuleH,
2151
+ pill,
2152
+ entries,
2153
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2154
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2155
+ };
2156
+ }
2157
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2158
+ const rows = [];
2159
+ const groupItems = [];
2160
+ if (activeCapsule) groupItems.push(activeCapsule);
2161
+ groupItems.push(...pills);
2162
+ let currentRowItems = [];
2163
+ let currentRowW = 0;
2164
+ let rowY = 0;
2165
+ for (const item of groupItems) {
2166
+ const itemW = item.width + LEGEND_GROUP_GAP;
2167
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2168
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2169
+ rows.push({ y: rowY, items: currentRowItems });
2170
+ rowY += LEGEND_HEIGHT;
2171
+ currentRowItems = [];
2172
+ currentRowW = 0;
2173
+ }
2174
+ item.x = currentRowW;
2175
+ item.y = rowY;
2176
+ currentRowItems.push(item);
2177
+ currentRowW += itemW;
2178
+ }
2179
+ if (controls.length > 0) {
2180
+ let cx = containerWidth;
2181
+ for (let i = controls.length - 1; i >= 0; i--) {
2182
+ cx -= controls[i].width;
2183
+ controls[i].x = cx;
2184
+ controls[i].y = 0;
2185
+ cx -= CONTROL_GAP;
2186
+ }
2187
+ if (rows.length > 0) {
2188
+ rows[0].items.push(...controls);
2189
+ } else if (currentRowItems.length > 0) {
2190
+ currentRowItems.push(...controls);
2191
+ } else {
2192
+ currentRowItems.push(...controls);
2193
+ }
2194
+ }
2195
+ if (currentRowItems.length > 0) {
2196
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2197
+ rows.push({ y: rowY, items: currentRowItems });
2198
+ }
2199
+ if (rows.length === 0) {
2200
+ rows.push({ y: 0, items: [] });
2201
+ }
2202
+ return rows;
2203
+ }
2204
+ function centerRowItems(items, containerWidth, totalControlsW) {
2205
+ const groupItems = items.filter((it) => "groupName" in it);
2206
+ if (groupItems.length === 0) return;
2207
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2208
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2209
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2210
+ let x = offset;
2211
+ for (const item of groupItems) {
2212
+ item.x = x;
2213
+ x += item.width + LEGEND_GROUP_GAP;
2214
+ }
2215
+ }
2216
+ function getLegendReservedHeight(config, state, containerWidth) {
2217
+ const layout = computeLegendLayout(config, state, containerWidth);
2218
+ return layout.height;
2219
+ }
2220
+ var LEGEND_MAX_ENTRY_ROWS, CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2221
+ var init_legend_layout = __esm({
2222
+ "src/utils/legend-layout.ts"() {
2223
+ "use strict";
2224
+ init_legend_constants();
2225
+ LEGEND_MAX_ENTRY_ROWS = 3;
2226
+ CONTROL_PILL_PAD = 16;
2227
+ CONTROL_FONT_SIZE = 11;
2228
+ CONTROL_ICON_GAP = 4;
2229
+ CONTROL_GAP = 8;
2230
+ }
2231
+ });
2232
+
2233
+ // src/utils/legend-d3.ts
2234
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2235
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2236
+ let currentState = { ...state };
2237
+ let currentLayout;
2238
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2239
+ function render2() {
2240
+ currentLayout = computeLegendLayout(config, currentState, width);
2241
+ legendG.selectAll("*").remove();
2242
+ if (currentLayout.height === 0) return;
2243
+ if (currentState.activeGroup) {
2244
+ legendG.attr(
2245
+ "data-legend-active",
2246
+ currentState.activeGroup.toLowerCase()
2247
+ );
2248
+ } else {
2249
+ legendG.attr("data-legend-active", null);
2250
+ }
2251
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2252
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2253
+ if (currentLayout.activeCapsule) {
2254
+ renderCapsule(
2255
+ legendG,
2256
+ currentLayout.activeCapsule,
2257
+ palette,
2258
+ groupBg,
2259
+ pillBorder,
2260
+ isDark,
2261
+ callbacks
2262
+ );
2263
+ }
2264
+ for (const pill of currentLayout.pills) {
2265
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2266
+ }
2267
+ for (const ctrl of currentLayout.controls) {
2268
+ renderControl(
2269
+ legendG,
2270
+ ctrl,
2271
+ palette,
2272
+ groupBg,
2273
+ pillBorder,
2274
+ isDark,
2275
+ config.controls
2276
+ );
2277
+ }
2278
+ }
2279
+ render2();
2280
+ return {
2281
+ setState(newState) {
2282
+ currentState = { ...newState };
2283
+ render2();
2284
+ },
2285
+ destroy() {
2286
+ legendG.remove();
2287
+ },
2288
+ getHeight() {
2289
+ return currentLayout?.height ?? 0;
2290
+ },
2291
+ getLayout() {
2292
+ return currentLayout;
2293
+ }
2294
+ };
2295
+ }
2296
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2297
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2298
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2299
+ const pill = capsule.pill;
2300
+ 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);
2301
+ 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);
2302
+ 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);
2303
+ for (const entry of capsule.entries) {
2304
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2305
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2306
+ 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);
2307
+ if (callbacks?.onEntryHover) {
2308
+ const groupName = capsule.groupName;
2309
+ const entryValue = entry.value;
2310
+ const onHover = callbacks.onEntryHover;
2311
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2312
+ }
2313
+ }
2314
+ if (capsule.moreCount) {
2315
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2316
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2317
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2318
+ 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`);
2319
+ }
2320
+ if (callbacks?.onGroupToggle) {
2321
+ const cb = callbacks.onGroupToggle;
2322
+ const name = capsule.groupName;
2323
+ g.on("click", () => cb(name));
2324
+ }
2325
+ if (callbacks?.onGroupRendered) {
2326
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2327
+ }
2328
+ }
2329
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2330
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2331
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2332
+ 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);
2333
+ if (callbacks?.onGroupToggle) {
2334
+ const cb = callbacks.onGroupToggle;
2335
+ const name = pill.groupName;
2336
+ g.on("click", () => cb(name));
2337
+ }
2338
+ if (callbacks?.onGroupRendered) {
2339
+ callbacks.onGroupRendered(pill.groupName, g, false);
2340
+ }
2341
+ }
2342
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2343
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2344
+ if (ctrl.exportBehavior === "strip") {
2345
+ g.attr("data-export-ignore", "true");
2346
+ }
2347
+ 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);
2348
+ let textX = ctrl.width / 2;
2349
+ if (ctrl.icon && ctrl.label) {
2350
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2351
+ iconG.html(ctrl.icon);
2352
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2353
+ }
2354
+ if (ctrl.label) {
2355
+ 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);
2356
+ }
2357
+ if (ctrl.children) {
2358
+ let cx = ctrl.width + 4;
2359
+ for (const child of ctrl.children) {
2360
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2361
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2362
+ "fill",
2363
+ child.isActive ? palette.primary ?? palette.text : "none"
2364
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2365
+ 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);
2366
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2367
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2368
+ if (configChild?.onClick) {
2369
+ const onClick = configChild.onClick;
2370
+ childG.on("click", () => onClick());
2371
+ }
2372
+ cx += child.width + 4;
2373
+ }
2374
+ }
2375
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2376
+ if (configCtrl?.onClick) {
2377
+ const onClick = configCtrl.onClick;
2378
+ g.on("click", () => onClick());
2379
+ }
2380
+ }
2381
+ var init_legend_d3 = __esm({
2382
+ "src/utils/legend-d3.ts"() {
2383
+ "use strict";
2384
+ init_legend_constants();
2385
+ init_legend_layout();
2386
+ init_color_utils();
2387
+ init_fonts();
2388
+ }
2389
+ });
2390
+
1904
2391
  // src/utils/title-constants.ts
1905
2392
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1906
2393
  var init_title_constants = __esm({
@@ -4830,10 +5317,10 @@ var init_chart = __esm({
4830
5317
  function esc(s) {
4831
5318
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4832
5319
  }
4833
- function pillWidth(name) {
5320
+ function pillWidth2(name) {
4834
5321
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4835
5322
  }
4836
- function entriesWidth(entries) {
5323
+ function entriesWidth2(entries) {
4837
5324
  let w = 0;
4838
5325
  for (const e of entries) {
4839
5326
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4841,9 +5328,9 @@ function entriesWidth(entries) {
4841
5328
  return w;
4842
5329
  }
4843
5330
  function groupTotalWidth(name, entries, isActive) {
4844
- const pw = pillWidth(name);
5331
+ const pw = pillWidth2(name);
4845
5332
  if (!isActive) return pw;
4846
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5333
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4847
5334
  }
4848
5335
  function renderLegendSvg(groups, options) {
4849
5336
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4851,7 +5338,7 @@ function renderLegendSvg(groups, options) {
4851
5338
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4852
5339
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4853
5340
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4854
- const pw = pillWidth(g.name);
5341
+ const pw = pillWidth2(g.name);
4855
5342
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4856
5343
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4857
5344
  });
@@ -4905,6 +5392,19 @@ function renderLegendSvg(groups, options) {
4905
5392
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4906
5393
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4907
5394
  }
5395
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5396
+ return renderLegendSvg(config.groups, {
5397
+ palette: {
5398
+ bg: palette.bg,
5399
+ surface: palette.surface,
5400
+ text: palette.text,
5401
+ textMuted: palette.textMuted
5402
+ },
5403
+ isDark: palette.isDark,
5404
+ containerWidth,
5405
+ activeGroup: state.activeGroup
5406
+ });
5407
+ }
4908
5408
  var init_legend_svg = __esm({
4909
5409
  "src/utils/legend-svg.ts"() {
4910
5410
  "use strict";
@@ -8357,7 +8857,12 @@ __export(parser_exports7, {
8357
8857
  function parseArrowLine(trimmed, palette) {
8358
8858
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8359
8859
  if (bareMatch) {
8360
- return { target: bareMatch[1].trim() };
8860
+ const rawTarget = bareMatch[1].trim();
8861
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8862
+ return {
8863
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8864
+ targetIsGroup: !!groupMatch
8865
+ };
8361
8866
  }
8362
8867
  const arrowMatch = trimmed.match(ARROW_RE);
8363
8868
  if (arrowMatch) {
@@ -8366,8 +8871,14 @@ function parseArrowLine(trimmed, palette) {
8366
8871
  if (label && !color) {
8367
8872
  color = inferArrowColor(label);
8368
8873
  }
8369
- const target = arrowMatch[3].trim();
8370
- return { label, color, target };
8874
+ const rawTarget = arrowMatch[3].trim();
8875
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8876
+ return {
8877
+ label,
8878
+ color,
8879
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8880
+ targetIsGroup: !!groupMatch
8881
+ };
8371
8882
  }
8372
8883
  return null;
8373
8884
  }
@@ -8429,6 +8940,7 @@ function parseSitemap(content, palette) {
8429
8940
  const aliasMap = /* @__PURE__ */ new Map();
8430
8941
  const indentStack = [];
8431
8942
  const labelToNode = /* @__PURE__ */ new Map();
8943
+ const labelToContainer = /* @__PURE__ */ new Map();
8432
8944
  const deferredArrows = [];
8433
8945
  for (let i = 0; i < lines.length; i++) {
8434
8946
  const line10 = lines[i];
@@ -8530,6 +9042,7 @@ function parseSitemap(content, palette) {
8530
9042
  deferredArrows.push({
8531
9043
  sourceNode: source,
8532
9044
  targetLabel: arrowInfo.target,
9045
+ targetIsGroup: arrowInfo.targetIsGroup,
8533
9046
  label: arrowInfo.label,
8534
9047
  color: arrowInfo.color,
8535
9048
  lineNumber
@@ -8563,6 +9076,7 @@ function parseSitemap(content, palette) {
8563
9076
  color
8564
9077
  };
8565
9078
  attachNode2(node, indent, indentStack, result);
9079
+ labelToContainer.set(label.toLowerCase(), node);
8566
9080
  } else if (metadataMatch && indentStack.length > 0) {
8567
9081
  const rawKey = metadataMatch[1].trim().toLowerCase();
8568
9082
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8603,22 +9117,41 @@ function parseSitemap(content, palette) {
8603
9117
  }
8604
9118
  for (const arrow of deferredArrows) {
8605
9119
  const targetKey = arrow.targetLabel.toLowerCase();
8606
- const targetNode = labelToNode.get(targetKey);
8607
- if (!targetNode) {
8608
- const allLabels = Array.from(labelToNode.keys());
8609
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8610
- const hint = suggest(targetKey, allLabels);
8611
- if (hint) msg += `. ${hint}`;
8612
- pushError(arrow.lineNumber, msg);
8613
- continue;
9120
+ if (arrow.targetIsGroup) {
9121
+ const targetContainer = labelToContainer.get(targetKey);
9122
+ if (!targetContainer) {
9123
+ const allLabels = Array.from(labelToContainer.keys());
9124
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9125
+ const hint = suggest(targetKey, allLabels);
9126
+ if (hint) msg += `. ${hint}`;
9127
+ pushError(arrow.lineNumber, msg);
9128
+ continue;
9129
+ }
9130
+ result.edges.push({
9131
+ sourceId: arrow.sourceNode.id,
9132
+ targetId: targetContainer.id,
9133
+ label: arrow.label,
9134
+ color: arrow.color,
9135
+ lineNumber: arrow.lineNumber
9136
+ });
9137
+ } else {
9138
+ const targetNode = labelToNode.get(targetKey);
9139
+ if (!targetNode) {
9140
+ const allLabels = Array.from(labelToNode.keys());
9141
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9142
+ const hint = suggest(targetKey, allLabels);
9143
+ if (hint) msg += `. ${hint}`;
9144
+ pushError(arrow.lineNumber, msg);
9145
+ continue;
9146
+ }
9147
+ result.edges.push({
9148
+ sourceId: arrow.sourceNode.id,
9149
+ targetId: targetNode.id,
9150
+ label: arrow.label,
9151
+ color: arrow.color,
9152
+ lineNumber: arrow.lineNumber
9153
+ });
8614
9154
  }
8615
- result.edges.push({
8616
- sourceId: arrow.sourceNode.id,
8617
- targetId: targetNode.id,
8618
- label: arrow.label,
8619
- color: arrow.color,
8620
- lineNumber: arrow.lineNumber
8621
- });
8622
9155
  }
8623
9156
  if (result.tagGroups.length > 0) {
8624
9157
  const allNodes = [];
@@ -10332,6 +10865,7 @@ function parseBoxesAndLines(content) {
10332
10865
  const nodeLabels = /* @__PURE__ */ new Set();
10333
10866
  const groupLabels = /* @__PURE__ */ new Set();
10334
10867
  let lastNodeLabel = null;
10868
+ let lastSourceIsGroup = false;
10335
10869
  const groupStack = [];
10336
10870
  let contentStarted = false;
10337
10871
  let currentTagGroup = null;
@@ -10570,6 +11104,8 @@ function parseBoxesAndLines(content) {
10570
11104
  };
10571
11105
  groupLabels.add(label);
10572
11106
  groupStack.push({ group, indent, depth: currentDepth });
11107
+ lastNodeLabel = label;
11108
+ lastSourceIsGroup = true;
10573
11109
  continue;
10574
11110
  }
10575
11111
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10587,7 +11123,8 @@ function parseBoxesAndLines(content) {
10587
11123
  );
10588
11124
  continue;
10589
11125
  }
10590
- edgeText = `${lastNodeLabel} ${trimmed}`;
11126
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11127
+ edgeText = `${sourcePrefix} ${trimmed}`;
10591
11128
  }
10592
11129
  const edge = parseEdgeLine(
10593
11130
  edgeText,
@@ -10610,6 +11147,7 @@ function parseBoxesAndLines(content) {
10610
11147
  continue;
10611
11148
  }
10612
11149
  lastNodeLabel = node.label;
11150
+ lastSourceIsGroup = false;
10613
11151
  const gs = currentGroupState();
10614
11152
  const isGroupChild = gs && indent > gs.indent;
10615
11153
  if (nodeLabels.has(node.label)) {
@@ -10637,14 +11175,42 @@ function parseBoxesAndLines(content) {
10637
11175
  const gs = groupStack.pop();
10638
11176
  result.groups.push(gs.group);
10639
11177
  }
11178
+ const validEdges = [];
10640
11179
  for (const edge of result.edges) {
10641
- if (!edge.source.startsWith("__group_")) {
11180
+ let valid = true;
11181
+ if (edge.source.startsWith("__group_")) {
11182
+ const label = edge.source.slice("__group_".length);
11183
+ const found = [...groupLabels].some(
11184
+ (g) => g.toLowerCase() === label.toLowerCase()
11185
+ );
11186
+ if (!found) {
11187
+ result.diagnostics.push(
11188
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11189
+ );
11190
+ valid = false;
11191
+ }
11192
+ } else {
10642
11193
  ensureNode(edge.source, edge.lineNumber);
10643
11194
  }
10644
- if (!edge.target.startsWith("__group_")) {
11195
+ if (edge.target.startsWith("__group_")) {
11196
+ const label = edge.target.slice("__group_".length);
11197
+ const found = [...groupLabels].some(
11198
+ (g) => g.toLowerCase() === label.toLowerCase()
11199
+ );
11200
+ if (!found) {
11201
+ result.diagnostics.push(
11202
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11203
+ );
11204
+ valid = false;
11205
+ }
11206
+ } else {
10645
11207
  ensureNode(edge.target, edge.lineNumber);
10646
11208
  }
11209
+ if (valid) {
11210
+ validEdges.push(edge);
11211
+ }
10647
11212
  }
11213
+ result.edges = validEdges;
10648
11214
  if (result.tagGroups.length > 0) {
10649
11215
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10650
11216
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10673,10 +11239,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10673
11239
  description
10674
11240
  };
10675
11241
  }
11242
+ function resolveEndpoint(name) {
11243
+ const m = name.match(/^\[(.+)\]$/);
11244
+ return m ? groupId2(m[1].trim()) : name;
11245
+ }
10676
11246
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10677
11247
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10678
11248
  if (biLabeledMatch) {
10679
- const source2 = biLabeledMatch[1].trim();
11249
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10680
11250
  const label = biLabeledMatch[2].trim();
10681
11251
  let rest2 = biLabeledMatch[3].trim();
10682
11252
  let metadata2 = {};
@@ -10697,7 +11267,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10697
11267
  }
10698
11268
  return {
10699
11269
  source: source2,
10700
- target: rest2,
11270
+ target: resolveEndpoint(rest2),
10701
11271
  label: label || void 0,
10702
11272
  bidirectional: true,
10703
11273
  lineNumber: lineNum,
@@ -10706,7 +11276,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10706
11276
  }
10707
11277
  const biIdx = trimmed.indexOf("<->");
10708
11278
  if (biIdx >= 0) {
10709
- const source2 = trimmed.slice(0, biIdx).trim();
11279
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10710
11280
  let rest2 = trimmed.slice(biIdx + 3).trim();
10711
11281
  let metadata2 = {};
10712
11282
  const pipeIdx2 = rest2.indexOf("|");
@@ -10726,7 +11296,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10726
11296
  }
10727
11297
  return {
10728
11298
  source: source2,
10729
- target: rest2,
11299
+ target: resolveEndpoint(rest2),
10730
11300
  bidirectional: true,
10731
11301
  lineNumber: lineNum,
10732
11302
  metadata: metadata2
@@ -10734,7 +11304,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10734
11304
  }
10735
11305
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10736
11306
  if (labeledMatch) {
10737
- const source2 = labeledMatch[1].trim();
11307
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10738
11308
  const label = labeledMatch[2].trim();
10739
11309
  let rest2 = labeledMatch[3].trim();
10740
11310
  if (label) {
@@ -10756,7 +11326,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10756
11326
  }
10757
11327
  return {
10758
11328
  source: source2,
10759
- target: rest2,
11329
+ target: resolveEndpoint(rest2),
10760
11330
  label,
10761
11331
  bidirectional: false,
10762
11332
  lineNumber: lineNum,
@@ -10766,7 +11336,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10766
11336
  }
10767
11337
  const arrowIdx = trimmed.indexOf("->");
10768
11338
  if (arrowIdx < 0) return null;
10769
- const source = trimmed.slice(0, arrowIdx).trim();
11339
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10770
11340
  let rest = trimmed.slice(arrowIdx + 2).trim();
10771
11341
  if (!source || !rest) {
10772
11342
  diagnostics.push(
@@ -10787,7 +11357,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10787
11357
  }
10788
11358
  return {
10789
11359
  source,
10790
- target: rest,
11360
+ target: resolveEndpoint(rest),
10791
11361
  bidirectional: false,
10792
11362
  lineNumber: lineNum,
10793
11363
  metadata
@@ -11102,14 +11672,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11102
11672
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11103
11673
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11104
11674
  if (visibleEntries.length === 0) continue;
11105
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11106
- const minPillWidth = pillWidth2;
11107
- let entriesWidth2 = 0;
11675
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11676
+ const minPillWidth = pillWidth3;
11677
+ let entriesWidth3 = 0;
11108
11678
  for (const entry of visibleEntries) {
11109
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11679
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11110
11680
  }
11111
11681
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11112
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11682
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11113
11683
  groups.push({
11114
11684
  name: group.name,
11115
11685
  alias: group.alias,
@@ -11119,7 +11689,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11119
11689
  })),
11120
11690
  x: 0,
11121
11691
  y: 0,
11122
- width: capsuleWidth,
11692
+ width: capsuleWidth2,
11123
11693
  height: LEGEND_HEIGHT2,
11124
11694
  minifiedWidth: minPillWidth,
11125
11695
  minifiedHeight: LEGEND_HEIGHT2
@@ -12060,66 +12630,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12060
12630
  }
12061
12631
  }
12062
12632
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12063
- const visibleGroups = layout.legend.filter((group) => {
12064
- if (legendOnly) return true;
12065
- if (activeTagGroup == null) return true;
12066
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12067
- });
12068
- let fixedPositions;
12069
- if (fixedLegend && visibleGroups.length > 0) {
12070
- fixedPositions = /* @__PURE__ */ new Map();
12071
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12072
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12073
- let cx = (width - totalW) / 2;
12074
- for (const g of visibleGroups) {
12075
- fixedPositions.set(g.name, cx);
12076
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12077
- }
12078
- }
12079
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12080
- const legendParent = legendParentBase;
12081
- if (fixedLegend && activeTagGroup) {
12082
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12633
+ const groups = layout.legend.map((g) => ({
12634
+ name: g.name,
12635
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12636
+ }));
12637
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12638
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12639
+ let legendHandle;
12640
+ if (legendOnly) {
12641
+ for (const lg of layout.legend) {
12642
+ const singleConfig = {
12643
+ groups: [
12644
+ {
12645
+ name: lg.name,
12646
+ entries: lg.entries.map((e) => ({
12647
+ value: e.value,
12648
+ color: e.color
12649
+ }))
12650
+ }
12651
+ ],
12652
+ position: { placement: "top-center", titleRelation: "below-title" },
12653
+ mode: "fixed"
12654
+ };
12655
+ const singleState = { activeGroup: lg.name };
12656
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12657
+ renderLegendD3(
12658
+ groupG,
12659
+ singleConfig,
12660
+ singleState,
12661
+ palette,
12662
+ isDark,
12663
+ void 0,
12664
+ lg.width
12665
+ );
12666
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12667
+ }
12668
+ legendHandle = null;
12669
+ } else {
12670
+ const legendConfig = {
12671
+ groups,
12672
+ position: { placement: "top-center", titleRelation: "below-title" },
12673
+ mode: "fixed",
12674
+ capsulePillAddonWidth: eyeAddonWidth
12675
+ };
12676
+ const legendState = { activeGroup: activeTagGroup ?? null };
12677
+ legendHandle = renderLegendD3(
12678
+ legendParentBase,
12679
+ legendConfig,
12680
+ legendState,
12681
+ palette,
12682
+ isDark,
12683
+ void 0,
12684
+ fixedLegend ? width : layout.width
12685
+ );
12686
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12083
12687
  }
12084
- for (const group of visibleGroups) {
12085
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12086
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12087
- const pillLabel = group.name;
12088
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12089
- const gX = fixedPositions?.get(group.name) ?? group.x;
12090
- const gY = fixedPositions ? 0 : group.y;
12091
- 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");
12092
- if (isActive) {
12093
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12094
- }
12095
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12096
- const pillYOff = LEGEND_CAPSULE_PAD;
12097
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12098
- 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);
12099
- if (isActive) {
12100
- 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);
12101
- }
12102
- 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);
12103
- if (isActive && fixedLegend) {
12104
- const groupKey = group.name.toLowerCase();
12688
+ if (fixedLegend && legendHandle) {
12689
+ const computedLayout = legendHandle.getLayout();
12690
+ if (computedLayout.activeCapsule?.addonX != null) {
12691
+ const capsule = computedLayout.activeCapsule;
12692
+ const groupKey = capsule.groupName.toLowerCase();
12105
12693
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12106
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12107
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12108
- const hitPad = 6;
12109
- 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);
12110
- 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");
12111
- 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");
12112
- }
12113
- if (isActive) {
12114
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12115
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12116
- for (const entry of group.entries) {
12117
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12118
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12119
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12120
- const entryLabel = entry.value;
12121
- 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);
12122
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12694
+ const activeGroupEl = legendParentBase.select(
12695
+ `[data-legend-group="${groupKey}"]`
12696
+ );
12697
+ if (!activeGroupEl.empty()) {
12698
+ const eyeX = capsule.addonX;
12699
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12700
+ const hitPad = 6;
12701
+ 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);
12702
+ 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");
12703
+ 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");
12123
12704
  }
12124
12705
  }
12125
12706
  }
@@ -12153,6 +12734,7 @@ var init_renderer = __esm({
12153
12734
  init_parser4();
12154
12735
  init_layout();
12155
12736
  init_legend_constants();
12737
+ init_legend_d3();
12156
12738
  init_title_constants();
12157
12739
  DIAGRAM_PADDING = 20;
12158
12740
  MAX_SCALE = 3;
@@ -12182,6 +12764,17 @@ __export(layout_exports2, {
12182
12764
  layoutSitemap: () => layoutSitemap
12183
12765
  });
12184
12766
  import dagre from "@dagrejs/dagre";
12767
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12768
+ const dx = tx - cx;
12769
+ const dy = ty - cy;
12770
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12771
+ const hw = w / 2;
12772
+ const hh = h / 2;
12773
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12774
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12775
+ const s = Math.min(sx, sy);
12776
+ return { x: cx + dx * s, y: cy + dy * s };
12777
+ }
12185
12778
  function filterMetadata2(metadata, hiddenAttributes) {
12186
12779
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12187
12780
  const filtered = {};
@@ -12198,7 +12791,10 @@ function computeCardWidth2(label, meta) {
12198
12791
  const lineChars = key.length + 2 + value.length;
12199
12792
  if (lineChars > maxChars) maxChars = lineChars;
12200
12793
  }
12201
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12794
+ return Math.max(
12795
+ MIN_CARD_WIDTH2,
12796
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
12797
+ );
12202
12798
  }
12203
12799
  function computeCardHeight2(meta) {
12204
12800
  const metaCount = Object.keys(meta).length;
@@ -12207,7 +12803,12 @@ function computeCardHeight2(meta) {
12207
12803
  }
12208
12804
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12209
12805
  if (node.color) return node.color;
12210
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
12806
+ return resolveTagColor(
12807
+ node.metadata,
12808
+ tagGroups,
12809
+ activeGroupName,
12810
+ node.isContainer
12811
+ );
12211
12812
  }
12212
12813
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12213
12814
  const groups = [];
@@ -12216,21 +12817,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12216
12817
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12217
12818
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12218
12819
  if (visibleEntries.length === 0) continue;
12219
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12220
- const minPillWidth = pillWidth2;
12221
- let entriesWidth2 = 0;
12820
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12821
+ const minPillWidth = pillWidth3;
12822
+ let entriesWidth3 = 0;
12222
12823
  for (const entry of visibleEntries) {
12223
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12824
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12224
12825
  }
12225
12826
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12226
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
12827
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12227
12828
  groups.push({
12228
12829
  name: group.name,
12229
12830
  alias: group.alias,
12230
12831
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12231
12832
  x: 0,
12232
12833
  y: 0,
12233
- width: capsuleWidth,
12834
+ width: capsuleWidth2,
12234
12835
  height: LEGEND_HEIGHT3,
12235
12836
  minifiedWidth: minPillWidth,
12236
12837
  minifiedHeight: LEGEND_HEIGHT3
@@ -12250,10 +12851,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12250
12851
  parentPageId,
12251
12852
  meta,
12252
12853
  fullMeta: { ...node.metadata },
12253
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
12854
+ width: Math.max(
12855
+ MIN_CARD_WIDTH2,
12856
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
12857
+ ),
12254
12858
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12255
12859
  });
12256
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
12860
+ flattenNodes(
12861
+ node.children,
12862
+ node.id,
12863
+ parentPageId,
12864
+ hiddenCounts,
12865
+ hiddenAttributes,
12866
+ result
12867
+ );
12257
12868
  } else {
12258
12869
  result.push({
12259
12870
  sitemapNode: node,
@@ -12265,14 +12876,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12265
12876
  height: computeCardHeight2(meta)
12266
12877
  });
12267
12878
  if (node.children.length > 0) {
12268
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
12879
+ flattenNodes(
12880
+ node.children,
12881
+ parentContainerId,
12882
+ node.id,
12883
+ hiddenCounts,
12884
+ hiddenAttributes,
12885
+ result
12886
+ );
12269
12887
  }
12270
12888
  }
12271
12889
  }
12272
12890
  }
12273
12891
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12274
12892
  if (parsed.roots.length === 0) {
12275
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
12893
+ return {
12894
+ nodes: [],
12895
+ edges: [],
12896
+ containers: [],
12897
+ legend: [],
12898
+ width: 0,
12899
+ height: 0
12900
+ };
12276
12901
  }
12277
12902
  const allNodes = [];
12278
12903
  const collect = (node) => {
@@ -12280,9 +12905,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12280
12905
  for (const child of node.children) collect(child);
12281
12906
  };
12282
12907
  for (const root of parsed.roots) collect(root);
12283
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
12908
+ injectDefaultTagMetadata(
12909
+ allNodes,
12910
+ parsed.tagGroups,
12911
+ (e) => e.isContainer
12912
+ );
12284
12913
  const flatNodes = [];
12285
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
12914
+ flattenNodes(
12915
+ parsed.roots,
12916
+ null,
12917
+ null,
12918
+ hiddenCounts,
12919
+ hiddenAttributes,
12920
+ flatNodes
12921
+ );
12286
12922
  const nodeMap = /* @__PURE__ */ new Map();
12287
12923
  for (const flat of flatNodes) {
12288
12924
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12344,14 +12980,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12344
12980
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12345
12981
  }
12346
12982
  }
12983
+ const expandedContainerIds = /* @__PURE__ */ new Set();
12984
+ for (const cid of containerIds) {
12985
+ if (!collapsedContainerIds.has(cid)) {
12986
+ expandedContainerIds.add(cid);
12987
+ }
12988
+ }
12989
+ const deferredEdgeIndices = [];
12347
12990
  for (let i = 0; i < parsed.edges.length; i++) {
12348
12991
  const edge = parsed.edges[i];
12349
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12350
- g.setEdge(edge.sourceId, edge.targetId, {
12992
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12993
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
12994
+ deferredEdgeIndices.push(i);
12995
+ continue;
12996
+ }
12997
+ g.setEdge(
12998
+ edge.sourceId,
12999
+ edge.targetId,
13000
+ {
12351
13001
  label: edge.label ?? "",
12352
13002
  minlen: 1
12353
- }, `e${i}`);
12354
- }
13003
+ },
13004
+ `e${i}`
13005
+ );
12355
13006
  }
12356
13007
  dagre.layout(g);
12357
13008
  const layoutNodes = [];
@@ -12419,19 +13070,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12419
13070
  });
12420
13071
  }
12421
13072
  }
13073
+ const deferredSet = new Set(deferredEdgeIndices);
12422
13074
  const layoutEdges = [];
12423
13075
  for (let i = 0; i < parsed.edges.length; i++) {
12424
13076
  const edge = parsed.edges[i];
12425
13077
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12426
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12427
- if (!edgeData) continue;
13078
+ let points;
13079
+ if (deferredSet.has(i)) {
13080
+ const srcNode = g.node(edge.sourceId);
13081
+ const tgtNode = g.node(edge.targetId);
13082
+ if (!srcNode || !tgtNode) continue;
13083
+ const srcPt = clipToRectBorder(
13084
+ srcNode.x,
13085
+ srcNode.y,
13086
+ srcNode.width,
13087
+ srcNode.height,
13088
+ tgtNode.x,
13089
+ tgtNode.y
13090
+ );
13091
+ const tgtPt = clipToRectBorder(
13092
+ tgtNode.x,
13093
+ tgtNode.y,
13094
+ tgtNode.width,
13095
+ tgtNode.height,
13096
+ srcNode.x,
13097
+ srcNode.y
13098
+ );
13099
+ const midX = (srcPt.x + tgtPt.x) / 2;
13100
+ const midY = (srcPt.y + tgtPt.y) / 2;
13101
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13102
+ } else {
13103
+ const edgeData = g.edge({
13104
+ v: edge.sourceId,
13105
+ w: edge.targetId,
13106
+ name: `e${i}`
13107
+ });
13108
+ if (!edgeData) continue;
13109
+ points = edgeData.points ?? [];
13110
+ }
12428
13111
  layoutEdges.push({
12429
13112
  sourceId: edge.sourceId,
12430
13113
  targetId: edge.targetId,
12431
- points: edgeData.points ?? [],
13114
+ points,
12432
13115
  label: edge.label,
12433
13116
  color: edge.color,
12434
- lineNumber: edge.lineNumber
13117
+ lineNumber: edge.lineNumber,
13118
+ deferred: deferredSet.has(i) || void 0
12435
13119
  });
12436
13120
  }
12437
13121
  {
@@ -12592,7 +13276,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12592
13276
  usedValuesByGroup.set(key, used);
12593
13277
  }
12594
13278
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12595
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13279
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13280
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13281
+ ) : legendGroups;
12596
13282
  const allExpanded = expandAllLegend && activeTagGroup == null;
12597
13283
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12598
13284
  if (visibleGroups.length > 0) {
@@ -12908,7 +13594,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12908
13594
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
12909
13595
  const edgeColor3 = edge.color ?? palette.textMuted;
12910
13596
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
12911
- const pathD = lineGenerator(edge.points);
13597
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13598
+ const pathD = gen(edge.points);
12912
13599
  if (pathD) {
12913
13600
  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");
12914
13601
  }
@@ -12999,57 +13686,44 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12999
13686
  }
13000
13687
  function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13001
13688
  if (legendGroups.length === 0) return;
13002
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13003
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13004
- ) : legendGroups;
13005
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13006
- let fixedPositions;
13007
- if (fixedWidth != null && visibleGroups.length > 0) {
13008
- fixedPositions = /* @__PURE__ */ new Map();
13009
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13010
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13011
- let cx = (fixedWidth - totalW) / 2;
13012
- for (const g of visibleGroups) {
13013
- fixedPositions.set(g.name, cx);
13014
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13015
- }
13016
- }
13017
- for (const group of visibleGroups) {
13018
- const isActive = activeTagGroup != null;
13019
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13020
- const gX = fixedPositions?.get(group.name) ?? group.x;
13021
- const gY = fixedPositions ? 0 : group.y;
13022
- 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");
13023
- if (isActive) {
13024
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13025
- }
13026
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13027
- const pillYOff = LEGEND_CAPSULE_PAD;
13028
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13029
- 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);
13030
- if (isActive) {
13031
- 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);
13032
- }
13033
- 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);
13034
- if (isActive && fixedWidth != null) {
13035
- const groupKey = group.name.toLowerCase();
13689
+ const groups = legendGroups.map((g) => ({
13690
+ name: g.name,
13691
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13692
+ }));
13693
+ const isFixedMode = fixedWidth != null;
13694
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13695
+ const legendConfig = {
13696
+ groups,
13697
+ position: { placement: "top-center", titleRelation: "below-title" },
13698
+ mode: "fixed",
13699
+ capsulePillAddonWidth: eyeAddonWidth
13700
+ };
13701
+ const legendState = { activeGroup: activeTagGroup ?? null };
13702
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13703
+ const legendHandle = renderLegendD3(
13704
+ parent,
13705
+ legendConfig,
13706
+ legendState,
13707
+ palette,
13708
+ isDark,
13709
+ void 0,
13710
+ containerWidth
13711
+ );
13712
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13713
+ if (isFixedMode) {
13714
+ const computedLayout = legendHandle.getLayout();
13715
+ if (computedLayout.activeCapsule?.addonX != null) {
13716
+ const capsule = computedLayout.activeCapsule;
13717
+ const groupKey = capsule.groupName.toLowerCase();
13036
13718
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13037
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13038
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13039
- const hitPad = 6;
13040
- 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);
13041
- 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");
13042
- 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");
13043
- }
13044
- if (isActive) {
13045
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13046
- let entryX = pillXOff + pillW + 4 + eyeShift;
13047
- for (const entry of group.entries) {
13048
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13049
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13050
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13051
- 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);
13052
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13719
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13720
+ if (!activeGroupEl.empty()) {
13721
+ const eyeX = capsule.addonX;
13722
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13723
+ const hitPad = 6;
13724
+ 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);
13725
+ 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");
13726
+ 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");
13053
13727
  }
13054
13728
  }
13055
13729
  }
@@ -13103,13 +13777,14 @@ async function renderSitemapForExport(content, theme, palette) {
13103
13777
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13104
13778
  return injectBranding2(svgHtml, brandColor);
13105
13779
  }
13106
- var 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;
13780
+ var 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;
13107
13781
  var init_renderer2 = __esm({
13108
13782
  "src/sitemap/renderer.ts"() {
13109
13783
  "use strict";
13110
13784
  init_fonts();
13111
13785
  init_color_utils();
13112
13786
  init_legend_constants();
13787
+ init_legend_d3();
13113
13788
  init_title_constants();
13114
13789
  DIAGRAM_PADDING2 = 20;
13115
13790
  MAX_SCALE2 = 3;
@@ -13133,6 +13808,7 @@ var init_renderer2 = __esm({
13133
13808
  COLLAPSE_BAR_HEIGHT2 = 6;
13134
13809
  LEGEND_FIXED_GAP2 = 8;
13135
13810
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
13811
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13136
13812
  }
13137
13813
  });
13138
13814
 
@@ -13410,53 +14086,22 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13410
14086
  }
13411
14087
  if (parsed.tagGroups.length > 0) {
13412
14088
  const legendY = height - LEGEND_HEIGHT;
13413
- let legendX = DIAGRAM_PADDING3;
13414
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13415
- const capsulePad = LEGEND_CAPSULE_PAD;
13416
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13417
- if (activeTagGroup) {
13418
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13419
- }
13420
- for (const group of parsed.tagGroups) {
13421
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13422
- if (activeTagGroup != null && !isActive) continue;
13423
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13424
- const pillWidth2 = pillTextWidth + 16;
13425
- let capsuleContentWidth = pillWidth2;
13426
- if (isActive) {
13427
- capsuleContentWidth += 4;
13428
- for (const entry of group.entries) {
13429
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13430
- }
13431
- }
13432
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13433
- if (isActive) {
13434
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13435
- }
13436
- const pillX = legendX + (isActive ? capsulePad : 0);
13437
- const pillBg = isActive ? palette.bg : groupBg;
13438
- 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());
13439
- if (isActive) {
13440
- 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);
13441
- }
13442
- 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);
13443
- if (isActive) {
13444
- let entryX = pillX + pillWidth2 + 4;
13445
- for (const entry of group.entries) {
13446
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13447
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13448
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13449
- entryG.append("text").attr("x", entryTextX).attr(
13450
- "y",
13451
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13452
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13453
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13454
- }
13455
- legendX += capsuleWidth + 12;
13456
- } else {
13457
- legendX += pillWidth2 + 12;
13458
- }
13459
- }
14089
+ const legendConfig = {
14090
+ groups: parsed.tagGroups,
14091
+ position: { placement: "top-center", titleRelation: "below-title" },
14092
+ mode: exportDims ? "inline" : "fixed"
14093
+ };
14094
+ const legendState = { activeGroup: activeTagGroup ?? null };
14095
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${DIAGRAM_PADDING3},${legendY})`);
14096
+ renderLegendD3(
14097
+ legendG,
14098
+ legendConfig,
14099
+ legendState,
14100
+ palette,
14101
+ isDark,
14102
+ void 0,
14103
+ width - DIAGRAM_PADDING3 * 2
14104
+ );
13460
14105
  }
13461
14106
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13462
14107
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13551,6 +14196,7 @@ var init_renderer3 = __esm({
13551
14196
  init_parser5();
13552
14197
  init_mutations();
13553
14198
  init_legend_constants();
14199
+ init_legend_d3();
13554
14200
  init_title_constants();
13555
14201
  DIAGRAM_PADDING3 = 20;
13556
14202
  COLUMN_GAP = 16;
@@ -13746,14 +14392,9 @@ function collectClassTypes(parsed) {
13746
14392
  if (c.color) continue;
13747
14393
  present.add(c.modifier ?? "class");
13748
14394
  }
13749
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13750
- }
13751
- function legendEntriesWidth(entries) {
13752
- let w = 0;
13753
- for (const e of entries) {
13754
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13755
- }
13756
- return w;
14395
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14396
+ (k) => CLASS_TYPE_MAP[k]
14397
+ );
13757
14398
  }
13758
14399
  function classTypeKey(modifier) {
13759
14400
  return modifier ?? "class";
@@ -13822,7 +14463,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13822
14463
  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);
13823
14464
  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);
13824
14465
  if (parsed.title) {
13825
- 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);
14466
+ 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(
14467
+ "cursor",
14468
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14469
+ ).text(parsed.title);
13826
14470
  if (parsed.titleLineNumber) {
13827
14471
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13828
14472
  if (onClickItem) {
@@ -13836,32 +14480,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13836
14480
  }
13837
14481
  const isLegendExpanded = legendActive !== false;
13838
14482
  if (hasLegend) {
13839
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13840
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13841
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13842
- const entriesW = legendEntriesWidth(legendEntries);
13843
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13844
- const legendX = (width - totalW) / 2;
13845
- const legendY = titleHeight;
13846
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13847
- if (isLegendExpanded) {
13848
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13849
- 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);
13850
- 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);
13851
- 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);
13852
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13853
- for (const entry of legendEntries) {
13854
- const color = palette.colors[entry.colorKey];
13855
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13856
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13857
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13858
- 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);
13859
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14483
+ const legendGroups = [
14484
+ {
14485
+ name: LEGEND_GROUP_NAME,
14486
+ entries: legendEntries.map((entry) => ({
14487
+ value: entry.label,
14488
+ color: palette.colors[entry.colorKey]
14489
+ }))
13860
14490
  }
13861
- } else {
13862
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13863
- 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);
13864
- }
14491
+ ];
14492
+ const legendConfig = {
14493
+ groups: legendGroups,
14494
+ position: { placement: "top-center", titleRelation: "below-title" },
14495
+ mode: "fixed"
14496
+ };
14497
+ const legendState = {
14498
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14499
+ };
14500
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14501
+ renderLegendD3(
14502
+ legendG,
14503
+ legendConfig,
14504
+ legendState,
14505
+ palette,
14506
+ isDark,
14507
+ void 0,
14508
+ width
14509
+ );
13865
14510
  }
13866
14511
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13867
14512
  for (const edge of layout.edges) {
@@ -13905,7 +14550,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13905
14550
  const colorOff = !!parsed.options?.["no-auto-color"];
13906
14551
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
13907
14552
  const effectiveColor = neutralize ? palette.primary : node.color;
13908
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14553
+ const fill2 = nodeFill3(
14554
+ palette,
14555
+ isDark,
14556
+ node.modifier,
14557
+ effectiveColor,
14558
+ colorOff
14559
+ );
13909
14560
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
13910
14561
  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);
13911
14562
  let yPos = -h / 2;
@@ -13974,15 +14625,10 @@ function renderClassDiagramForExport(content, theme, palette) {
13974
14625
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
13975
14626
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
13976
14627
  return runInExportContainer(exportWidth, exportHeight, (container) => {
13977
- renderClassDiagram(
13978
- container,
13979
- parsed,
13980
- layout,
13981
- palette,
13982
- isDark,
13983
- void 0,
13984
- { width: exportWidth, height: exportHeight }
13985
- );
14628
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14629
+ width: exportWidth,
14630
+ height: exportHeight
14631
+ });
13986
14632
  return extractExportSvg(container, theme);
13987
14633
  });
13988
14634
  }
@@ -13993,6 +14639,7 @@ var init_renderer4 = __esm({
13993
14639
  init_fonts();
13994
14640
  init_export_container();
13995
14641
  init_legend_constants();
14642
+ init_legend_d3();
13996
14643
  init_title_constants();
13997
14644
  init_color_utils();
13998
14645
  init_parser2();
@@ -14601,35 +15248,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14601
15248
  }
14602
15249
  }
14603
15250
  if (parsed.tagGroups.length > 0) {
14604
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14605
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14606
- const LEGEND_GAP = 8;
14607
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14608
- if (activeTagGroup) {
14609
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14610
- }
14611
- let legendX = DIAGRAM_PADDING5;
14612
15251
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14613
- for (const group of parsed.tagGroups) {
14614
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14615
- 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}:`);
14616
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14617
- legendX += labelWidth;
14618
- for (const entry of group.entries) {
14619
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14620
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14621
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14622
- tmpText.remove();
14623
- const pillW = textW + LEGEND_PILL_PAD * 2;
14624
- 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(
14625
- "fill",
14626
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14627
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14628
- 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);
14629
- legendX += pillW + LEGEND_GAP;
14630
- }
14631
- legendX += LEGEND_GROUP_GAP;
14632
- }
15252
+ const legendConfig = {
15253
+ groups: parsed.tagGroups,
15254
+ position: { placement: "top-center", titleRelation: "below-title" },
15255
+ mode: "fixed"
15256
+ };
15257
+ const legendState = { activeGroup: activeTagGroup ?? null };
15258
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15259
+ renderLegendD3(
15260
+ legendG,
15261
+ legendConfig,
15262
+ legendState,
15263
+ palette,
15264
+ isDark,
15265
+ void 0,
15266
+ viewW
15267
+ );
15268
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14633
15269
  }
14634
15270
  if (semanticRoles) {
14635
15271
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14639,55 +15275,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14639
15275
  return false;
14640
15276
  });
14641
15277
  if (presentRoles.length > 0) {
14642
- const measureLabelW = (text, fontSize) => {
14643
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14644
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14645
- dummy.remove();
14646
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14647
- };
14648
- const labelWidths = /* @__PURE__ */ new Map();
14649
- for (const role of presentRoles) {
14650
- labelWidths.set(
14651
- role,
14652
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14653
- );
14654
- }
14655
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14656
- const groupName = "Role";
14657
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14658
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14659
- let totalWidth;
14660
- let entriesWidth2 = 0;
14661
- if (semanticActive) {
14662
- for (const role of presentRoles) {
14663
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14664
- }
14665
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14666
- } else {
14667
- totalWidth = pillWidth2;
14668
- }
14669
- const legendX = (viewW - totalWidth) / 2;
14670
15278
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14671
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14672
- if (semanticActive) {
14673
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14674
- 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);
14675
- 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);
14676
- 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);
14677
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14678
- for (const role of presentRoles) {
14679
- const label = ROLE_LABELS[role];
14680
- const roleColor = palette.colors[ROLE_COLORS[role]];
14681
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14682
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14683
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14684
- 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);
14685
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15279
+ const semanticGroups = [
15280
+ {
15281
+ name: "Role",
15282
+ entries: presentRoles.map((role) => ({
15283
+ value: ROLE_LABELS[role],
15284
+ color: palette.colors[ROLE_COLORS[role]]
15285
+ }))
14686
15286
  }
14687
- } else {
14688
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14689
- 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);
14690
- }
15287
+ ];
15288
+ const legendConfig = {
15289
+ groups: semanticGroups,
15290
+ position: { placement: "top-center", titleRelation: "below-title" },
15291
+ mode: "fixed"
15292
+ };
15293
+ const legendState = {
15294
+ activeGroup: semanticActive ? "Role" : null
15295
+ };
15296
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15297
+ renderLegendD3(
15298
+ legendG,
15299
+ legendConfig,
15300
+ legendState,
15301
+ palette,
15302
+ isDark,
15303
+ void 0,
15304
+ viewW
15305
+ );
15306
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14691
15307
  }
14692
15308
  }
14693
15309
  }
@@ -14730,6 +15346,7 @@ var init_renderer5 = __esm({
14730
15346
  init_palettes();
14731
15347
  init_tag_groups();
14732
15348
  init_legend_constants();
15349
+ init_legend_d3();
14733
15350
  init_title_constants();
14734
15351
  init_parser3();
14735
15352
  init_layout4();
@@ -14754,6 +15371,17 @@ __export(layout_exports5, {
14754
15371
  layoutBoxesAndLines: () => layoutBoxesAndLines
14755
15372
  });
14756
15373
  import dagre4 from "@dagrejs/dagre";
15374
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15375
+ const dx = tx - cx;
15376
+ const dy = ty - cy;
15377
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15378
+ const hw = w / 2;
15379
+ const hh = h / 2;
15380
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15381
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15382
+ const s = Math.min(sx, sy);
15383
+ return { x: cx + dx * s, y: cy + dy * s };
15384
+ }
14757
15385
  function computeNodeSize(_node) {
14758
15386
  const PHI = 1.618;
14759
15387
  const NODE_HEIGHT = 60;
@@ -14906,13 +15534,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14906
15534
  const srcNode = g.node(edge.source);
14907
15535
  const tgtNode = g.node(edge.target);
14908
15536
  if (!srcNode || !tgtNode) continue;
14909
- const midX = (srcNode.x + tgtNode.x) / 2;
14910
- const midY = (srcNode.y + tgtNode.y) / 2;
14911
- points = [
14912
- { x: srcNode.x, y: srcNode.y },
14913
- { x: midX, y: midY },
14914
- { x: tgtNode.x, y: tgtNode.y }
14915
- ];
15537
+ const srcPt = clipToRectBorder2(
15538
+ srcNode.x,
15539
+ srcNode.y,
15540
+ srcNode.width,
15541
+ srcNode.height,
15542
+ tgtNode.x,
15543
+ tgtNode.y
15544
+ );
15545
+ const tgtPt = clipToRectBorder2(
15546
+ tgtNode.x,
15547
+ tgtNode.y,
15548
+ tgtNode.width,
15549
+ tgtNode.height,
15550
+ srcNode.x,
15551
+ srcNode.y
15552
+ );
15553
+ const midX = (srcPt.x + tgtPt.x) / 2;
15554
+ const midY = (srcPt.y + tgtPt.y) / 2;
15555
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
14916
15556
  } else {
14917
15557
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
14918
15558
  points = dagreEdge?.points ?? [];
@@ -14935,7 +15575,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14935
15575
  labelY,
14936
15576
  yOffset: edgeYOffsets[i],
14937
15577
  parallelCount: edgeParallelCounts[i],
14938
- metadata: edge.metadata
15578
+ metadata: edge.metadata,
15579
+ deferred: deferredSet.has(i) || void 0
14939
15580
  });
14940
15581
  }
14941
15582
  let maxX = 0;
@@ -15205,12 +15846,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15205
15846
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15206
15847
  edgeGroups.set(i, edgeG);
15207
15848
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15208
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15209
- "d",
15210
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15211
- points
15212
- ) ?? ""
15213
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15849
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
15850
+ 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})`);
15214
15851
  if (le.bidirectional) {
15215
15852
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15216
15853
  path.attr("marker-start", `url(#${revId})`);
@@ -15291,50 +15928,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15291
15928
  }
15292
15929
  }
15293
15930
  if (parsed.tagGroups.length > 0) {
15294
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15295
- }
15296
- }
15297
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15298
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15299
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15300
- let totalW = 0;
15301
- for (const tg of parsed.tagGroups) {
15302
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15303
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15304
- if (isActive) {
15305
- totalW += 6;
15306
- for (const entry of tg.entries) {
15307
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15308
- }
15309
- }
15310
- totalW += LEGEND_GROUP_GAP;
15311
- }
15312
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15313
- const legendY = titleOffset + 4;
15314
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15315
- let x = 0;
15316
- for (const tg of parsed.tagGroups) {
15317
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15318
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15319
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15320
- 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);
15321
- if (isActiveGroup) {
15322
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15323
- }
15324
- 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);
15325
- x += nameW;
15326
- if (isActiveGroup) {
15327
- x += 6;
15328
- for (const entry of tg.entries) {
15329
- const entryColor = entry.color || palette.textMuted;
15330
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15331
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15332
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15333
- 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);
15334
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15335
- }
15336
- }
15337
- x += LEGEND_GROUP_GAP;
15931
+ const legendConfig = {
15932
+ groups: parsed.tagGroups,
15933
+ position: { placement: "top-center", titleRelation: "below-title" },
15934
+ mode: "fixed"
15935
+ };
15936
+ const legendState = { activeGroup };
15937
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
15938
+ renderLegendD3(
15939
+ legendG,
15940
+ legendConfig,
15941
+ legendState,
15942
+ palette,
15943
+ isDark,
15944
+ void 0,
15945
+ width
15946
+ );
15947
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15338
15948
  }
15339
15949
  }
15340
15950
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15342,12 +15952,13 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15342
15952
  exportDims: options?.exportDims
15343
15953
  });
15344
15954
  }
15345
- var 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;
15955
+ var 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;
15346
15956
  var init_renderer6 = __esm({
15347
15957
  "src/boxes-and-lines/renderer.ts"() {
15348
15958
  "use strict";
15349
15959
  init_fonts();
15350
15960
  init_legend_constants();
15961
+ init_legend_d3();
15351
15962
  init_title_constants();
15352
15963
  init_color_utils();
15353
15964
  init_tag_groups();
@@ -15368,6 +15979,7 @@ var init_renderer6 = __esm({
15368
15979
  GROUP_LABEL_FONT_SIZE = 14;
15369
15980
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15370
15981
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
15982
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15371
15983
  }
15372
15984
  });
15373
15985
 
@@ -17276,7 +17888,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17276
17888
  if (activeTagGroup) {
17277
17889
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17278
17890
  }
17279
- renderLegend3(
17891
+ renderLegend2(
17280
17892
  legendParent,
17281
17893
  layout,
17282
17894
  palette,
@@ -17637,52 +18249,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17637
18249
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17638
18250
  }
17639
18251
  }
17640
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17641
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17642
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17643
- ) : layout.legend;
17644
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17645
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17646
- let fixedPositions = null;
17647
- if (fixedWidth != null && visibleGroups.length > 0) {
17648
- fixedPositions = /* @__PURE__ */ new Map();
17649
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17650
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17651
- for (const g of visibleGroups) {
17652
- fixedPositions.set(g.name, cx);
17653
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17654
- }
17655
- }
17656
- for (const group of visibleGroups) {
17657
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17658
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17659
- const pillLabel = group.name;
17660
- const pillWidth2 = pillWidthOf(group);
17661
- const gX = fixedPositions?.get(group.name) ?? group.x;
17662
- const gY = fixedPositions != null ? 0 : group.y;
17663
- 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");
17664
- if (isActive) {
17665
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17666
- }
17667
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17668
- const pillY = LEGEND_CAPSULE_PAD;
17669
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17670
- 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);
17671
- if (isActive) {
17672
- 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);
17673
- }
17674
- 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);
17675
- if (isActive) {
17676
- let entryX = pillX + pillWidth2 + 4;
17677
- for (const entry of group.entries) {
17678
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17679
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17680
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17681
- 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);
17682
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17683
- }
17684
- }
17685
- }
18252
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18253
+ const groups = layout.legend.map((g) => ({
18254
+ name: g.name,
18255
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18256
+ }));
18257
+ const legendConfig = {
18258
+ groups,
18259
+ position: { placement: "top-center", titleRelation: "below-title" },
18260
+ mode: "fixed"
18261
+ };
18262
+ const legendState = { activeGroup: activeTagGroup ?? null };
18263
+ const containerWidth = fixedWidth ?? layout.width;
18264
+ renderLegendD3(
18265
+ parent,
18266
+ legendConfig,
18267
+ legendState,
18268
+ palette,
18269
+ isDark,
18270
+ void 0,
18271
+ containerWidth
18272
+ );
18273
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17686
18274
  }
17687
18275
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17688
18276
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -17893,7 +18481,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
17893
18481
  if (activeTagGroup) {
17894
18482
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17895
18483
  }
17896
- renderLegend3(
18484
+ renderLegend2(
17897
18485
  legendParent,
17898
18486
  layout,
17899
18487
  palette,
@@ -18021,6 +18609,7 @@ var init_renderer7 = __esm({
18021
18609
  init_parser6();
18022
18610
  init_layout6();
18023
18611
  init_legend_constants();
18612
+ init_legend_d3();
18024
18613
  init_title_constants();
18025
18614
  DIAGRAM_PADDING7 = 20;
18026
18615
  MAX_SCALE5 = 3;
@@ -20910,17 +21499,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20910
21499
  color: r.color,
20911
21500
  key: r.name.toLowerCase().replace(/\s+/g, "-")
20912
21501
  }));
20913
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20914
- let entriesWidth2 = 0;
21502
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21503
+ let entriesWidth3 = 0;
20915
21504
  for (const e of entries) {
20916
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21505
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20917
21506
  }
20918
21507
  groups.push({
20919
21508
  name: "Capabilities",
20920
21509
  type: "role",
20921
21510
  entries,
20922
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20923
- minifiedWidth: pillWidth2
21511
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21512
+ minifiedWidth: pillWidth3
20924
21513
  });
20925
21514
  }
20926
21515
  for (const tg of tagGroups) {
@@ -20935,113 +21524,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20935
21524
  }
20936
21525
  }
20937
21526
  if (entries.length === 0) continue;
20938
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20939
- let entriesWidth2 = 0;
21527
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21528
+ let entriesWidth3 = 0;
20940
21529
  for (const e of entries) {
20941
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21530
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20942
21531
  }
20943
21532
  groups.push({
20944
21533
  name: tg.name,
20945
21534
  type: "tag",
20946
21535
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
20947
21536
  entries,
20948
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20949
- minifiedWidth: pillWidth2
21537
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21538
+ minifiedWidth: pillWidth3
20950
21539
  });
20951
21540
  }
20952
21541
  return groups;
20953
21542
  }
20954
- function computePlaybackWidth(playback) {
20955
- if (!playback) return 0;
20956
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20957
- if (!playback.expanded) return pillWidth2;
20958
- let entriesW = 8;
20959
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
20960
- for (const s of playback.speedOptions) {
20961
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
20962
- }
20963
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
20964
- }
20965
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21543
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
20966
21544
  if (legendGroups.length === 0 && !playback) return;
20967
21545
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
20968
21546
  if (activeGroup) {
20969
21547
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
20970
21548
  }
20971
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
20972
- const playbackW = computePlaybackWidth(playback);
20973
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
20974
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
20975
- let cursorX = (totalWidth - totalLegendW) / 2;
21549
+ const allGroups = legendGroups.map((g) => ({
21550
+ name: g.name,
21551
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21552
+ }));
21553
+ if (playback) {
21554
+ allGroups.push({ name: "Playback", entries: [] });
21555
+ }
21556
+ const legendConfig = {
21557
+ groups: allGroups,
21558
+ position: { placement: "top-center", titleRelation: "below-title" },
21559
+ mode: "fixed",
21560
+ showEmptyGroups: true
21561
+ };
21562
+ const legendState = { activeGroup };
21563
+ renderLegendD3(
21564
+ legendG,
21565
+ legendConfig,
21566
+ legendState,
21567
+ palette,
21568
+ isDark,
21569
+ void 0,
21570
+ totalWidth
21571
+ );
21572
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
20976
21573
  for (const group of legendGroups) {
20977
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
20978
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20979
- const pillLabel = group.name;
20980
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20981
- 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");
20982
- if (isActive) {
20983
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
20984
- }
20985
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
20986
- const pillYOff = LEGEND_CAPSULE_PAD;
20987
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
20988
- 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);
20989
- if (isActive) {
20990
- 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);
20991
- }
20992
- 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);
20993
- if (isActive) {
20994
- let entryX = pillXOff + pillWidth2 + 4;
20995
- for (const entry of group.entries) {
20996
- 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(
21574
+ const groupKey = group.name.toLowerCase();
21575
+ for (const entry of group.entries) {
21576
+ const entryEl = legendG.select(
21577
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21578
+ );
21579
+ if (!entryEl.empty()) {
21580
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
20997
21581
  "data-legend-tag-group",
20998
21582
  group.type === "tag" ? group.tagKey ?? "" : null
20999
- ).style("cursor", "pointer");
21000
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21001
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21002
- 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);
21003
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21583
+ );
21004
21584
  }
21005
21585
  }
21006
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21007
21586
  }
21008
- if (playback) {
21009
- const isExpanded = playback.expanded;
21010
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21011
- const pillLabel = "Playback";
21012
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21013
- const fullW = computePlaybackWidth(playback);
21014
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21015
- if (isExpanded) {
21016
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21017
- }
21018
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21019
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21020
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21021
- 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);
21022
- if (isExpanded) {
21023
- 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);
21024
- }
21025
- 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);
21026
- if (isExpanded) {
21027
- let entryX = pillXOff + pillWidth2 + 8;
21028
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21029
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21030
- 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);
21031
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21032
- for (const s of playback.speedOptions) {
21033
- const label = `${s}x`;
21034
- const isActive = playback.speed === s;
21035
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21036
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21037
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21038
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21039
- 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");
21040
- 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);
21041
- entryX += slotW + SPEED_BADGE_GAP;
21042
- }
21043
- }
21044
- cursorX += fullW + LEGEND_GROUP_GAP;
21587
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21588
+ if (!playbackEl.empty()) {
21589
+ playbackEl.classed("infra-playback-pill", true);
21590
+ }
21591
+ if (playback && playback.expanded && !playbackEl.empty()) {
21592
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21593
+ let entryX = pillWidth3 + 8;
21594
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21595
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21596
+ 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);
21597
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21598
+ for (const s of playback.speedOptions) {
21599
+ const label = `${s}x`;
21600
+ const isSpeedActive = playback.speed === s;
21601
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21602
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21603
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21604
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21605
+ 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");
21606
+ 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);
21607
+ entryX += slotW + SPEED_BADGE_GAP;
21608
+ }
21045
21609
  }
21046
21610
  }
21047
21611
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21172,7 +21736,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21172
21736
  "viewBox",
21173
21737
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21174
21738
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21175
- renderLegend4(
21739
+ renderLegend3(
21176
21740
  legendSvg,
21177
21741
  legendGroups,
21178
21742
  containerWidth,
@@ -21184,7 +21748,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21184
21748
  );
21185
21749
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21186
21750
  } else {
21187
- renderLegend4(
21751
+ renderLegend3(
21188
21752
  rootSvg,
21189
21753
  legendGroups,
21190
21754
  totalWidth,
@@ -21216,6 +21780,7 @@ var init_renderer8 = __esm({
21216
21780
  init_compute();
21217
21781
  init_layout8();
21218
21782
  init_legend_constants();
21783
+ init_legend_d3();
21219
21784
  init_title_constants();
21220
21785
  NODE_FONT_SIZE3 = 13;
21221
21786
  META_FONT_SIZE5 = 10;
@@ -22843,7 +23408,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22843
23408
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22844
23409
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22845
23410
  const showIcon = !legendViewMode && tagGroups.length > 0;
22846
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23411
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22847
23412
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22848
23413
  let groupW = pillW;
22849
23414
  if (isActive) {
@@ -22870,83 +23435,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22870
23435
  const legendX = (containerWidth - totalW) / 2;
22871
23436
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22872
23437
  let cursorX = 0;
22873
- for (let i = 0; i < visibleGroups.length; i++) {
22874
- const group = visibleGroups[i];
22875
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22876
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23438
+ if (visibleGroups.length > 0) {
22877
23439
  const showIcon = !legendViewMode && tagGroups.length > 0;
22878
23440
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
22879
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22880
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
22881
- const groupW = groupWidths[i];
22882
- 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", () => {
22883
- if (onToggle) onToggle(group.name);
23441
+ const legendGroups = visibleGroups.map((g) => {
23442
+ const key = g.name.toLowerCase();
23443
+ const entries = filteredEntries.get(key) ?? g.entries;
23444
+ return {
23445
+ name: g.name,
23446
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23447
+ };
22884
23448
  });
22885
- if (isActive) {
22886
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
22887
- }
22888
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
22889
- const pillYOff = LEGEND_CAPSULE_PAD;
22890
- 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);
22891
- if (isActive) {
22892
- 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);
22893
- }
22894
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
22895
- 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);
22896
- if (showIcon) {
22897
- const iconX = pillXOff + textW + 3;
22898
- const iconY = (LEGEND_HEIGHT - 10) / 2;
22899
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
22900
- iconEl.append("title").text(`Group by ${group.name}`);
22901
- iconEl.style("cursor", "pointer").on("click", (event) => {
22902
- event.stopPropagation();
22903
- if (onSwimlaneChange) {
22904
- onSwimlaneChange(
22905
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
22906
- );
22907
- }
22908
- });
22909
- }
22910
- if (isActive) {
22911
- const tagKey = group.name.toLowerCase();
22912
- const entries = filteredEntries.get(tagKey) ?? group.entries;
22913
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
22914
- for (const entry of entries) {
22915
- const entryValue = entry.value.toLowerCase();
22916
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
22917
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
22918
- 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);
22919
- entryG.on("mouseenter", () => {
23449
+ const legendConfig = {
23450
+ groups: legendGroups,
23451
+ position: {
23452
+ placement: "top-center",
23453
+ titleRelation: "below-title"
23454
+ },
23455
+ mode: "fixed",
23456
+ capsulePillAddonWidth: iconReserve
23457
+ };
23458
+ const legendState = { activeGroup: activeGroupName };
23459
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23460
+ const tagGroupG = legendRow.append("g");
23461
+ const legendCallbacks = {
23462
+ onGroupToggle: onToggle,
23463
+ onEntryHover: (groupName, entryValue) => {
23464
+ const tagKey = groupName.toLowerCase();
23465
+ if (entryValue) {
23466
+ const ev = entryValue.toLowerCase();
22920
23467
  chartG.selectAll(".gantt-task").each(function() {
22921
23468
  const el = d3Selection10.select(this);
22922
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22923
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23469
+ el.attr(
23470
+ "opacity",
23471
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23472
+ );
22924
23473
  });
22925
23474
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
22926
23475
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
22927
23476
  svg.selectAll(".gantt-task-label").each(function() {
22928
23477
  const el = d3Selection10.select(this);
22929
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22930
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23478
+ el.attr(
23479
+ "opacity",
23480
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23481
+ );
22931
23482
  });
22932
23483
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
22933
23484
  svg.selectAll(".gantt-lane-header").each(function() {
22934
23485
  const el = d3Selection10.select(this);
22935
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22936
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23486
+ el.attr(
23487
+ "opacity",
23488
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23489
+ );
22937
23490
  });
22938
23491
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
22939
- }).on("mouseleave", () => {
23492
+ } else {
22940
23493
  if (criticalPathActive) {
22941
23494
  applyCriticalPathHighlight(svg, chartG);
22942
23495
  } else {
22943
23496
  resetHighlightAll(svg, chartG);
22944
23497
  }
22945
- });
22946
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23498
+ }
23499
+ },
23500
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23501
+ const group = visibleGroups.find((g) => g.name === groupName);
23502
+ if (group) {
23503
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23504
+ }
23505
+ if (showIcon && _isActive) {
23506
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23507
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23508
+ const pillXOff = LEGEND_CAPSULE_PAD;
23509
+ const iconX = pillXOff + textW + 3;
23510
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23511
+ const iconEl = drawSwimlaneIcon(
23512
+ groupEl,
23513
+ iconX,
23514
+ iconY,
23515
+ isSwimlane,
23516
+ palette
23517
+ );
23518
+ iconEl.append("title").text(`Group by ${groupName}`);
23519
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23520
+ event.stopPropagation();
23521
+ if (onSwimlaneChange) {
23522
+ onSwimlaneChange(
23523
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23524
+ );
23525
+ }
23526
+ });
23527
+ }
22947
23528
  }
23529
+ };
23530
+ renderLegendD3(
23531
+ tagGroupG,
23532
+ legendConfig,
23533
+ legendState,
23534
+ palette,
23535
+ isDark,
23536
+ legendCallbacks,
23537
+ tagGroupsW
23538
+ );
23539
+ for (let i = 0; i < visibleGroups.length; i++) {
23540
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
22948
23541
  }
22949
- cursorX += groupW + LEGEND_GROUP_GAP;
22950
23542
  }
22951
23543
  if (hasCriticalPath) {
22952
23544
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23573,6 +24165,7 @@ var init_renderer9 = __esm({
23573
24165
  init_tag_groups();
23574
24166
  init_d3();
23575
24167
  init_legend_constants();
24168
+ init_legend_d3();
23576
24169
  init_title_constants();
23577
24170
  BAR_H = 22;
23578
24171
  ROW_GAP = 6;
@@ -24743,57 +25336,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24743
25336
  }
24744
25337
  if (parsed.tagGroups.length > 0) {
24745
25338
  const legendY = TOP_MARGIN + titleOffset;
24746
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24747
- const legendItems = [];
24748
- for (const tg of parsed.tagGroups) {
24749
- if (tg.entries.length === 0) continue;
24750
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24751
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24752
- const entries = tg.entries.map((e) => ({
25339
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25340
+ name: tg.name,
25341
+ entries: tg.entries.map((e) => ({
24753
25342
  value: e.value,
24754
25343
  color: resolveColor(e.color) ?? e.color
24755
- }));
24756
- let totalWidth2 = pillWidth2;
24757
- if (isActive) {
24758
- let entriesWidth2 = 0;
24759
- for (const entry of entries) {
24760
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24761
- }
24762
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24763
- }
24764
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24765
- }
24766
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24767
- let legendX = (svgWidth - totalLegendWidth) / 2;
24768
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24769
- if (activeTagGroup) {
24770
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24771
- }
24772
- for (const item of legendItems) {
24773
- 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");
24774
- if (item.isActive) {
24775
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24776
- }
24777
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24778
- const pillYOff = LEGEND_CAPSULE_PAD;
24779
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24780
- 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);
24781
- if (item.isActive) {
24782
- 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);
24783
- }
24784
- 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);
24785
- if (item.isActive) {
24786
- let entryX = pillXOff + item.pillWidth + 4;
24787
- for (const entry of item.entries) {
24788
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24789
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24790
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24791
- 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);
24792
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24793
- }
24794
- }
24795
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24796
- }
25344
+ }))
25345
+ }));
25346
+ const legendConfig = {
25347
+ groups: resolvedGroups,
25348
+ position: { placement: "top-center", titleRelation: "below-title" },
25349
+ mode: "fixed"
25350
+ };
25351
+ const legendState = { activeGroup: activeTagGroup ?? null };
25352
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25353
+ renderLegendD3(
25354
+ legendG,
25355
+ legendConfig,
25356
+ legendState,
25357
+ palette,
25358
+ isDark,
25359
+ void 0,
25360
+ svgWidth
25361
+ );
24797
25362
  }
24798
25363
  for (const group of groups) {
24799
25364
  if (group.participantIds.length === 0) continue;
@@ -25352,6 +25917,7 @@ var init_renderer10 = __esm({
25352
25917
  init_parser();
25353
25918
  init_tag_resolution();
25354
25919
  init_legend_constants();
25920
+ init_legend_d3();
25355
25921
  init_title_constants();
25356
25922
  PARTICIPANT_GAP = 160;
25357
25923
  PARTICIPANT_BOX_WIDTH = 120;
@@ -28012,7 +28578,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28012
28578
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28013
28579
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28014
28580
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28015
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28016
28581
  const LG_ICON_W = 20;
28017
28582
  const mainSvg = d3Selection13.select(container).select("svg");
28018
28583
  const mainG = mainSvg.select("g");
@@ -28051,11 +28616,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28051
28616
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28052
28617
  ) : legendGroups;
28053
28618
  if (visibleGroups.length === 0) return;
28054
- const totalW = visibleGroups.reduce((s, lg) => {
28055
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28056
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28057
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28058
- let cx = (width - totalW) / 2;
28059
28619
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28060
28620
  if (currentActiveGroup) {
28061
28621
  legendContainer.attr(
@@ -28063,82 +28623,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28063
28623
  currentActiveGroup.toLowerCase()
28064
28624
  );
28065
28625
  }
28066
- for (const lg of visibleGroups) {
28067
- const groupKey = lg.group.name.toLowerCase();
28068
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28069
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28070
- const pillLabel = lg.group.name;
28071
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28072
- 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__");
28073
- if (!viewMode) {
28074
- gEl.style("cursor", "pointer").on("click", () => {
28075
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28076
- drawLegend2();
28077
- recolorEvents2();
28078
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28079
- });
28080
- }
28081
- if (isActive) {
28082
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28083
- }
28084
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28085
- const pillYOff = LG_CAPSULE_PAD;
28086
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28087
- 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);
28088
- if (isActive) {
28089
- 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);
28090
- }
28091
- 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);
28092
- if (isActive) {
28093
- let entryX;
28094
- if (!viewMode) {
28095
- const iconX = pillXOff + pillWidth2 + 5;
28626
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28627
+ const centralGroups = visibleGroups.map((lg) => ({
28628
+ name: lg.group.name,
28629
+ entries: lg.group.entries.map((e) => ({
28630
+ value: e.value,
28631
+ color: e.color
28632
+ }))
28633
+ }));
28634
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28635
+ const centralConfig = {
28636
+ groups: centralGroups,
28637
+ position: { placement: "top-center", titleRelation: "below-title" },
28638
+ mode: "fixed",
28639
+ capsulePillAddonWidth: iconAddon
28640
+ };
28641
+ const centralState = { activeGroup: centralActive };
28642
+ const centralCallbacks = viewMode ? {} : {
28643
+ onGroupToggle: (groupName) => {
28644
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28645
+ drawLegend2();
28646
+ recolorEvents2();
28647
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28648
+ },
28649
+ onEntryHover: (groupName, entryValue) => {
28650
+ const tagKey = groupName.toLowerCase();
28651
+ if (entryValue) {
28652
+ const tagVal = entryValue.toLowerCase();
28653
+ fadeToTagValue(mainG, tagKey, tagVal);
28654
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28655
+ const el = d3Selection13.select(this);
28656
+ const ev = el.attr("data-legend-entry");
28657
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28658
+ el.attr(
28659
+ "opacity",
28660
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28661
+ );
28662
+ });
28663
+ } else {
28664
+ fadeReset(mainG);
28665
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28666
+ }
28667
+ },
28668
+ onGroupRendered: (groupName, groupEl, isActive) => {
28669
+ const groupKey = groupName.toLowerCase();
28670
+ groupEl.attr("data-tag-group", groupKey);
28671
+ if (isActive && !viewMode) {
28672
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28673
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28674
+ const pillXOff = LG_CAPSULE_PAD;
28675
+ const iconX = pillXOff + pillWidth3 + 5;
28096
28676
  const iconY = (LG_HEIGHT - 10) / 2;
28097
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28677
+ const iconEl = drawSwimlaneIcon3(
28678
+ groupEl,
28679
+ iconX,
28680
+ iconY,
28681
+ isSwimActive
28682
+ );
28098
28683
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28099
28684
  event.stopPropagation();
28100
28685
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28101
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28686
+ onTagStateChange?.(
28687
+ currentActiveGroup,
28688
+ currentSwimlaneGroup
28689
+ );
28102
28690
  relayout2();
28103
28691
  });
28104
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28105
- } else {
28106
- entryX = pillXOff + pillWidth2 + 8;
28107
- }
28108
- for (const entry of lg.group.entries) {
28109
- const tagKey = lg.group.name.toLowerCase();
28110
- const tagVal = entry.value.toLowerCase();
28111
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28112
- if (!viewMode) {
28113
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28114
- event.stopPropagation();
28115
- fadeToTagValue(mainG, tagKey, tagVal);
28116
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28117
- const el = d3Selection13.select(this);
28118
- const ev = el.attr("data-legend-entry");
28119
- if (ev === "__group__") return;
28120
- const eg = el.attr("data-tag-group");
28121
- el.attr(
28122
- "opacity",
28123
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28124
- );
28125
- });
28126
- }).on("mouseleave", (event) => {
28127
- event.stopPropagation();
28128
- fadeReset(mainG);
28129
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28130
- }).on("click", (event) => {
28131
- event.stopPropagation();
28132
- });
28133
- }
28134
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28135
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28136
- 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);
28137
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28138
28692
  }
28139
28693
  }
28140
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28141
- }
28694
+ };
28695
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28696
+ renderLegendD3(
28697
+ legendInnerG,
28698
+ centralConfig,
28699
+ centralState,
28700
+ palette,
28701
+ isDark,
28702
+ centralCallbacks,
28703
+ width
28704
+ );
28142
28705
  }, recolorEvents2 = function() {
28143
28706
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28144
28707
  mainG.selectAll(".tl-event").each(function() {
@@ -28163,7 +28726,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28163
28726
  };
28164
28727
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28165
28728
  const legendY = title ? 50 : 10;
28166
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28167
28729
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28168
28730
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28169
28731
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -29405,6 +29967,7 @@ var init_d3 = __esm({
29405
29967
  init_parsing();
29406
29968
  init_tag_groups();
29407
29969
  init_legend_constants();
29970
+ init_legend_d3();
29408
29971
  init_title_constants();
29409
29972
  DEFAULT_CLOUD_OPTIONS = {
29410
29973
  rotate: "none",
@@ -29559,11 +30122,26 @@ async function ensureDom() {
29559
30122
  const { JSDOM } = await import("jsdom");
29560
30123
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29561
30124
  const win = dom.window;
29562
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29563
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29564
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29565
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29566
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30125
+ Object.defineProperty(globalThis, "document", {
30126
+ value: win.document,
30127
+ configurable: true
30128
+ });
30129
+ Object.defineProperty(globalThis, "window", {
30130
+ value: win,
30131
+ configurable: true
30132
+ });
30133
+ Object.defineProperty(globalThis, "navigator", {
30134
+ value: win.navigator,
30135
+ configurable: true
30136
+ });
30137
+ Object.defineProperty(globalThis, "HTMLElement", {
30138
+ value: win.HTMLElement,
30139
+ configurable: true
30140
+ });
30141
+ Object.defineProperty(globalThis, "SVGElement", {
30142
+ value: win.SVGElement,
30143
+ configurable: true
30144
+ });
29567
30145
  }
29568
30146
  async function render(content, options) {
29569
30147
  const theme = options?.theme ?? "light";
@@ -29572,11 +30150,17 @@ async function render(content, options) {
29572
30150
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29573
30151
  const chartType = parseDgmoChartType(content);
29574
30152
  const category = chartType ? getRenderCategory(chartType) : null;
30153
+ const legendExportState = options?.legendState ? {
30154
+ activeTagGroup: options.legendState.activeGroup ?? null,
30155
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30156
+ } : void 0;
29575
30157
  if (category === "data-chart") {
29576
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30158
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30159
+ branding
30160
+ });
29577
30161
  }
29578
30162
  await ensureDom();
29579
- return renderForExport(content, theme, paletteColors, void 0, {
30163
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29580
30164
  branding,
29581
30165
  c4Level: options?.c4Level,
29582
30166
  c4System: options?.c4System,
@@ -30299,6 +30883,8 @@ init_flowchart_renderer();
30299
30883
  init_echarts();
30300
30884
  init_legend_svg();
30301
30885
  init_legend_constants();
30886
+ init_legend_d3();
30887
+ init_legend_layout();
30302
30888
  init_d3();
30303
30889
  init_renderer10();
30304
30890
  init_colors();
@@ -31145,6 +31731,7 @@ export {
31145
31731
  computeCardMove,
31146
31732
  computeInfra,
31147
31733
  computeInfraLegendGroups,
31734
+ computeLegendLayout,
31148
31735
  computeScatterLabelGraphics,
31149
31736
  computeTimeTicks,
31150
31737
  contrastText,
@@ -31156,6 +31743,7 @@ export {
31156
31743
  formatDgmoError,
31157
31744
  getAvailablePalettes,
31158
31745
  getExtendedChartLegendGroups,
31746
+ getLegendReservedHeight,
31159
31747
  getPalette,
31160
31748
  getRenderCategory,
31161
31749
  getSeriesColors,
@@ -31244,7 +31832,9 @@ export {
31244
31832
  renderInfra,
31245
31833
  renderKanban,
31246
31834
  renderKanbanForExport,
31835
+ renderLegendD3,
31247
31836
  renderLegendSvg,
31837
+ renderLegendSvgFromConfig,
31248
31838
  renderOrg,
31249
31839
  renderOrgForExport,
31250
31840
  renderQuadrant,