@diagrammo/dgmo 0.8.8 → 0.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1923,6 +1923,493 @@ var init_legend_constants = __esm({
1923
1923
  }
1924
1924
  });
1925
1925
 
1926
+ // src/utils/legend-layout.ts
1927
+ function pillWidth(name) {
1928
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1929
+ }
1930
+ function entriesWidth(entries) {
1931
+ let w = 0;
1932
+ for (const e of entries) {
1933
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1934
+ }
1935
+ return w;
1936
+ }
1937
+ function entryWidth(value) {
1938
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1939
+ }
1940
+ function controlWidth(control) {
1941
+ let w = CONTROL_PILL_PAD;
1942
+ if (control.label) {
1943
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
1944
+ if (control.icon) w += CONTROL_ICON_GAP;
1945
+ }
1946
+ if (control.icon) w += 14;
1947
+ if (control.children) {
1948
+ for (const child of control.children) {
1949
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
1950
+ }
1951
+ }
1952
+ return w;
1953
+ }
1954
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
1955
+ const pw = pillWidth(name);
1956
+ const maxCapsuleW = containerWidth;
1957
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
1958
+ const ew = entriesWidth(entries);
1959
+ const singleRowW = baseW + ew;
1960
+ if (singleRowW <= maxCapsuleW) {
1961
+ return {
1962
+ width: singleRowW,
1963
+ entryRows: 1,
1964
+ moreCount: 0,
1965
+ visibleEntries: entries.length
1966
+ };
1967
+ }
1968
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
1969
+ let row = 1;
1970
+ let rowX = pw + 4;
1971
+ let visible = 0;
1972
+ for (let i = 0; i < entries.length; i++) {
1973
+ const ew2 = entryWidth(entries[i].value);
1974
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
1975
+ row++;
1976
+ rowX = 0;
1977
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
1978
+ return {
1979
+ width: maxCapsuleW,
1980
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
1981
+ moreCount: entries.length - visible,
1982
+ visibleEntries: visible
1983
+ };
1984
+ }
1985
+ }
1986
+ rowX += ew2;
1987
+ visible++;
1988
+ }
1989
+ return {
1990
+ width: maxCapsuleW,
1991
+ entryRows: row,
1992
+ moreCount: 0,
1993
+ visibleEntries: entries.length
1994
+ };
1995
+ }
1996
+ function computeLegendLayout(config, state, containerWidth) {
1997
+ const { groups, controls: configControls, mode } = config;
1998
+ const isExport = mode === "inline";
1999
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
2000
+ if (isExport && !activeGroupName) {
2001
+ return {
2002
+ height: 0,
2003
+ width: 0,
2004
+ rows: [],
2005
+ controls: [],
2006
+ pills: [],
2007
+ activeCapsule: void 0
2008
+ };
2009
+ }
2010
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2011
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2012
+ return {
2013
+ height: 0,
2014
+ width: 0,
2015
+ rows: [],
2016
+ controls: [],
2017
+ pills: [],
2018
+ activeCapsule: void 0
2019
+ };
2020
+ }
2021
+ const controlLayouts = [];
2022
+ let totalControlsW = 0;
2023
+ if (configControls && !isExport) {
2024
+ for (const ctrl of configControls) {
2025
+ const w = controlWidth(ctrl);
2026
+ controlLayouts.push({
2027
+ id: ctrl.id,
2028
+ x: 0,
2029
+ // positioned later
2030
+ y: 0,
2031
+ width: w,
2032
+ height: LEGEND_HEIGHT,
2033
+ icon: ctrl.icon,
2034
+ label: ctrl.label,
2035
+ exportBehavior: ctrl.exportBehavior,
2036
+ children: ctrl.children?.map((c) => ({
2037
+ id: c.id,
2038
+ label: c.label,
2039
+ x: 0,
2040
+ y: 0,
2041
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2042
+ isActive: c.isActive
2043
+ }))
2044
+ });
2045
+ totalControlsW += w + CONTROL_GAP;
2046
+ }
2047
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2048
+ } else if (configControls && isExport) {
2049
+ for (const ctrl of configControls) {
2050
+ if (ctrl.exportBehavior === "strip") continue;
2051
+ const w = controlWidth(ctrl);
2052
+ controlLayouts.push({
2053
+ id: ctrl.id,
2054
+ x: 0,
2055
+ y: 0,
2056
+ width: w,
2057
+ height: LEGEND_HEIGHT,
2058
+ icon: ctrl.icon,
2059
+ label: ctrl.label,
2060
+ exportBehavior: ctrl.exportBehavior,
2061
+ children: ctrl.children?.map((c) => ({
2062
+ id: c.id,
2063
+ label: c.label,
2064
+ x: 0,
2065
+ y: 0,
2066
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2067
+ isActive: c.isActive
2068
+ }))
2069
+ });
2070
+ totalControlsW += w + CONTROL_GAP;
2071
+ }
2072
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2073
+ }
2074
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2075
+ const groupAvailW = containerWidth - controlsSpace;
2076
+ const pills = [];
2077
+ let activeCapsule;
2078
+ for (const g of visibleGroups) {
2079
+ const isActive = activeGroupName === g.name.toLowerCase();
2080
+ if (isExport && !isActive) continue;
2081
+ if (isActive) {
2082
+ activeCapsule = buildCapsuleLayout(
2083
+ g,
2084
+ containerWidth,
2085
+ config.capsulePillAddonWidth ?? 0
2086
+ );
2087
+ } else {
2088
+ const pw = pillWidth(g.name);
2089
+ pills.push({
2090
+ groupName: g.name,
2091
+ x: 0,
2092
+ y: 0,
2093
+ width: pw,
2094
+ height: LEGEND_HEIGHT,
2095
+ isActive: false
2096
+ });
2097
+ }
2098
+ }
2099
+ const rows = layoutRows(
2100
+ activeCapsule,
2101
+ pills,
2102
+ controlLayouts,
2103
+ groupAvailW,
2104
+ containerWidth,
2105
+ totalControlsW
2106
+ );
2107
+ const height = rows.length * LEGEND_HEIGHT;
2108
+ const width = containerWidth;
2109
+ return {
2110
+ height,
2111
+ width,
2112
+ rows,
2113
+ activeCapsule,
2114
+ controls: controlLayouts,
2115
+ pills
2116
+ };
2117
+ }
2118
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2119
+ const pw = pillWidth(group.name);
2120
+ const info = capsuleWidth(
2121
+ group.name,
2122
+ group.entries,
2123
+ containerWidth,
2124
+ addonWidth
2125
+ );
2126
+ const pill = {
2127
+ groupName: group.name,
2128
+ x: LEGEND_CAPSULE_PAD,
2129
+ y: LEGEND_CAPSULE_PAD,
2130
+ width: pw,
2131
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2132
+ isActive: true
2133
+ };
2134
+ const entries = [];
2135
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2136
+ let ey = 0;
2137
+ let rowX = ex;
2138
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2139
+ let currentRow = 0;
2140
+ for (let i = 0; i < info.visibleEntries; i++) {
2141
+ const entry = group.entries[i];
2142
+ const ew = entryWidth(entry.value);
2143
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2144
+ currentRow++;
2145
+ rowX = 0;
2146
+ ey = currentRow * LEGEND_HEIGHT;
2147
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2148
+ }
2149
+ const dotCx = rowX + LEGEND_DOT_R;
2150
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2151
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2152
+ const textY = ey + LEGEND_HEIGHT / 2;
2153
+ entries.push({
2154
+ value: entry.value,
2155
+ color: entry.color,
2156
+ x: rowX,
2157
+ y: ey,
2158
+ dotCx,
2159
+ dotCy,
2160
+ textX,
2161
+ textY
2162
+ });
2163
+ rowX += ew;
2164
+ }
2165
+ const totalRows = info.entryRows;
2166
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2167
+ return {
2168
+ groupName: group.name,
2169
+ x: 0,
2170
+ y: 0,
2171
+ width: info.width,
2172
+ height: capsuleH,
2173
+ pill,
2174
+ entries,
2175
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2176
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2177
+ };
2178
+ }
2179
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2180
+ const rows = [];
2181
+ const groupItems = [];
2182
+ if (activeCapsule) groupItems.push(activeCapsule);
2183
+ groupItems.push(...pills);
2184
+ let currentRowItems = [];
2185
+ let currentRowW = 0;
2186
+ let rowY = 0;
2187
+ for (const item of groupItems) {
2188
+ const itemW = item.width + LEGEND_GROUP_GAP;
2189
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2190
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2191
+ rows.push({ y: rowY, items: currentRowItems });
2192
+ rowY += LEGEND_HEIGHT;
2193
+ currentRowItems = [];
2194
+ currentRowW = 0;
2195
+ }
2196
+ item.x = currentRowW;
2197
+ item.y = rowY;
2198
+ currentRowItems.push(item);
2199
+ currentRowW += itemW;
2200
+ }
2201
+ if (controls.length > 0) {
2202
+ let cx = containerWidth;
2203
+ for (let i = controls.length - 1; i >= 0; i--) {
2204
+ cx -= controls[i].width;
2205
+ controls[i].x = cx;
2206
+ controls[i].y = 0;
2207
+ cx -= CONTROL_GAP;
2208
+ }
2209
+ if (rows.length > 0) {
2210
+ rows[0].items.push(...controls);
2211
+ } else if (currentRowItems.length > 0) {
2212
+ currentRowItems.push(...controls);
2213
+ } else {
2214
+ currentRowItems.push(...controls);
2215
+ }
2216
+ }
2217
+ if (currentRowItems.length > 0) {
2218
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2219
+ rows.push({ y: rowY, items: currentRowItems });
2220
+ }
2221
+ if (rows.length === 0) {
2222
+ rows.push({ y: 0, items: [] });
2223
+ }
2224
+ return rows;
2225
+ }
2226
+ function centerRowItems(items, containerWidth, totalControlsW) {
2227
+ const groupItems = items.filter((it) => "groupName" in it);
2228
+ if (groupItems.length === 0) return;
2229
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2230
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2231
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2232
+ let x = offset;
2233
+ for (const item of groupItems) {
2234
+ item.x = x;
2235
+ x += item.width + LEGEND_GROUP_GAP;
2236
+ }
2237
+ }
2238
+ function getLegendReservedHeight(config, state, containerWidth) {
2239
+ const layout = computeLegendLayout(config, state, containerWidth);
2240
+ return layout.height;
2241
+ }
2242
+ var LEGEND_MAX_ENTRY_ROWS, CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2243
+ var init_legend_layout = __esm({
2244
+ "src/utils/legend-layout.ts"() {
2245
+ "use strict";
2246
+ init_legend_constants();
2247
+ LEGEND_MAX_ENTRY_ROWS = 3;
2248
+ CONTROL_PILL_PAD = 16;
2249
+ CONTROL_FONT_SIZE = 11;
2250
+ CONTROL_ICON_GAP = 4;
2251
+ CONTROL_GAP = 8;
2252
+ }
2253
+ });
2254
+
2255
+ // src/utils/legend-d3.ts
2256
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2257
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2258
+ let currentState = { ...state };
2259
+ let currentLayout;
2260
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2261
+ function render2() {
2262
+ currentLayout = computeLegendLayout(config, currentState, width);
2263
+ legendG.selectAll("*").remove();
2264
+ if (currentLayout.height === 0) return;
2265
+ if (currentState.activeGroup) {
2266
+ legendG.attr(
2267
+ "data-legend-active",
2268
+ currentState.activeGroup.toLowerCase()
2269
+ );
2270
+ } else {
2271
+ legendG.attr("data-legend-active", null);
2272
+ }
2273
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2274
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2275
+ if (currentLayout.activeCapsule) {
2276
+ renderCapsule(
2277
+ legendG,
2278
+ currentLayout.activeCapsule,
2279
+ palette,
2280
+ groupBg,
2281
+ pillBorder,
2282
+ isDark,
2283
+ callbacks
2284
+ );
2285
+ }
2286
+ for (const pill of currentLayout.pills) {
2287
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2288
+ }
2289
+ for (const ctrl of currentLayout.controls) {
2290
+ renderControl(
2291
+ legendG,
2292
+ ctrl,
2293
+ palette,
2294
+ groupBg,
2295
+ pillBorder,
2296
+ isDark,
2297
+ config.controls
2298
+ );
2299
+ }
2300
+ }
2301
+ render2();
2302
+ return {
2303
+ setState(newState) {
2304
+ currentState = { ...newState };
2305
+ render2();
2306
+ },
2307
+ destroy() {
2308
+ legendG.remove();
2309
+ },
2310
+ getHeight() {
2311
+ return currentLayout?.height ?? 0;
2312
+ },
2313
+ getLayout() {
2314
+ return currentLayout;
2315
+ }
2316
+ };
2317
+ }
2318
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2319
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2320
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2321
+ const pill = capsule.pill;
2322
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
2323
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2324
+ g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
2325
+ for (const entry of capsule.entries) {
2326
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2327
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2328
+ entryG.append("text").attr("x", entry.textX).attr("y", entry.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.value);
2329
+ if (callbacks?.onEntryHover) {
2330
+ const groupName = capsule.groupName;
2331
+ const entryValue = entry.value;
2332
+ const onHover = callbacks.onEntryHover;
2333
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2334
+ }
2335
+ }
2336
+ if (capsule.moreCount) {
2337
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2338
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2339
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2340
+ g.append("text").attr("x", moreX).attr("y", moreY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-style", "italic").attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(`+${capsule.moreCount} more`);
2341
+ }
2342
+ if (callbacks?.onGroupToggle) {
2343
+ const cb = callbacks.onGroupToggle;
2344
+ const name = capsule.groupName;
2345
+ g.on("click", () => cb(name));
2346
+ }
2347
+ if (callbacks?.onGroupRendered) {
2348
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2349
+ }
2350
+ }
2351
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2352
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2353
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2354
+ g.append("text").attr("x", pill.width / 2).attr("y", pill.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(pill.groupName);
2355
+ if (callbacks?.onGroupToggle) {
2356
+ const cb = callbacks.onGroupToggle;
2357
+ const name = pill.groupName;
2358
+ g.on("click", () => cb(name));
2359
+ }
2360
+ if (callbacks?.onGroupRendered) {
2361
+ callbacks.onGroupRendered(pill.groupName, g, false);
2362
+ }
2363
+ }
2364
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2365
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2366
+ if (ctrl.exportBehavior === "strip") {
2367
+ g.attr("data-export-ignore", "true");
2368
+ }
2369
+ g.append("rect").attr("width", ctrl.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2370
+ let textX = ctrl.width / 2;
2371
+ if (ctrl.icon && ctrl.label) {
2372
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2373
+ iconG.html(ctrl.icon);
2374
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2375
+ }
2376
+ if (ctrl.label) {
2377
+ g.append("text").attr("x", textX).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(ctrl.label);
2378
+ }
2379
+ if (ctrl.children) {
2380
+ let cx = ctrl.width + 4;
2381
+ for (const child of ctrl.children) {
2382
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2383
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2384
+ "fill",
2385
+ child.isActive ? palette.primary ?? palette.text : "none"
2386
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2387
+ childG.append("text").attr("x", child.width / 2).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", child.isActive ? palette.bg : palette.textMuted).attr("font-family", FONT_FAMILY).text(child.label);
2388
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2389
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2390
+ if (configChild?.onClick) {
2391
+ const onClick = configChild.onClick;
2392
+ childG.on("click", () => onClick());
2393
+ }
2394
+ cx += child.width + 4;
2395
+ }
2396
+ }
2397
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2398
+ if (configCtrl?.onClick) {
2399
+ const onClick = configCtrl.onClick;
2400
+ g.on("click", () => onClick());
2401
+ }
2402
+ }
2403
+ var init_legend_d3 = __esm({
2404
+ "src/utils/legend-d3.ts"() {
2405
+ "use strict";
2406
+ init_legend_constants();
2407
+ init_legend_layout();
2408
+ init_color_utils();
2409
+ init_fonts();
2410
+ }
2411
+ });
2412
+
1926
2413
  // src/utils/title-constants.ts
1927
2414
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1928
2415
  var init_title_constants = __esm({
@@ -4852,10 +5339,10 @@ var init_chart = __esm({
4852
5339
  function esc(s) {
4853
5340
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4854
5341
  }
4855
- function pillWidth(name) {
5342
+ function pillWidth2(name) {
4856
5343
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4857
5344
  }
4858
- function entriesWidth(entries) {
5345
+ function entriesWidth2(entries) {
4859
5346
  let w = 0;
4860
5347
  for (const e of entries) {
4861
5348
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4863,9 +5350,9 @@ function entriesWidth(entries) {
4863
5350
  return w;
4864
5351
  }
4865
5352
  function groupTotalWidth(name, entries, isActive) {
4866
- const pw = pillWidth(name);
5353
+ const pw = pillWidth2(name);
4867
5354
  if (!isActive) return pw;
4868
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5355
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4869
5356
  }
4870
5357
  function renderLegendSvg(groups, options) {
4871
5358
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4873,7 +5360,7 @@ function renderLegendSvg(groups, options) {
4873
5360
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4874
5361
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4875
5362
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4876
- const pw = pillWidth(g.name);
5363
+ const pw = pillWidth2(g.name);
4877
5364
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4878
5365
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4879
5366
  });
@@ -4927,6 +5414,19 @@ function renderLegendSvg(groups, options) {
4927
5414
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4928
5415
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4929
5416
  }
5417
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5418
+ return renderLegendSvg(config.groups, {
5419
+ palette: {
5420
+ bg: palette.bg,
5421
+ surface: palette.surface,
5422
+ text: palette.text,
5423
+ textMuted: palette.textMuted
5424
+ },
5425
+ isDark: palette.isDark,
5426
+ containerWidth,
5427
+ activeGroup: state.activeGroup
5428
+ });
5429
+ }
4930
5430
  var init_legend_svg = __esm({
4931
5431
  "src/utils/legend-svg.ts"() {
4932
5432
  "use strict";
@@ -5355,52 +5855,26 @@ function buildChartCommons(parsed, palette, isDark) {
5355
5855
  fontFamily: FONT_FAMILY
5356
5856
  }
5357
5857
  } : void 0;
5358
- const tooltipTheme = {
5359
- backgroundColor: palette.surface,
5360
- borderColor: palette.border,
5361
- textStyle: { color: palette.text }
5362
- };
5363
5858
  return {
5364
5859
  textColor,
5365
5860
  axisLineColor,
5366
5861
  splitLineColor,
5367
5862
  gridOpacity,
5368
5863
  colors,
5369
- titleConfig,
5370
- tooltipTheme
5864
+ titleConfig
5371
5865
  };
5372
5866
  }
5373
5867
  function buildExtendedChartOption(parsed, palette, isDark) {
5374
5868
  if (parsed.error) {
5375
5869
  return {};
5376
5870
  }
5377
- const {
5378
- textColor,
5379
- axisLineColor,
5380
- gridOpacity,
5381
- colors,
5382
- titleConfig,
5383
- tooltipTheme
5384
- } = buildChartCommons(parsed, palette, isDark);
5871
+ const { textColor, axisLineColor, gridOpacity, colors, titleConfig } = buildChartCommons(parsed, palette, isDark);
5385
5872
  if (parsed.type === "sankey") {
5386
- return buildSankeyOption(
5387
- parsed,
5388
- textColor,
5389
- colors,
5390
- titleConfig,
5391
- tooltipTheme
5392
- );
5873
+ return buildSankeyOption(parsed, textColor, colors, titleConfig);
5393
5874
  }
5394
5875
  if (parsed.type === "chord") {
5395
5876
  const bg = isDark ? palette.surface : palette.bg;
5396
- return buildChordOption(
5397
- parsed,
5398
- textColor,
5399
- colors,
5400
- bg,
5401
- titleConfig,
5402
- tooltipTheme
5403
- );
5877
+ return buildChordOption(parsed, textColor, colors, bg, titleConfig);
5404
5878
  }
5405
5879
  if (parsed.type === "function") {
5406
5880
  return buildFunctionOption(
@@ -5410,8 +5884,7 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5410
5884
  axisLineColor,
5411
5885
  gridOpacity,
5412
5886
  colors,
5413
- titleConfig,
5414
- tooltipTheme
5887
+ titleConfig
5415
5888
  );
5416
5889
  }
5417
5890
  if (parsed.type === "scatter") {
@@ -5424,20 +5897,12 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5424
5897
  gridOpacity,
5425
5898
  colors,
5426
5899
  bg,
5427
- titleConfig,
5428
- tooltipTheme
5900
+ titleConfig
5429
5901
  );
5430
5902
  }
5431
5903
  if (parsed.type === "funnel") {
5432
5904
  const bg = isDark ? palette.surface : palette.bg;
5433
- return buildFunnelOption(
5434
- parsed,
5435
- textColor,
5436
- colors,
5437
- bg,
5438
- titleConfig,
5439
- tooltipTheme
5440
- );
5905
+ return buildFunnelOption(parsed, textColor, colors, bg, titleConfig);
5441
5906
  }
5442
5907
  return buildHeatmapOption(
5443
5908
  parsed,
@@ -5445,11 +5910,10 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5445
5910
  isDark,
5446
5911
  textColor,
5447
5912
  axisLineColor,
5448
- titleConfig,
5449
- tooltipTheme
5913
+ titleConfig
5450
5914
  );
5451
5915
  }
5452
- function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme) {
5916
+ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5453
5917
  const nodeSet = /* @__PURE__ */ new Set();
5454
5918
  if (parsed.links) {
5455
5919
  for (const link of parsed.links) {
@@ -5468,17 +5932,15 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme)
5468
5932
  title: titleConfig,
5469
5933
  xAxis: { show: false },
5470
5934
  yAxis: { show: false },
5471
- tooltip: {
5472
- show: false,
5473
- ...tooltipTheme
5474
- },
5475
5935
  series: [
5476
5936
  {
5477
5937
  type: "sankey",
5478
5938
  emphasis: {
5479
5939
  focus: "adjacency",
5480
- blurScope: "global"
5940
+ blurScope: "global",
5941
+ itemStyle: { opacity: 1 }
5481
5942
  },
5943
+ blur: BLUR_DIM,
5482
5944
  nodeAlign: "left",
5483
5945
  nodeGap: 12,
5484
5946
  nodeWidth: 20,
@@ -5501,7 +5963,7 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme)
5501
5963
  ]
5502
5964
  };
5503
5965
  }
5504
- function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
5966
+ function buildChordOption(parsed, textColor, colors, bg, titleConfig) {
5505
5967
  const nodeSet = /* @__PURE__ */ new Set();
5506
5968
  if (parsed.links) {
5507
5969
  for (const link of parsed.links) {
@@ -5535,17 +5997,6 @@ function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipThe
5535
5997
  return {
5536
5998
  ...CHART_BASE,
5537
5999
  title: titleConfig,
5538
- tooltip: {
5539
- trigger: "item",
5540
- ...tooltipTheme,
5541
- formatter: (params) => {
5542
- const p = params;
5543
- if (p.data && p.data.source && p.data.target) {
5544
- return `${p.data.source} \u2192 ${p.data.target}: ${p.data.value}`;
5545
- }
5546
- return "";
5547
- }
5548
- },
5549
6000
  xAxis: { show: false },
5550
6001
  yAxis: { show: false },
5551
6002
  series: [
@@ -5604,11 +6055,13 @@ function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipThe
5604
6055
  },
5605
6056
  emphasis: {
5606
6057
  focus: "adjacency",
6058
+ itemStyle: { opacity: 1 },
5607
6059
  lineStyle: {
5608
6060
  width: 5,
5609
6061
  opacity: 1
5610
6062
  }
5611
- }
6063
+ },
6064
+ blur: BLUR_DIM
5612
6065
  }
5613
6066
  ]
5614
6067
  };
@@ -5622,7 +6075,7 @@ function evaluateExpression(expr, x) {
5622
6075
  return NaN;
5623
6076
  }
5624
6077
  }
5625
- function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, titleConfig, tooltipTheme) {
6078
+ function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, titleConfig) {
5626
6079
  const xRange = parsed.xRange ?? { min: -10, max: 10 };
5627
6080
  const samples = 200;
5628
6081
  const step = (xRange.max - xRange.min) / samples;
@@ -5655,19 +6108,13 @@ function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpac
5655
6108
  opacity: 0.15
5656
6109
  }
5657
6110
  },
5658
- emphasis: EMPHASIS_SELF
6111
+ emphasis: EMPHASIS_SELF,
6112
+ blur: BLUR_DIM
5659
6113
  };
5660
6114
  });
5661
6115
  return {
5662
6116
  ...CHART_BASE,
5663
6117
  title: titleConfig,
5664
- tooltip: {
5665
- trigger: "axis",
5666
- ...tooltipTheme,
5667
- axisPointer: {
5668
- type: "cross"
5669
- }
5670
- },
5671
6118
  legend: {
5672
6119
  data: (parsed.functions ?? []).map((fn) => fn.name),
5673
6120
  bottom: 10,
@@ -5916,7 +6363,7 @@ function dataToPixel(dataX, dataY, xMin, xMax, yMin, yMax, gridLeftPct, gridRigh
5916
6363
  const py = gridTopPx + (yMax - dataY) / (yMax - yMin) * plotHeight;
5917
6364
  return { px, py };
5918
6365
  }
5919
- function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme) {
6366
+ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, bg, titleConfig) {
5920
6367
  const points = parsed.scatterPoints ?? [];
5921
6368
  const defaultSize = 15;
5922
6369
  const hasCategories = points.some((p) => p.category !== void 0);
@@ -5932,11 +6379,9 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5932
6379
  };
5933
6380
  const emphasisConfig = {
5934
6381
  focus: "self",
5935
- itemStyle: {
5936
- shadowBlur: 10,
5937
- shadowColor: "rgba(0, 0, 0, 0.3)"
5938
- }
6382
+ itemStyle: { opacity: 1 }
5939
6383
  };
6384
+ const blurConfig = BLUR_DIM;
5940
6385
  let series;
5941
6386
  if (hasCategories) {
5942
6387
  const categories2 = [
@@ -5967,7 +6412,8 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5967
6412
  borderWidth: CHART_BORDER_WIDTH
5968
6413
  },
5969
6414
  label: labelConfig,
5970
- emphasis: emphasisConfig
6415
+ emphasis: emphasisConfig,
6416
+ blur: blurConfig
5971
6417
  };
5972
6418
  });
5973
6419
  } else {
@@ -5989,24 +6435,11 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5989
6435
  type: "scatter",
5990
6436
  data,
5991
6437
  label: labelConfig,
5992
- emphasis: emphasisConfig
6438
+ emphasis: emphasisConfig,
6439
+ blur: blurConfig
5993
6440
  }
5994
6441
  ];
5995
6442
  }
5996
- const tooltip = {
5997
- trigger: "item",
5998
- ...tooltipTheme,
5999
- formatter: (params) => {
6000
- const p = params;
6001
- const xLabel = parsed.xlabel || "x";
6002
- const yLabel = parsed.ylabel || "y";
6003
- let html = `<strong>${p.name}</strong>`;
6004
- if (hasCategories) html += `<br/>${p.seriesName}`;
6005
- html += `<br/>${xLabel}: ${p.value[0]}<br/>${yLabel}: ${p.value[1]}`;
6006
- if (hasSize) html += `<br/>${parsed.sizelabel || "size"}: ${p.value[2]}`;
6007
- return html;
6008
- }
6009
- };
6010
6443
  const xValues = points.map((p) => p.x);
6011
6444
  const yValues = points.map((p) => p.y);
6012
6445
  const xMin = Math.min(...xValues);
@@ -6087,7 +6520,6 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
6087
6520
  ...CHART_BASE,
6088
6521
  title: titleConfig,
6089
6522
  ...legendConfig && { legend: legendConfig },
6090
- tooltip,
6091
6523
  grid: {
6092
6524
  left: `${gridLeft}%`,
6093
6525
  right: `${gridRight}%`,
@@ -6149,7 +6581,7 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
6149
6581
  ...graphic && { graphic }
6150
6582
  };
6151
6583
  }
6152
- function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, titleConfig, tooltipTheme) {
6584
+ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, titleConfig) {
6153
6585
  const bg = isDark ? palette.surface : palette.bg;
6154
6586
  const heatmapRows = parsed.heatmapRows ?? [];
6155
6587
  const columns = parsed.columns ?? [];
@@ -6167,16 +6599,6 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6167
6599
  return {
6168
6600
  ...CHART_BASE,
6169
6601
  title: titleConfig,
6170
- tooltip: {
6171
- trigger: "item",
6172
- ...tooltipTheme,
6173
- formatter: (params) => {
6174
- const p = params;
6175
- const colName = columns[p.data[0]] ?? p.data[0];
6176
- const rowName = rowLabels[p.data[1]] ?? p.data[1];
6177
- return `${rowName} / ${colName}: <strong>${p.data[2]}</strong>`;
6178
- }
6179
- },
6180
6602
  grid: {
6181
6603
  left: "3%",
6182
6604
  right: "10%",
@@ -6246,19 +6668,15 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6246
6668
  fontWeight: "bold"
6247
6669
  },
6248
6670
  emphasis: {
6249
- ...EMPHASIS_SELF,
6250
- itemStyle: {
6251
- shadowBlur: 10,
6252
- shadowColor: "rgba(0, 0, 0, 0.5)"
6253
- }
6254
- }
6671
+ ...EMPHASIS_SELF
6672
+ },
6673
+ blur: BLUR_DIM
6255
6674
  }
6256
6675
  ]
6257
6676
  };
6258
6677
  }
6259
- function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
6678
+ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig) {
6260
6679
  const sorted = [...parsed.data].sort((a, b) => b.value - a.value);
6261
- const topValue = sorted.length > 0 ? sorted[0].value : 1;
6262
6680
  const data = sorted.map((d) => {
6263
6681
  const stroke2 = d.color ?? colors[parsed.data.indexOf(d) % colors.length];
6264
6682
  return {
@@ -6293,25 +6711,6 @@ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTh
6293
6711
  title: titleConfig,
6294
6712
  xAxis: { show: false },
6295
6713
  yAxis: { show: false },
6296
- tooltip: {
6297
- trigger: "item",
6298
- ...tooltipTheme,
6299
- formatter: (params) => {
6300
- const p = params;
6301
- const val = p.value;
6302
- const prev = prevValueMap.get(p.name) ?? val;
6303
- const isFirst = p.dataIndex === 0;
6304
- if (isFirst) return "";
6305
- const parts = [];
6306
- const stepDrop = ((1 - val / prev) * 100).toFixed(1);
6307
- parts.push(`Step drop-off: ${stepDrop}%`);
6308
- if (topValue > 0) {
6309
- const totalDrop = ((1 - val / topValue) * 100).toFixed(1);
6310
- parts.push(`Overall drop-off: ${totalDrop}%`);
6311
- }
6312
- return parts.join("<br/>");
6313
- }
6314
- },
6315
6714
  series: [
6316
6715
  {
6317
6716
  type: "funnel",
@@ -6329,11 +6728,9 @@ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTh
6329
6728
  lineStyle: { color: textColor, opacity: 0.3 }
6330
6729
  },
6331
6730
  emphasis: {
6332
- ...EMPHASIS_SELF,
6333
- label: {
6334
- fontSize: 15
6335
- }
6731
+ ...EMPHASIS_SELF
6336
6732
  },
6733
+ blur: BLUR_DIM,
6337
6734
  data
6338
6735
  },
6339
6736
  {
@@ -6427,8 +6824,7 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6427
6824
  splitLineColor,
6428
6825
  gridOpacity,
6429
6826
  colors,
6430
- titleConfig,
6431
- tooltipTheme
6827
+ titleConfig
6432
6828
  } = buildChartCommons(parsed, palette, isDark);
6433
6829
  const bg = isDark ? palette.surface : palette.bg;
6434
6830
  switch (parsed.type) {
@@ -6442,7 +6838,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6442
6838
  colors,
6443
6839
  bg,
6444
6840
  titleConfig,
6445
- tooltipTheme,
6446
6841
  chartWidth
6447
6842
  );
6448
6843
  case "bar-stacked":
@@ -6455,7 +6850,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6455
6850
  colors,
6456
6851
  bg,
6457
6852
  titleConfig,
6458
- tooltipTheme,
6459
6853
  chartWidth
6460
6854
  );
6461
6855
  case "line":
@@ -6468,7 +6862,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6468
6862
  gridOpacity,
6469
6863
  colors,
6470
6864
  titleConfig,
6471
- tooltipTheme,
6472
6865
  chartWidth
6473
6866
  ) : buildLineOption(
6474
6867
  parsed,
@@ -6478,7 +6871,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6478
6871
  splitLineColor,
6479
6872
  gridOpacity,
6480
6873
  titleConfig,
6481
- tooltipTheme,
6482
6874
  chartWidth
6483
6875
  );
6484
6876
  case "area":
@@ -6490,7 +6882,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6490
6882
  splitLineColor,
6491
6883
  gridOpacity,
6492
6884
  titleConfig,
6493
- tooltipTheme,
6494
6885
  chartWidth
6495
6886
  );
6496
6887
  case "pie":
@@ -6500,7 +6891,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6500
6891
  getSegmentColors(palette, parsed.data.length),
6501
6892
  bg,
6502
6893
  titleConfig,
6503
- tooltipTheme,
6504
6894
  false
6505
6895
  );
6506
6896
  case "doughnut":
@@ -6510,7 +6900,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6510
6900
  getSegmentColors(palette, parsed.data.length),
6511
6901
  bg,
6512
6902
  titleConfig,
6513
- tooltipTheme,
6514
6903
  true
6515
6904
  );
6516
6905
  case "radar":
@@ -6520,8 +6909,7 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6520
6909
  isDark,
6521
6910
  textColor,
6522
6911
  gridOpacity,
6523
- titleConfig,
6524
- tooltipTheme
6912
+ titleConfig
6525
6913
  );
6526
6914
  case "polar-area":
6527
6915
  return buildPolarAreaOption(
@@ -6529,21 +6917,36 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6529
6917
  textColor,
6530
6918
  getSegmentColors(palette, parsed.data.length),
6531
6919
  bg,
6532
- titleConfig,
6533
- tooltipTheme
6920
+ titleConfig
6534
6921
  );
6535
6922
  }
6536
6923
  }
6537
6924
  function makeChartGrid(options) {
6925
+ const left = options.yLabel ? "12%" : "3%";
6538
6926
  return {
6539
- left: options.yLabel ? "12%" : "3%",
6927
+ left,
6540
6928
  right: "4%",
6541
6929
  bottom: options.hasLegend ? "15%" : options.xLabel ? "10%" : "3%",
6542
6930
  top: options.hasTitle ? "15%" : "5%",
6543
6931
  containLabel: true
6544
6932
  };
6545
6933
  }
6546
- function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth) {
6934
+ function wrapLabel(text, maxChars) {
6935
+ const words = text.split(" ");
6936
+ const lines = [];
6937
+ let current = "";
6938
+ for (const word of words) {
6939
+ if (current && current.length + 1 + word.length > maxChars) {
6940
+ lines.push(current);
6941
+ current = word;
6942
+ } else {
6943
+ current = current ? current + " " + word : word;
6944
+ }
6945
+ }
6946
+ if (current) lines.push(current);
6947
+ return lines.join("\n");
6948
+ }
6949
+ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, chartWidth) {
6547
6950
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6548
6951
  const isHorizontal = parsed.orientation === "horizontal";
6549
6952
  const labels = parsed.data.map((d) => d.label);
@@ -6558,7 +6961,11 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
6558
6961
  }
6559
6962
  };
6560
6963
  });
6561
- const hCatGap = isHorizontal && yLabel ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16) : void 0;
6964
+ const catLabels = isHorizontal ? labels.map((l) => wrapLabel(l, 12)) : labels;
6965
+ const maxVisibleLen = Math.max(
6966
+ ...catLabels.map((l) => Math.max(...l.split("\n").map((seg) => seg.length)))
6967
+ );
6968
+ const hCatGap = isHorizontal && yLabel ? Math.max(40, maxVisibleLen * 8 + 16) : void 0;
6562
6969
  const categoryAxis = makeGridAxis(
6563
6970
  "category",
6564
6971
  textColor,
@@ -6566,29 +6973,38 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
6566
6973
  splitLineColor,
6567
6974
  gridOpacity,
6568
6975
  isHorizontal ? yLabel : xLabel,
6569
- labels,
6976
+ catLabels,
6570
6977
  hCatGap,
6571
6978
  !isHorizontal ? chartWidth : void 0
6572
6979
  );
6980
+ const hValueGap = isHorizontal && xLabel ? 40 : void 0;
6573
6981
  const valueAxis = makeGridAxis(
6574
6982
  "value",
6575
6983
  textColor,
6576
6984
  axisLineColor,
6577
6985
  splitLineColor,
6578
6986
  gridOpacity,
6579
- isHorizontal ? xLabel : yLabel
6987
+ isHorizontal ? xLabel : yLabel,
6988
+ void 0,
6989
+ hValueGap
6580
6990
  );
6581
6991
  return {
6582
6992
  ...CHART_BASE,
6583
6993
  title: titleConfig,
6584
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6994
+ grid: makeChartGrid({
6995
+ xLabel,
6996
+ yLabel,
6997
+ hasTitle: !!parsed.title,
6998
+ isHorizontal
6999
+ }),
6585
7000
  xAxis: isHorizontal ? valueAxis : categoryAxis,
6586
- yAxis: isHorizontal ? categoryAxis : valueAxis,
7001
+ yAxis: isHorizontal ? { ...categoryAxis, inverse: true } : valueAxis,
6587
7002
  series: [
6588
7003
  {
6589
7004
  type: "bar",
6590
7005
  data,
6591
- emphasis: EMPHASIS_SELF
7006
+ emphasis: EMPHASIS_SELF,
7007
+ blur: BLUR_DIM
6592
7008
  }
6593
7009
  ]
6594
7010
  };
@@ -6605,7 +7021,7 @@ function buildMarkArea(eras, labels, textColor, defaultColor) {
6605
7021
  if (eras.length === 0) return void 0;
6606
7022
  return {
6607
7023
  silent: false,
6608
- tooltip: { show: true },
7024
+ tooltip: { show: false },
6609
7025
  data: eras.map((era) => {
6610
7026
  const startIdx = labels.indexOf(era.start);
6611
7027
  const endIdx = labels.indexOf(era.end);
@@ -6628,7 +7044,7 @@ function buildMarkArea(eras, labels, textColor, defaultColor) {
6628
7044
  })
6629
7045
  };
6630
7046
  }
6631
- function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth) {
7047
+ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, chartWidth) {
6632
7048
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6633
7049
  const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6634
7050
  const labels = parsed.data.map((d) => d.label);
@@ -6639,11 +7055,6 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
6639
7055
  return {
6640
7056
  ...CHART_BASE,
6641
7057
  title: titleConfig,
6642
- tooltip: {
6643
- trigger: "axis",
6644
- ...tooltipTheme,
6645
- axisPointer: { type: "line" }
6646
- },
6647
7058
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6648
7059
  xAxis: makeGridAxis(
6649
7060
  "category",
@@ -6674,12 +7085,13 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
6674
7085
  lineStyle: { color: lineColor, width: 3 },
6675
7086
  itemStyle: { color: lineColor },
6676
7087
  emphasis: EMPHASIS_LINE,
7088
+ blur: BLUR_DIM,
6677
7089
  ...markArea && { markArea }
6678
7090
  }
6679
7091
  ]
6680
7092
  };
6681
7093
  }
6682
- function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth) {
7094
+ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, chartWidth) {
6683
7095
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6684
7096
  const seriesNames = parsed.seriesNames ?? [];
6685
7097
  const labels = parsed.data.map((d) => d.label);
@@ -6700,17 +7112,13 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
6700
7112
  lineStyle: { color, width: 3 },
6701
7113
  itemStyle: { color },
6702
7114
  emphasis: EMPHASIS_LINE,
7115
+ blur: BLUR_DIM,
6703
7116
  ...idx === 0 && markArea && { markArea }
6704
7117
  };
6705
7118
  });
6706
7119
  return {
6707
7120
  ...CHART_BASE,
6708
7121
  title: titleConfig,
6709
- tooltip: {
6710
- trigger: "axis",
6711
- ...tooltipTheme,
6712
- axisPointer: { type: "line" }
6713
- },
6714
7122
  legend: {
6715
7123
  data: seriesNames,
6716
7124
  bottom: 10,
@@ -6745,7 +7153,7 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
6745
7153
  series
6746
7154
  };
6747
7155
  }
6748
- function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth) {
7156
+ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, chartWidth) {
6749
7157
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6750
7158
  const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6751
7159
  const labels = parsed.data.map((d) => d.label);
@@ -6756,11 +7164,6 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
6756
7164
  return {
6757
7165
  ...CHART_BASE,
6758
7166
  title: titleConfig,
6759
- tooltip: {
6760
- trigger: "axis",
6761
- ...tooltipTheme,
6762
- axisPointer: { type: "line" }
6763
- },
6764
7167
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6765
7168
  xAxis: makeGridAxis(
6766
7169
  "category",
@@ -6792,6 +7195,7 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
6792
7195
  itemStyle: { color: lineColor },
6793
7196
  areaStyle: { opacity: 0.25 },
6794
7197
  emphasis: EMPHASIS_LINE,
7198
+ blur: BLUR_DIM,
6795
7199
  ...markArea && { markArea }
6796
7200
  }
6797
7201
  ]
@@ -6823,7 +7227,7 @@ function pieLabelLayout(parsed) {
6823
7227
  if (maxLen > 18) return { outerRadius: 55, fontSize: 13 };
6824
7228
  return { outerRadius: 70, fontSize: 14 };
6825
7229
  }
6826
- function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme, isDoughnut) {
7230
+ function buildPieOption(parsed, textColor, colors, bg, titleConfig, isDoughnut) {
6827
7231
  const HIDE_AXES = { xAxis: { show: false }, yAxis: { show: false } };
6828
7232
  const data = parsed.data.map((d, i) => {
6829
7233
  const stroke2 = d.color ?? colors[i % colors.length];
@@ -6842,10 +7246,6 @@ function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme
6842
7246
  ...CHART_BASE,
6843
7247
  ...HIDE_AXES,
6844
7248
  title: titleConfig,
6845
- tooltip: {
6846
- trigger: "item",
6847
- ...tooltipTheme
6848
- },
6849
7249
  series: [
6850
7250
  {
6851
7251
  type: "pie",
@@ -6859,12 +7259,13 @@ function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme
6859
7259
  fontSize
6860
7260
  },
6861
7261
  labelLine: { show: true },
6862
- emphasis: EMPHASIS_SELF
7262
+ emphasis: EMPHASIS_SELF,
7263
+ blur: BLUR_DIM
6863
7264
  }
6864
7265
  ]
6865
7266
  };
6866
7267
  }
6867
- function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig, tooltipTheme) {
7268
+ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig) {
6868
7269
  const bg = isDark ? palette.surface : palette.bg;
6869
7270
  const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6870
7271
  const values = parsed.data.map((d) => d.value);
@@ -6878,10 +7279,6 @@ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, title
6878
7279
  title: titleConfig,
6879
7280
  xAxis: { show: false },
6880
7281
  yAxis: { show: false },
6881
- tooltip: {
6882
- trigger: "item",
6883
- ...tooltipTheme
6884
- },
6885
7282
  radar: {
6886
7283
  indicator,
6887
7284
  axisName: {
@@ -6918,12 +7315,13 @@ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, title
6918
7315
  }
6919
7316
  }
6920
7317
  ],
6921
- emphasis: EMPHASIS_SELF
7318
+ emphasis: EMPHASIS_SELF,
7319
+ blur: BLUR_DIM
6922
7320
  }
6923
7321
  ]
6924
7322
  };
6925
7323
  }
6926
- function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
7324
+ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig) {
6927
7325
  const data = parsed.data.map((d, i) => {
6928
7326
  const stroke2 = d.color ?? colors[i % colors.length];
6929
7327
  return {
@@ -6941,10 +7339,6 @@ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, toolti
6941
7339
  title: titleConfig,
6942
7340
  xAxis: { show: false },
6943
7341
  yAxis: { show: false },
6944
- tooltip: {
6945
- trigger: "item",
6946
- ...tooltipTheme
6947
- },
6948
7342
  series: [
6949
7343
  {
6950
7344
  type: "pie",
@@ -6962,12 +7356,13 @@ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, toolti
6962
7356
  fontSize: pieLabelLayout(parsed).fontSize
6963
7357
  },
6964
7358
  labelLine: { show: true },
6965
- emphasis: EMPHASIS_SELF
7359
+ emphasis: EMPHASIS_SELF,
7360
+ blur: BLUR_DIM
6966
7361
  }
6967
7362
  ]
6968
7363
  };
6969
7364
  }
6970
- function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth) {
7365
+ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, chartWidth) {
6971
7366
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6972
7367
  const isHorizontal = parsed.orientation === "horizontal";
6973
7368
  const seriesNames = parsed.seriesNames ?? [];
@@ -6996,7 +7391,8 @@ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor,
6996
7391
  fontWeight: "bold",
6997
7392
  fontFamily: FONT_FAMILY
6998
7393
  },
6999
- emphasis: EMPHASIS_SELF
7394
+ emphasis: EMPHASIS_SERIES,
7395
+ blur: BLUR_DIM
7000
7396
  };
7001
7397
  });
7002
7398
  const hCatGap = isHorizontal && yLabel ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16) : void 0;
@@ -7037,7 +7433,7 @@ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor,
7037
7433
  hasLegend: true
7038
7434
  }),
7039
7435
  xAxis: isHorizontal ? valueAxis : categoryAxis,
7040
- yAxis: isHorizontal ? categoryAxis : valueAxis,
7436
+ yAxis: isHorizontal ? { ...categoryAxis, inverse: true } : valueAxis,
7041
7437
  series
7042
7438
  };
7043
7439
  }
@@ -7121,7 +7517,7 @@ async function renderExtendedChartForExport(content, theme, palette, options) {
7121
7517
  chart.dispose();
7122
7518
  }
7123
7519
  }
7124
- var echarts, EMPHASIS_SELF, EMPHASIS_LINE, CHART_BASE, CHART_BORDER_WIDTH, VALID_EXTENDED_TYPES, KNOWN_EXTENDED_OPTIONS, ECHART_EXPORT_WIDTH, ECHART_EXPORT_HEIGHT, STANDARD_CHART_TYPES;
7520
+ var echarts, EMPHASIS_SELF, EMPHASIS_SERIES, BLUR_DIM, EMPHASIS_LINE, CHART_BASE, CHART_BORDER_WIDTH, VALID_EXTENDED_TYPES, KNOWN_EXTENDED_OPTIONS, ECHART_EXPORT_WIDTH, ECHART_EXPORT_HEIGHT, STANDARD_CHART_TYPES;
7125
7521
  var init_echarts = __esm({
7126
7522
  "src/echarts.ts"() {
7127
7523
  "use strict";
@@ -7136,20 +7532,22 @@ var init_echarts = __esm({
7136
7532
  init_colors();
7137
7533
  init_parsing();
7138
7534
  init_chart();
7139
- EMPHASIS_SELF = { focus: "self", blurScope: "global" };
7140
- EMPHASIS_LINE = {
7141
- ...EMPHASIS_SELF,
7142
- scale: 2.5,
7143
- itemStyle: {
7144
- borderWidth: 2,
7145
- borderColor: "#fff",
7146
- shadowBlur: 8,
7147
- shadowColor: "rgba(0,0,0,0.4)"
7148
- }
7535
+ EMPHASIS_SELF = {
7536
+ focus: "self",
7537
+ blurScope: "global",
7538
+ itemStyle: { opacity: 1 }
7539
+ };
7540
+ EMPHASIS_SERIES = {
7541
+ focus: "series",
7542
+ blurScope: "global",
7543
+ itemStyle: { opacity: 1 }
7149
7544
  };
7545
+ BLUR_DIM = { itemStyle: { opacity: 0.15 }, lineStyle: { opacity: 0.15 } };
7546
+ EMPHASIS_LINE = { ...EMPHASIS_SELF };
7150
7547
  CHART_BASE = {
7151
7548
  backgroundColor: "transparent",
7152
- animation: false
7549
+ animation: false,
7550
+ tooltip: { show: false }
7153
7551
  };
7154
7552
  CHART_BORDER_WIDTH = 2;
7155
7553
  VALID_EXTENDED_TYPES = /* @__PURE__ */ new Set([
@@ -8481,7 +8879,12 @@ __export(parser_exports7, {
8481
8879
  function parseArrowLine(trimmed, palette) {
8482
8880
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8483
8881
  if (bareMatch) {
8484
- return { target: bareMatch[1].trim() };
8882
+ const rawTarget = bareMatch[1].trim();
8883
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8884
+ return {
8885
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8886
+ targetIsGroup: !!groupMatch
8887
+ };
8485
8888
  }
8486
8889
  const arrowMatch = trimmed.match(ARROW_RE);
8487
8890
  if (arrowMatch) {
@@ -8490,8 +8893,14 @@ function parseArrowLine(trimmed, palette) {
8490
8893
  if (label && !color) {
8491
8894
  color = inferArrowColor(label);
8492
8895
  }
8493
- const target = arrowMatch[3].trim();
8494
- return { label, color, target };
8896
+ const rawTarget = arrowMatch[3].trim();
8897
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8898
+ return {
8899
+ label,
8900
+ color,
8901
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8902
+ targetIsGroup: !!groupMatch
8903
+ };
8495
8904
  }
8496
8905
  return null;
8497
8906
  }
@@ -8553,6 +8962,7 @@ function parseSitemap(content, palette) {
8553
8962
  const aliasMap = /* @__PURE__ */ new Map();
8554
8963
  const indentStack = [];
8555
8964
  const labelToNode = /* @__PURE__ */ new Map();
8965
+ const labelToContainer = /* @__PURE__ */ new Map();
8556
8966
  const deferredArrows = [];
8557
8967
  for (let i = 0; i < lines.length; i++) {
8558
8968
  const line10 = lines[i];
@@ -8654,6 +9064,7 @@ function parseSitemap(content, palette) {
8654
9064
  deferredArrows.push({
8655
9065
  sourceNode: source,
8656
9066
  targetLabel: arrowInfo.target,
9067
+ targetIsGroup: arrowInfo.targetIsGroup,
8657
9068
  label: arrowInfo.label,
8658
9069
  color: arrowInfo.color,
8659
9070
  lineNumber
@@ -8687,6 +9098,7 @@ function parseSitemap(content, palette) {
8687
9098
  color
8688
9099
  };
8689
9100
  attachNode2(node, indent, indentStack, result);
9101
+ labelToContainer.set(label.toLowerCase(), node);
8690
9102
  } else if (metadataMatch && indentStack.length > 0) {
8691
9103
  const rawKey = metadataMatch[1].trim().toLowerCase();
8692
9104
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8727,22 +9139,41 @@ function parseSitemap(content, palette) {
8727
9139
  }
8728
9140
  for (const arrow of deferredArrows) {
8729
9141
  const targetKey = arrow.targetLabel.toLowerCase();
8730
- const targetNode = labelToNode.get(targetKey);
8731
- if (!targetNode) {
8732
- const allLabels = Array.from(labelToNode.keys());
8733
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8734
- const hint = suggest(targetKey, allLabels);
8735
- if (hint) msg += `. ${hint}`;
8736
- pushError(arrow.lineNumber, msg);
8737
- continue;
9142
+ if (arrow.targetIsGroup) {
9143
+ const targetContainer = labelToContainer.get(targetKey);
9144
+ if (!targetContainer) {
9145
+ const allLabels = Array.from(labelToContainer.keys());
9146
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9147
+ const hint = suggest(targetKey, allLabels);
9148
+ if (hint) msg += `. ${hint}`;
9149
+ pushError(arrow.lineNumber, msg);
9150
+ continue;
9151
+ }
9152
+ result.edges.push({
9153
+ sourceId: arrow.sourceNode.id,
9154
+ targetId: targetContainer.id,
9155
+ label: arrow.label,
9156
+ color: arrow.color,
9157
+ lineNumber: arrow.lineNumber
9158
+ });
9159
+ } else {
9160
+ const targetNode = labelToNode.get(targetKey);
9161
+ if (!targetNode) {
9162
+ const allLabels = Array.from(labelToNode.keys());
9163
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9164
+ const hint = suggest(targetKey, allLabels);
9165
+ if (hint) msg += `. ${hint}`;
9166
+ pushError(arrow.lineNumber, msg);
9167
+ continue;
9168
+ }
9169
+ result.edges.push({
9170
+ sourceId: arrow.sourceNode.id,
9171
+ targetId: targetNode.id,
9172
+ label: arrow.label,
9173
+ color: arrow.color,
9174
+ lineNumber: arrow.lineNumber
9175
+ });
8738
9176
  }
8739
- result.edges.push({
8740
- sourceId: arrow.sourceNode.id,
8741
- targetId: targetNode.id,
8742
- label: arrow.label,
8743
- color: arrow.color,
8744
- lineNumber: arrow.lineNumber
8745
- });
8746
9177
  }
8747
9178
  if (result.tagGroups.length > 0) {
8748
9179
  const allNodes = [];
@@ -10456,6 +10887,7 @@ function parseBoxesAndLines(content) {
10456
10887
  const nodeLabels = /* @__PURE__ */ new Set();
10457
10888
  const groupLabels = /* @__PURE__ */ new Set();
10458
10889
  let lastNodeLabel = null;
10890
+ let lastSourceIsGroup = false;
10459
10891
  const groupStack = [];
10460
10892
  let contentStarted = false;
10461
10893
  let currentTagGroup = null;
@@ -10694,6 +11126,8 @@ function parseBoxesAndLines(content) {
10694
11126
  };
10695
11127
  groupLabels.add(label);
10696
11128
  groupStack.push({ group, indent, depth: currentDepth });
11129
+ lastNodeLabel = label;
11130
+ lastSourceIsGroup = true;
10697
11131
  continue;
10698
11132
  }
10699
11133
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10711,7 +11145,8 @@ function parseBoxesAndLines(content) {
10711
11145
  );
10712
11146
  continue;
10713
11147
  }
10714
- edgeText = `${lastNodeLabel} ${trimmed}`;
11148
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11149
+ edgeText = `${sourcePrefix} ${trimmed}`;
10715
11150
  }
10716
11151
  const edge = parseEdgeLine(
10717
11152
  edgeText,
@@ -10734,6 +11169,7 @@ function parseBoxesAndLines(content) {
10734
11169
  continue;
10735
11170
  }
10736
11171
  lastNodeLabel = node.label;
11172
+ lastSourceIsGroup = false;
10737
11173
  const gs = currentGroupState();
10738
11174
  const isGroupChild = gs && indent > gs.indent;
10739
11175
  if (nodeLabels.has(node.label)) {
@@ -10761,14 +11197,42 @@ function parseBoxesAndLines(content) {
10761
11197
  const gs = groupStack.pop();
10762
11198
  result.groups.push(gs.group);
10763
11199
  }
11200
+ const validEdges = [];
10764
11201
  for (const edge of result.edges) {
10765
- if (!edge.source.startsWith("__group_")) {
11202
+ let valid = true;
11203
+ if (edge.source.startsWith("__group_")) {
11204
+ const label = edge.source.slice("__group_".length);
11205
+ const found = [...groupLabels].some(
11206
+ (g) => g.toLowerCase() === label.toLowerCase()
11207
+ );
11208
+ if (!found) {
11209
+ result.diagnostics.push(
11210
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11211
+ );
11212
+ valid = false;
11213
+ }
11214
+ } else {
10766
11215
  ensureNode(edge.source, edge.lineNumber);
10767
11216
  }
10768
- if (!edge.target.startsWith("__group_")) {
11217
+ if (edge.target.startsWith("__group_")) {
11218
+ const label = edge.target.slice("__group_".length);
11219
+ const found = [...groupLabels].some(
11220
+ (g) => g.toLowerCase() === label.toLowerCase()
11221
+ );
11222
+ if (!found) {
11223
+ result.diagnostics.push(
11224
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11225
+ );
11226
+ valid = false;
11227
+ }
11228
+ } else {
10769
11229
  ensureNode(edge.target, edge.lineNumber);
10770
11230
  }
11231
+ if (valid) {
11232
+ validEdges.push(edge);
11233
+ }
10771
11234
  }
11235
+ result.edges = validEdges;
10772
11236
  if (result.tagGroups.length > 0) {
10773
11237
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10774
11238
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10797,10 +11261,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10797
11261
  description
10798
11262
  };
10799
11263
  }
11264
+ function resolveEndpoint(name) {
11265
+ const m = name.match(/^\[(.+)\]$/);
11266
+ return m ? groupId2(m[1].trim()) : name;
11267
+ }
10800
11268
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10801
11269
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10802
11270
  if (biLabeledMatch) {
10803
- const source2 = biLabeledMatch[1].trim();
11271
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10804
11272
  const label = biLabeledMatch[2].trim();
10805
11273
  let rest2 = biLabeledMatch[3].trim();
10806
11274
  let metadata2 = {};
@@ -10821,7 +11289,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10821
11289
  }
10822
11290
  return {
10823
11291
  source: source2,
10824
- target: rest2,
11292
+ target: resolveEndpoint(rest2),
10825
11293
  label: label || void 0,
10826
11294
  bidirectional: true,
10827
11295
  lineNumber: lineNum,
@@ -10830,7 +11298,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10830
11298
  }
10831
11299
  const biIdx = trimmed.indexOf("<->");
10832
11300
  if (biIdx >= 0) {
10833
- const source2 = trimmed.slice(0, biIdx).trim();
11301
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10834
11302
  let rest2 = trimmed.slice(biIdx + 3).trim();
10835
11303
  let metadata2 = {};
10836
11304
  const pipeIdx2 = rest2.indexOf("|");
@@ -10850,7 +11318,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10850
11318
  }
10851
11319
  return {
10852
11320
  source: source2,
10853
- target: rest2,
11321
+ target: resolveEndpoint(rest2),
10854
11322
  bidirectional: true,
10855
11323
  lineNumber: lineNum,
10856
11324
  metadata: metadata2
@@ -10858,7 +11326,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10858
11326
  }
10859
11327
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10860
11328
  if (labeledMatch) {
10861
- const source2 = labeledMatch[1].trim();
11329
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10862
11330
  const label = labeledMatch[2].trim();
10863
11331
  let rest2 = labeledMatch[3].trim();
10864
11332
  if (label) {
@@ -10880,7 +11348,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10880
11348
  }
10881
11349
  return {
10882
11350
  source: source2,
10883
- target: rest2,
11351
+ target: resolveEndpoint(rest2),
10884
11352
  label,
10885
11353
  bidirectional: false,
10886
11354
  lineNumber: lineNum,
@@ -10890,7 +11358,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10890
11358
  }
10891
11359
  const arrowIdx = trimmed.indexOf("->");
10892
11360
  if (arrowIdx < 0) return null;
10893
- const source = trimmed.slice(0, arrowIdx).trim();
11361
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10894
11362
  let rest = trimmed.slice(arrowIdx + 2).trim();
10895
11363
  if (!source || !rest) {
10896
11364
  diagnostics.push(
@@ -10911,7 +11379,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10911
11379
  }
10912
11380
  return {
10913
11381
  source,
10914
- target: rest,
11382
+ target: resolveEndpoint(rest),
10915
11383
  bidirectional: false,
10916
11384
  lineNumber: lineNum,
10917
11385
  metadata
@@ -11225,14 +11693,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11225
11693
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11226
11694
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11227
11695
  if (visibleEntries.length === 0) continue;
11228
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11229
- const minPillWidth = pillWidth2;
11230
- let entriesWidth2 = 0;
11696
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11697
+ const minPillWidth = pillWidth3;
11698
+ let entriesWidth3 = 0;
11231
11699
  for (const entry of visibleEntries) {
11232
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11700
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11233
11701
  }
11234
11702
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11235
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11703
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11236
11704
  groups.push({
11237
11705
  name: group.name,
11238
11706
  alias: group.alias,
@@ -11242,7 +11710,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11242
11710
  })),
11243
11711
  x: 0,
11244
11712
  y: 0,
11245
- width: capsuleWidth,
11713
+ width: capsuleWidth2,
11246
11714
  height: LEGEND_HEIGHT2,
11247
11715
  minifiedWidth: minPillWidth,
11248
11716
  minifiedHeight: LEGEND_HEIGHT2
@@ -12183,66 +12651,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12183
12651
  }
12184
12652
  }
12185
12653
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12186
- const visibleGroups = layout.legend.filter((group) => {
12187
- if (legendOnly) return true;
12188
- if (activeTagGroup == null) return true;
12189
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12190
- });
12191
- let fixedPositions;
12192
- if (fixedLegend && visibleGroups.length > 0) {
12193
- fixedPositions = /* @__PURE__ */ new Map();
12194
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12195
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12196
- let cx = (width - totalW) / 2;
12197
- for (const g of visibleGroups) {
12198
- fixedPositions.set(g.name, cx);
12199
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12200
- }
12201
- }
12202
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12203
- const legendParent = legendParentBase;
12204
- if (fixedLegend && activeTagGroup) {
12205
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12654
+ const groups = layout.legend.map((g) => ({
12655
+ name: g.name,
12656
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12657
+ }));
12658
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12659
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12660
+ let legendHandle;
12661
+ if (legendOnly) {
12662
+ for (const lg of layout.legend) {
12663
+ const singleConfig = {
12664
+ groups: [
12665
+ {
12666
+ name: lg.name,
12667
+ entries: lg.entries.map((e) => ({
12668
+ value: e.value,
12669
+ color: e.color
12670
+ }))
12671
+ }
12672
+ ],
12673
+ position: { placement: "top-center", titleRelation: "below-title" },
12674
+ mode: "fixed"
12675
+ };
12676
+ const singleState = { activeGroup: lg.name };
12677
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12678
+ renderLegendD3(
12679
+ groupG,
12680
+ singleConfig,
12681
+ singleState,
12682
+ palette,
12683
+ isDark,
12684
+ void 0,
12685
+ lg.width
12686
+ );
12687
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12688
+ }
12689
+ legendHandle = null;
12690
+ } else {
12691
+ const legendConfig = {
12692
+ groups,
12693
+ position: { placement: "top-center", titleRelation: "below-title" },
12694
+ mode: "fixed",
12695
+ capsulePillAddonWidth: eyeAddonWidth
12696
+ };
12697
+ const legendState = { activeGroup: activeTagGroup ?? null };
12698
+ legendHandle = renderLegendD3(
12699
+ legendParentBase,
12700
+ legendConfig,
12701
+ legendState,
12702
+ palette,
12703
+ isDark,
12704
+ void 0,
12705
+ fixedLegend ? width : layout.width
12706
+ );
12707
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12206
12708
  }
12207
- for (const group of visibleGroups) {
12208
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12209
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12210
- const pillLabel = group.name;
12211
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12212
- const gX = fixedPositions?.get(group.name) ?? group.x;
12213
- const gY = fixedPositions ? 0 : group.y;
12214
- 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");
12215
- if (isActive) {
12216
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12217
- }
12218
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12219
- const pillYOff = LEGEND_CAPSULE_PAD;
12220
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12221
- 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);
12222
- if (isActive) {
12223
- 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);
12224
- }
12225
- 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);
12226
- if (isActive && fixedLegend) {
12227
- const groupKey = group.name.toLowerCase();
12709
+ if (fixedLegend && legendHandle) {
12710
+ const computedLayout = legendHandle.getLayout();
12711
+ if (computedLayout.activeCapsule?.addonX != null) {
12712
+ const capsule = computedLayout.activeCapsule;
12713
+ const groupKey = capsule.groupName.toLowerCase();
12228
12714
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12229
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12230
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12231
- const hitPad = 6;
12232
- 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);
12233
- 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");
12234
- 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");
12235
- }
12236
- if (isActive) {
12237
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12238
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12239
- for (const entry of group.entries) {
12240
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12241
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12242
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12243
- const entryLabel = entry.value;
12244
- 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);
12245
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12715
+ const activeGroupEl = legendParentBase.select(
12716
+ `[data-legend-group="${groupKey}"]`
12717
+ );
12718
+ if (!activeGroupEl.empty()) {
12719
+ const eyeX = capsule.addonX;
12720
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12721
+ const hitPad = 6;
12722
+ const eyeG = activeGroupEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12723
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12724
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12246
12725
  }
12247
12726
  }
12248
12727
  }
@@ -12277,6 +12756,7 @@ var init_renderer = __esm({
12277
12756
  init_parser4();
12278
12757
  init_layout();
12279
12758
  init_legend_constants();
12759
+ init_legend_d3();
12280
12760
  init_title_constants();
12281
12761
  DIAGRAM_PADDING = 20;
12282
12762
  MAX_SCALE = 3;
@@ -12305,6 +12785,17 @@ var layout_exports2 = {};
12305
12785
  __export(layout_exports2, {
12306
12786
  layoutSitemap: () => layoutSitemap
12307
12787
  });
12788
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12789
+ const dx = tx - cx;
12790
+ const dy = ty - cy;
12791
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12792
+ const hw = w / 2;
12793
+ const hh = h / 2;
12794
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12795
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12796
+ const s = Math.min(sx, sy);
12797
+ return { x: cx + dx * s, y: cy + dy * s };
12798
+ }
12308
12799
  function filterMetadata2(metadata, hiddenAttributes) {
12309
12800
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12310
12801
  const filtered = {};
@@ -12321,7 +12812,10 @@ function computeCardWidth2(label, meta) {
12321
12812
  const lineChars = key.length + 2 + value.length;
12322
12813
  if (lineChars > maxChars) maxChars = lineChars;
12323
12814
  }
12324
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12815
+ return Math.max(
12816
+ MIN_CARD_WIDTH2,
12817
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
12818
+ );
12325
12819
  }
12326
12820
  function computeCardHeight2(meta) {
12327
12821
  const metaCount = Object.keys(meta).length;
@@ -12330,7 +12824,12 @@ function computeCardHeight2(meta) {
12330
12824
  }
12331
12825
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12332
12826
  if (node.color) return node.color;
12333
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
12827
+ return resolveTagColor(
12828
+ node.metadata,
12829
+ tagGroups,
12830
+ activeGroupName,
12831
+ node.isContainer
12832
+ );
12334
12833
  }
12335
12834
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12336
12835
  const groups = [];
@@ -12339,21 +12838,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12339
12838
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12340
12839
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12341
12840
  if (visibleEntries.length === 0) continue;
12342
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12343
- const minPillWidth = pillWidth2;
12344
- let entriesWidth2 = 0;
12841
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12842
+ const minPillWidth = pillWidth3;
12843
+ let entriesWidth3 = 0;
12345
12844
  for (const entry of visibleEntries) {
12346
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12845
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12347
12846
  }
12348
12847
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12349
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
12848
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12350
12849
  groups.push({
12351
12850
  name: group.name,
12352
12851
  alias: group.alias,
12353
12852
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12354
12853
  x: 0,
12355
12854
  y: 0,
12356
- width: capsuleWidth,
12855
+ width: capsuleWidth2,
12357
12856
  height: LEGEND_HEIGHT3,
12358
12857
  minifiedWidth: minPillWidth,
12359
12858
  minifiedHeight: LEGEND_HEIGHT3
@@ -12373,10 +12872,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12373
12872
  parentPageId,
12374
12873
  meta,
12375
12874
  fullMeta: { ...node.metadata },
12376
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
12875
+ width: Math.max(
12876
+ MIN_CARD_WIDTH2,
12877
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
12878
+ ),
12377
12879
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12378
12880
  });
12379
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
12881
+ flattenNodes(
12882
+ node.children,
12883
+ node.id,
12884
+ parentPageId,
12885
+ hiddenCounts,
12886
+ hiddenAttributes,
12887
+ result
12888
+ );
12380
12889
  } else {
12381
12890
  result.push({
12382
12891
  sitemapNode: node,
@@ -12388,14 +12897,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12388
12897
  height: computeCardHeight2(meta)
12389
12898
  });
12390
12899
  if (node.children.length > 0) {
12391
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
12900
+ flattenNodes(
12901
+ node.children,
12902
+ parentContainerId,
12903
+ node.id,
12904
+ hiddenCounts,
12905
+ hiddenAttributes,
12906
+ result
12907
+ );
12392
12908
  }
12393
12909
  }
12394
12910
  }
12395
12911
  }
12396
12912
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12397
12913
  if (parsed.roots.length === 0) {
12398
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
12914
+ return {
12915
+ nodes: [],
12916
+ edges: [],
12917
+ containers: [],
12918
+ legend: [],
12919
+ width: 0,
12920
+ height: 0
12921
+ };
12399
12922
  }
12400
12923
  const allNodes = [];
12401
12924
  const collect = (node) => {
@@ -12403,9 +12926,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12403
12926
  for (const child of node.children) collect(child);
12404
12927
  };
12405
12928
  for (const root of parsed.roots) collect(root);
12406
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
12929
+ injectDefaultTagMetadata(
12930
+ allNodes,
12931
+ parsed.tagGroups,
12932
+ (e) => e.isContainer
12933
+ );
12407
12934
  const flatNodes = [];
12408
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
12935
+ flattenNodes(
12936
+ parsed.roots,
12937
+ null,
12938
+ null,
12939
+ hiddenCounts,
12940
+ hiddenAttributes,
12941
+ flatNodes
12942
+ );
12409
12943
  const nodeMap = /* @__PURE__ */ new Map();
12410
12944
  for (const flat of flatNodes) {
12411
12945
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12467,14 +13001,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12467
13001
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12468
13002
  }
12469
13003
  }
13004
+ const expandedContainerIds = /* @__PURE__ */ new Set();
13005
+ for (const cid of containerIds) {
13006
+ if (!collapsedContainerIds.has(cid)) {
13007
+ expandedContainerIds.add(cid);
13008
+ }
13009
+ }
13010
+ const deferredEdgeIndices = [];
12470
13011
  for (let i = 0; i < parsed.edges.length; i++) {
12471
13012
  const edge = parsed.edges[i];
12472
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12473
- g.setEdge(edge.sourceId, edge.targetId, {
13013
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
13014
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
13015
+ deferredEdgeIndices.push(i);
13016
+ continue;
13017
+ }
13018
+ g.setEdge(
13019
+ edge.sourceId,
13020
+ edge.targetId,
13021
+ {
12474
13022
  label: edge.label ?? "",
12475
13023
  minlen: 1
12476
- }, `e${i}`);
12477
- }
13024
+ },
13025
+ `e${i}`
13026
+ );
12478
13027
  }
12479
13028
  import_dagre.default.layout(g);
12480
13029
  const layoutNodes = [];
@@ -12542,19 +13091,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12542
13091
  });
12543
13092
  }
12544
13093
  }
13094
+ const deferredSet = new Set(deferredEdgeIndices);
12545
13095
  const layoutEdges = [];
12546
13096
  for (let i = 0; i < parsed.edges.length; i++) {
12547
13097
  const edge = parsed.edges[i];
12548
13098
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12549
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12550
- if (!edgeData) continue;
13099
+ let points;
13100
+ if (deferredSet.has(i)) {
13101
+ const srcNode = g.node(edge.sourceId);
13102
+ const tgtNode = g.node(edge.targetId);
13103
+ if (!srcNode || !tgtNode) continue;
13104
+ const srcPt = clipToRectBorder(
13105
+ srcNode.x,
13106
+ srcNode.y,
13107
+ srcNode.width,
13108
+ srcNode.height,
13109
+ tgtNode.x,
13110
+ tgtNode.y
13111
+ );
13112
+ const tgtPt = clipToRectBorder(
13113
+ tgtNode.x,
13114
+ tgtNode.y,
13115
+ tgtNode.width,
13116
+ tgtNode.height,
13117
+ srcNode.x,
13118
+ srcNode.y
13119
+ );
13120
+ const midX = (srcPt.x + tgtPt.x) / 2;
13121
+ const midY = (srcPt.y + tgtPt.y) / 2;
13122
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13123
+ } else {
13124
+ const edgeData = g.edge({
13125
+ v: edge.sourceId,
13126
+ w: edge.targetId,
13127
+ name: `e${i}`
13128
+ });
13129
+ if (!edgeData) continue;
13130
+ points = edgeData.points ?? [];
13131
+ }
12551
13132
  layoutEdges.push({
12552
13133
  sourceId: edge.sourceId,
12553
13134
  targetId: edge.targetId,
12554
- points: edgeData.points ?? [],
13135
+ points,
12555
13136
  label: edge.label,
12556
13137
  color: edge.color,
12557
- lineNumber: edge.lineNumber
13138
+ lineNumber: edge.lineNumber,
13139
+ deferred: deferredSet.has(i) || void 0
12558
13140
  });
12559
13141
  }
12560
13142
  {
@@ -12715,7 +13297,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12715
13297
  usedValuesByGroup.set(key, used);
12716
13298
  }
12717
13299
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12718
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13300
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13301
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13302
+ ) : legendGroups;
12719
13303
  const allExpanded = expandAllLegend && activeTagGroup == null;
12720
13304
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12721
13305
  if (visibleGroups.length > 0) {
@@ -13030,7 +13614,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13030
13614
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
13031
13615
  const edgeColor3 = edge.color ?? palette.textMuted;
13032
13616
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
13033
- const pathD = lineGenerator(edge.points);
13617
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13618
+ const pathD = gen(edge.points);
13034
13619
  if (pathD) {
13035
13620
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor3).attr("stroke-width", EDGE_STROKE_WIDTH2).attr("marker-end", `url(#${markerId})`).attr("class", "sitemap-edge");
13036
13621
  }
@@ -13121,57 +13706,44 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13121
13706
  }
13122
13707
  function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13123
13708
  if (legendGroups.length === 0) return;
13124
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13125
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13126
- ) : legendGroups;
13127
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13128
- let fixedPositions;
13129
- if (fixedWidth != null && visibleGroups.length > 0) {
13130
- fixedPositions = /* @__PURE__ */ new Map();
13131
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13132
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13133
- let cx = (fixedWidth - totalW) / 2;
13134
- for (const g of visibleGroups) {
13135
- fixedPositions.set(g.name, cx);
13136
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13137
- }
13138
- }
13139
- for (const group of visibleGroups) {
13140
- const isActive = activeTagGroup != null;
13141
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13142
- const gX = fixedPositions?.get(group.name) ?? group.x;
13143
- const gY = fixedPositions ? 0 : group.y;
13144
- 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");
13145
- if (isActive) {
13146
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13147
- }
13148
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13149
- const pillYOff = LEGEND_CAPSULE_PAD;
13150
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13151
- 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);
13152
- if (isActive) {
13153
- 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);
13154
- }
13155
- 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);
13156
- if (isActive && fixedWidth != null) {
13157
- const groupKey = group.name.toLowerCase();
13709
+ const groups = legendGroups.map((g) => ({
13710
+ name: g.name,
13711
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13712
+ }));
13713
+ const isFixedMode = fixedWidth != null;
13714
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13715
+ const legendConfig = {
13716
+ groups,
13717
+ position: { placement: "top-center", titleRelation: "below-title" },
13718
+ mode: "fixed",
13719
+ capsulePillAddonWidth: eyeAddonWidth
13720
+ };
13721
+ const legendState = { activeGroup: activeTagGroup ?? null };
13722
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13723
+ const legendHandle = renderLegendD3(
13724
+ parent,
13725
+ legendConfig,
13726
+ legendState,
13727
+ palette,
13728
+ isDark,
13729
+ void 0,
13730
+ containerWidth
13731
+ );
13732
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13733
+ if (isFixedMode) {
13734
+ const computedLayout = legendHandle.getLayout();
13735
+ if (computedLayout.activeCapsule?.addonX != null) {
13736
+ const capsule = computedLayout.activeCapsule;
13737
+ const groupKey = capsule.groupName.toLowerCase();
13158
13738
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13159
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13160
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13161
- const hitPad = 6;
13162
- 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);
13163
- 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");
13164
- 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");
13165
- }
13166
- if (isActive) {
13167
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13168
- let entryX = pillXOff + pillW + 4 + eyeShift;
13169
- for (const entry of group.entries) {
13170
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13171
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13172
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13173
- 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);
13174
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13739
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13740
+ if (!activeGroupEl.empty()) {
13741
+ const eyeX = capsule.addonX;
13742
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13743
+ const hitPad = 6;
13744
+ const eyeG = activeGroupEl.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13745
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13746
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13175
13747
  }
13176
13748
  }
13177
13749
  }
@@ -13225,7 +13797,7 @@ async function renderSitemapForExport(content, theme, palette) {
13225
13797
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13226
13798
  return injectBranding2(svgHtml, brandColor);
13227
13799
  }
13228
- var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
13800
+ var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator, lineGeneratorLinear;
13229
13801
  var init_renderer2 = __esm({
13230
13802
  "src/sitemap/renderer.ts"() {
13231
13803
  "use strict";
@@ -13234,6 +13806,7 @@ var init_renderer2 = __esm({
13234
13806
  init_fonts();
13235
13807
  init_color_utils();
13236
13808
  init_legend_constants();
13809
+ init_legend_d3();
13237
13810
  init_title_constants();
13238
13811
  DIAGRAM_PADDING2 = 20;
13239
13812
  MAX_SCALE2 = 3;
@@ -13257,6 +13830,7 @@ var init_renderer2 = __esm({
13257
13830
  COLLAPSE_BAR_HEIGHT2 = 6;
13258
13831
  LEGEND_FIXED_GAP2 = 8;
13259
13832
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
13833
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13260
13834
  }
13261
13835
  });
13262
13836
 
@@ -13533,53 +14107,22 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13533
14107
  }
13534
14108
  if (parsed.tagGroups.length > 0) {
13535
14109
  const legendY = height - LEGEND_HEIGHT;
13536
- let legendX = DIAGRAM_PADDING3;
13537
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13538
- const capsulePad = LEGEND_CAPSULE_PAD;
13539
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13540
- if (activeTagGroup) {
13541
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13542
- }
13543
- for (const group of parsed.tagGroups) {
13544
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13545
- if (activeTagGroup != null && !isActive) continue;
13546
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13547
- const pillWidth2 = pillTextWidth + 16;
13548
- let capsuleContentWidth = pillWidth2;
13549
- if (isActive) {
13550
- capsuleContentWidth += 4;
13551
- for (const entry of group.entries) {
13552
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13553
- }
13554
- }
13555
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13556
- if (isActive) {
13557
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13558
- }
13559
- const pillX = legendX + (isActive ? capsulePad : 0);
13560
- const pillBg = isActive ? palette.bg : groupBg;
13561
- 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());
13562
- if (isActive) {
13563
- 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);
13564
- }
13565
- 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);
13566
- if (isActive) {
13567
- let entryX = pillX + pillWidth2 + 4;
13568
- for (const entry of group.entries) {
13569
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13570
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13571
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13572
- entryG.append("text").attr("x", entryTextX).attr(
13573
- "y",
13574
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13575
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13576
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13577
- }
13578
- legendX += capsuleWidth + 12;
13579
- } else {
13580
- legendX += pillWidth2 + 12;
13581
- }
13582
- }
14110
+ const legendConfig = {
14111
+ groups: parsed.tagGroups,
14112
+ position: { placement: "top-center", titleRelation: "below-title" },
14113
+ mode: exportDims ? "inline" : "fixed"
14114
+ };
14115
+ const legendState = { activeGroup: activeTagGroup ?? null };
14116
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${DIAGRAM_PADDING3},${legendY})`);
14117
+ renderLegendD3(
14118
+ legendG,
14119
+ legendConfig,
14120
+ legendState,
14121
+ palette,
14122
+ isDark,
14123
+ void 0,
14124
+ width - DIAGRAM_PADDING3 * 2
14125
+ );
13583
14126
  }
13584
14127
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13585
14128
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13675,6 +14218,7 @@ var init_renderer3 = __esm({
13675
14218
  init_parser5();
13676
14219
  init_mutations();
13677
14220
  init_legend_constants();
14221
+ init_legend_d3();
13678
14222
  init_title_constants();
13679
14223
  DIAGRAM_PADDING3 = 20;
13680
14224
  COLUMN_GAP = 16;
@@ -13868,14 +14412,9 @@ function collectClassTypes(parsed) {
13868
14412
  if (c.color) continue;
13869
14413
  present.add(c.modifier ?? "class");
13870
14414
  }
13871
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13872
- }
13873
- function legendEntriesWidth(entries) {
13874
- let w = 0;
13875
- for (const e of entries) {
13876
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13877
- }
13878
- return w;
14415
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14416
+ (k) => CLASS_TYPE_MAP[k]
14417
+ );
13879
14418
  }
13880
14419
  function classTypeKey(modifier) {
13881
14420
  return modifier ?? "class";
@@ -13944,7 +14483,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13944
14483
  defs.append("marker").attr("id", "cd-arrow-depend").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13945
14484
  defs.append("marker").attr("id", "cd-arrow-assoc").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13946
14485
  if (parsed.title) {
13947
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
14486
+ const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style(
14487
+ "cursor",
14488
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14489
+ ).text(parsed.title);
13948
14490
  if (parsed.titleLineNumber) {
13949
14491
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13950
14492
  if (onClickItem) {
@@ -13958,32 +14500,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13958
14500
  }
13959
14501
  const isLegendExpanded = legendActive !== false;
13960
14502
  if (hasLegend) {
13961
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13962
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13963
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13964
- const entriesW = legendEntriesWidth(legendEntries);
13965
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13966
- const legendX = (width - totalW) / 2;
13967
- const legendY = titleHeight;
13968
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13969
- if (isLegendExpanded) {
13970
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13971
- 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);
13972
- 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);
13973
- 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);
13974
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13975
- for (const entry of legendEntries) {
13976
- const color = palette.colors[entry.colorKey];
13977
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13978
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13979
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13980
- 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);
13981
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14503
+ const legendGroups = [
14504
+ {
14505
+ name: LEGEND_GROUP_NAME,
14506
+ entries: legendEntries.map((entry) => ({
14507
+ value: entry.label,
14508
+ color: palette.colors[entry.colorKey]
14509
+ }))
13982
14510
  }
13983
- } else {
13984
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13985
- 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);
13986
- }
14511
+ ];
14512
+ const legendConfig = {
14513
+ groups: legendGroups,
14514
+ position: { placement: "top-center", titleRelation: "below-title" },
14515
+ mode: "fixed"
14516
+ };
14517
+ const legendState = {
14518
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14519
+ };
14520
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14521
+ renderLegendD3(
14522
+ legendG,
14523
+ legendConfig,
14524
+ legendState,
14525
+ palette,
14526
+ isDark,
14527
+ void 0,
14528
+ width
14529
+ );
13987
14530
  }
13988
14531
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13989
14532
  for (const edge of layout.edges) {
@@ -14027,7 +14570,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
14027
14570
  const colorOff = !!parsed.options?.["no-auto-color"];
14028
14571
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
14029
14572
  const effectiveColor = neutralize ? palette.primary : node.color;
14030
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14573
+ const fill2 = nodeFill3(
14574
+ palette,
14575
+ isDark,
14576
+ node.modifier,
14577
+ effectiveColor,
14578
+ colorOff
14579
+ );
14031
14580
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
14032
14581
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", 3).attr("ry", 3).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", NODE_STROKE_WIDTH3);
14033
14582
  let yPos = -h / 2;
@@ -14096,15 +14645,10 @@ function renderClassDiagramForExport(content, theme, palette) {
14096
14645
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
14097
14646
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
14098
14647
  return runInExportContainer(exportWidth, exportHeight, (container) => {
14099
- renderClassDiagram(
14100
- container,
14101
- parsed,
14102
- layout,
14103
- palette,
14104
- isDark,
14105
- void 0,
14106
- { width: exportWidth, height: exportHeight }
14107
- );
14648
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14649
+ width: exportWidth,
14650
+ height: exportHeight
14651
+ });
14108
14652
  return extractExportSvg(container, theme);
14109
14653
  });
14110
14654
  }
@@ -14117,6 +14661,7 @@ var init_renderer4 = __esm({
14117
14661
  init_fonts();
14118
14662
  init_export_container();
14119
14663
  init_legend_constants();
14664
+ init_legend_d3();
14120
14665
  init_title_constants();
14121
14666
  init_color_utils();
14122
14667
  init_parser2();
@@ -14723,35 +15268,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14723
15268
  }
14724
15269
  }
14725
15270
  if (parsed.tagGroups.length > 0) {
14726
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14727
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14728
- const LEGEND_GAP = 8;
14729
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14730
- if (activeTagGroup) {
14731
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14732
- }
14733
- let legendX = DIAGRAM_PADDING5;
14734
15271
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14735
- for (const group of parsed.tagGroups) {
14736
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14737
- 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}:`);
14738
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14739
- legendX += labelWidth;
14740
- for (const entry of group.entries) {
14741
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14742
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14743
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14744
- tmpText.remove();
14745
- const pillW = textW + LEGEND_PILL_PAD * 2;
14746
- 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(
14747
- "fill",
14748
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14749
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14750
- 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);
14751
- legendX += pillW + LEGEND_GAP;
14752
- }
14753
- legendX += LEGEND_GROUP_GAP;
14754
- }
15272
+ const legendConfig = {
15273
+ groups: parsed.tagGroups,
15274
+ position: { placement: "top-center", titleRelation: "below-title" },
15275
+ mode: "fixed"
15276
+ };
15277
+ const legendState = { activeGroup: activeTagGroup ?? null };
15278
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15279
+ renderLegendD3(
15280
+ legendG,
15281
+ legendConfig,
15282
+ legendState,
15283
+ palette,
15284
+ isDark,
15285
+ void 0,
15286
+ viewW
15287
+ );
15288
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14755
15289
  }
14756
15290
  if (semanticRoles) {
14757
15291
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14761,55 +15295,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14761
15295
  return false;
14762
15296
  });
14763
15297
  if (presentRoles.length > 0) {
14764
- const measureLabelW = (text, fontSize) => {
14765
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14766
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14767
- dummy.remove();
14768
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14769
- };
14770
- const labelWidths = /* @__PURE__ */ new Map();
14771
- for (const role of presentRoles) {
14772
- labelWidths.set(
14773
- role,
14774
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14775
- );
14776
- }
14777
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14778
- const groupName = "Role";
14779
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14780
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14781
- let totalWidth;
14782
- let entriesWidth2 = 0;
14783
- if (semanticActive) {
14784
- for (const role of presentRoles) {
14785
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14786
- }
14787
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14788
- } else {
14789
- totalWidth = pillWidth2;
14790
- }
14791
- const legendX = (viewW - totalWidth) / 2;
14792
15298
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14793
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14794
- if (semanticActive) {
14795
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14796
- 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);
14797
- 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);
14798
- 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);
14799
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14800
- for (const role of presentRoles) {
14801
- const label = ROLE_LABELS[role];
14802
- const roleColor = palette.colors[ROLE_COLORS[role]];
14803
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14804
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14805
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14806
- 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);
14807
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15299
+ const semanticGroups = [
15300
+ {
15301
+ name: "Role",
15302
+ entries: presentRoles.map((role) => ({
15303
+ value: ROLE_LABELS[role],
15304
+ color: palette.colors[ROLE_COLORS[role]]
15305
+ }))
14808
15306
  }
14809
- } else {
14810
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14811
- 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);
14812
- }
15307
+ ];
15308
+ const legendConfig = {
15309
+ groups: semanticGroups,
15310
+ position: { placement: "top-center", titleRelation: "below-title" },
15311
+ mode: "fixed"
15312
+ };
15313
+ const legendState = {
15314
+ activeGroup: semanticActive ? "Role" : null
15315
+ };
15316
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15317
+ renderLegendD3(
15318
+ legendG,
15319
+ legendConfig,
15320
+ legendState,
15321
+ palette,
15322
+ isDark,
15323
+ void 0,
15324
+ viewW
15325
+ );
15326
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14813
15327
  }
14814
15328
  }
14815
15329
  }
@@ -14854,6 +15368,7 @@ var init_renderer5 = __esm({
14854
15368
  init_palettes();
14855
15369
  init_tag_groups();
14856
15370
  init_legend_constants();
15371
+ init_legend_d3();
14857
15372
  init_title_constants();
14858
15373
  init_parser3();
14859
15374
  init_layout4();
@@ -14877,6 +15392,17 @@ var layout_exports5 = {};
14877
15392
  __export(layout_exports5, {
14878
15393
  layoutBoxesAndLines: () => layoutBoxesAndLines
14879
15394
  });
15395
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15396
+ const dx = tx - cx;
15397
+ const dy = ty - cy;
15398
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15399
+ const hw = w / 2;
15400
+ const hh = h / 2;
15401
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15402
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15403
+ const s = Math.min(sx, sy);
15404
+ return { x: cx + dx * s, y: cy + dy * s };
15405
+ }
14880
15406
  function computeNodeSize(_node) {
14881
15407
  const PHI = 1.618;
14882
15408
  const NODE_HEIGHT = 60;
@@ -15029,13 +15555,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
15029
15555
  const srcNode = g.node(edge.source);
15030
15556
  const tgtNode = g.node(edge.target);
15031
15557
  if (!srcNode || !tgtNode) continue;
15032
- const midX = (srcNode.x + tgtNode.x) / 2;
15033
- const midY = (srcNode.y + tgtNode.y) / 2;
15034
- points = [
15035
- { x: srcNode.x, y: srcNode.y },
15036
- { x: midX, y: midY },
15037
- { x: tgtNode.x, y: tgtNode.y }
15038
- ];
15558
+ const srcPt = clipToRectBorder2(
15559
+ srcNode.x,
15560
+ srcNode.y,
15561
+ srcNode.width,
15562
+ srcNode.height,
15563
+ tgtNode.x,
15564
+ tgtNode.y
15565
+ );
15566
+ const tgtPt = clipToRectBorder2(
15567
+ tgtNode.x,
15568
+ tgtNode.y,
15569
+ tgtNode.width,
15570
+ tgtNode.height,
15571
+ srcNode.x,
15572
+ srcNode.y
15573
+ );
15574
+ const midX = (srcPt.x + tgtPt.x) / 2;
15575
+ const midY = (srcPt.y + tgtPt.y) / 2;
15576
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
15039
15577
  } else {
15040
15578
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
15041
15579
  points = dagreEdge?.points ?? [];
@@ -15058,7 +15596,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
15058
15596
  labelY,
15059
15597
  yOffset: edgeYOffsets[i],
15060
15598
  parallelCount: edgeParallelCounts[i],
15061
- metadata: edge.metadata
15599
+ metadata: edge.metadata,
15600
+ deferred: deferredSet.has(i) || void 0
15062
15601
  });
15063
15602
  }
15064
15603
  let maxX = 0;
@@ -15327,12 +15866,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15327
15866
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15328
15867
  edgeGroups.set(i, edgeG);
15329
15868
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15330
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15331
- "d",
15332
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15333
- points
15334
- ) ?? ""
15335
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15869
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
15870
+ const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15336
15871
  if (le.bidirectional) {
15337
15872
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15338
15873
  path.attr("marker-start", `url(#${revId})`);
@@ -15413,50 +15948,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15413
15948
  }
15414
15949
  }
15415
15950
  if (parsed.tagGroups.length > 0) {
15416
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15417
- }
15418
- }
15419
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15420
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15421
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15422
- let totalW = 0;
15423
- for (const tg of parsed.tagGroups) {
15424
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15425
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15426
- if (isActive) {
15427
- totalW += 6;
15428
- for (const entry of tg.entries) {
15429
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15430
- }
15431
- }
15432
- totalW += LEGEND_GROUP_GAP;
15433
- }
15434
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15435
- const legendY = titleOffset + 4;
15436
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15437
- let x = 0;
15438
- for (const tg of parsed.tagGroups) {
15439
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15440
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15441
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15442
- 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);
15443
- if (isActiveGroup) {
15444
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15445
- }
15446
- 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);
15447
- x += nameW;
15448
- if (isActiveGroup) {
15449
- x += 6;
15450
- for (const entry of tg.entries) {
15451
- const entryColor = entry.color || palette.textMuted;
15452
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15453
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15454
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15455
- 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);
15456
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15457
- }
15458
- }
15459
- x += LEGEND_GROUP_GAP;
15951
+ const legendConfig = {
15952
+ groups: parsed.tagGroups,
15953
+ position: { placement: "top-center", titleRelation: "below-title" },
15954
+ mode: "fixed"
15955
+ };
15956
+ const legendState = { activeGroup };
15957
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
15958
+ renderLegendD3(
15959
+ legendG,
15960
+ legendConfig,
15961
+ legendState,
15962
+ palette,
15963
+ isDark,
15964
+ void 0,
15965
+ width
15966
+ );
15967
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15460
15968
  }
15461
15969
  }
15462
15970
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15464,7 +15972,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15464
15972
  exportDims: options?.exportDims
15465
15973
  });
15466
15974
  }
15467
- var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
15975
+ var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB, lineGeneratorLinear2;
15468
15976
  var init_renderer6 = __esm({
15469
15977
  "src/boxes-and-lines/renderer.ts"() {
15470
15978
  "use strict";
@@ -15472,6 +15980,7 @@ var init_renderer6 = __esm({
15472
15980
  d3Shape4 = __toESM(require("d3-shape"), 1);
15473
15981
  init_fonts();
15474
15982
  init_legend_constants();
15983
+ init_legend_d3();
15475
15984
  init_title_constants();
15476
15985
  init_color_utils();
15477
15986
  init_tag_groups();
@@ -15492,6 +16001,7 @@ var init_renderer6 = __esm({
15492
16001
  GROUP_LABEL_FONT_SIZE = 14;
15493
16002
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15494
16003
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
16004
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15495
16005
  }
15496
16006
  });
15497
16007
 
@@ -17398,7 +17908,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17398
17908
  if (activeTagGroup) {
17399
17909
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17400
17910
  }
17401
- renderLegend3(
17911
+ renderLegend2(
17402
17912
  legendParent,
17403
17913
  layout,
17404
17914
  palette,
@@ -17759,52 +18269,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17759
18269
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17760
18270
  }
17761
18271
  }
17762
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17763
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17764
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17765
- ) : layout.legend;
17766
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17767
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17768
- let fixedPositions = null;
17769
- if (fixedWidth != null && visibleGroups.length > 0) {
17770
- fixedPositions = /* @__PURE__ */ new Map();
17771
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17772
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17773
- for (const g of visibleGroups) {
17774
- fixedPositions.set(g.name, cx);
17775
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17776
- }
17777
- }
17778
- for (const group of visibleGroups) {
17779
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17780
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17781
- const pillLabel = group.name;
17782
- const pillWidth2 = pillWidthOf(group);
17783
- const gX = fixedPositions?.get(group.name) ?? group.x;
17784
- const gY = fixedPositions != null ? 0 : group.y;
17785
- 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");
17786
- if (isActive) {
17787
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17788
- }
17789
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17790
- const pillY = LEGEND_CAPSULE_PAD;
17791
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17792
- 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);
17793
- if (isActive) {
17794
- 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);
17795
- }
17796
- 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);
17797
- if (isActive) {
17798
- let entryX = pillX + pillWidth2 + 4;
17799
- for (const entry of group.entries) {
17800
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17801
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17802
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17803
- 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);
17804
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17805
- }
17806
- }
17807
- }
18272
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18273
+ const groups = layout.legend.map((g) => ({
18274
+ name: g.name,
18275
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18276
+ }));
18277
+ const legendConfig = {
18278
+ groups,
18279
+ position: { placement: "top-center", titleRelation: "below-title" },
18280
+ mode: "fixed"
18281
+ };
18282
+ const legendState = { activeGroup: activeTagGroup ?? null };
18283
+ const containerWidth = fixedWidth ?? layout.width;
18284
+ renderLegendD3(
18285
+ parent,
18286
+ legendConfig,
18287
+ legendState,
18288
+ palette,
18289
+ isDark,
18290
+ void 0,
18291
+ containerWidth
18292
+ );
18293
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17808
18294
  }
17809
18295
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17810
18296
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -18015,7 +18501,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
18015
18501
  if (activeTagGroup) {
18016
18502
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
18017
18503
  }
18018
- renderLegend3(
18504
+ renderLegend2(
18019
18505
  legendParent,
18020
18506
  layout,
18021
18507
  palette,
@@ -18145,6 +18631,7 @@ var init_renderer7 = __esm({
18145
18631
  init_parser6();
18146
18632
  init_layout6();
18147
18633
  init_legend_constants();
18634
+ init_legend_d3();
18148
18635
  init_title_constants();
18149
18636
  DIAGRAM_PADDING7 = 20;
18150
18637
  MAX_SCALE5 = 3;
@@ -21032,17 +21519,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
21032
21519
  color: r.color,
21033
21520
  key: r.name.toLowerCase().replace(/\s+/g, "-")
21034
21521
  }));
21035
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21036
- let entriesWidth2 = 0;
21522
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21523
+ let entriesWidth3 = 0;
21037
21524
  for (const e of entries) {
21038
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21525
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21039
21526
  }
21040
21527
  groups.push({
21041
21528
  name: "Capabilities",
21042
21529
  type: "role",
21043
21530
  entries,
21044
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
21045
- minifiedWidth: pillWidth2
21531
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21532
+ minifiedWidth: pillWidth3
21046
21533
  });
21047
21534
  }
21048
21535
  for (const tg of tagGroups) {
@@ -21057,113 +21544,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
21057
21544
  }
21058
21545
  }
21059
21546
  if (entries.length === 0) continue;
21060
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21061
- let entriesWidth2 = 0;
21547
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21548
+ let entriesWidth3 = 0;
21062
21549
  for (const e of entries) {
21063
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21550
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21064
21551
  }
21065
21552
  groups.push({
21066
21553
  name: tg.name,
21067
21554
  type: "tag",
21068
21555
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
21069
21556
  entries,
21070
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
21071
- minifiedWidth: pillWidth2
21557
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21558
+ minifiedWidth: pillWidth3
21072
21559
  });
21073
21560
  }
21074
21561
  return groups;
21075
21562
  }
21076
- function computePlaybackWidth(playback) {
21077
- if (!playback) return 0;
21078
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21079
- if (!playback.expanded) return pillWidth2;
21080
- let entriesW = 8;
21081
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21082
- for (const s of playback.speedOptions) {
21083
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
21084
- }
21085
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
21086
- }
21087
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21563
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21088
21564
  if (legendGroups.length === 0 && !playback) return;
21089
21565
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
21090
21566
  if (activeGroup) {
21091
21567
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
21092
21568
  }
21093
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
21094
- const playbackW = computePlaybackWidth(playback);
21095
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
21096
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
21097
- let cursorX = (totalWidth - totalLegendW) / 2;
21569
+ const allGroups = legendGroups.map((g) => ({
21570
+ name: g.name,
21571
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21572
+ }));
21573
+ if (playback) {
21574
+ allGroups.push({ name: "Playback", entries: [] });
21575
+ }
21576
+ const legendConfig = {
21577
+ groups: allGroups,
21578
+ position: { placement: "top-center", titleRelation: "below-title" },
21579
+ mode: "fixed",
21580
+ showEmptyGroups: true
21581
+ };
21582
+ const legendState = { activeGroup };
21583
+ renderLegendD3(
21584
+ legendG,
21585
+ legendConfig,
21586
+ legendState,
21587
+ palette,
21588
+ isDark,
21589
+ void 0,
21590
+ totalWidth
21591
+ );
21592
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
21098
21593
  for (const group of legendGroups) {
21099
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
21100
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
21101
- const pillLabel = group.name;
21102
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21103
- 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");
21104
- if (isActive) {
21105
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21106
- }
21107
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
21108
- const pillYOff = LEGEND_CAPSULE_PAD;
21109
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
21110
- 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);
21111
- if (isActive) {
21112
- 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);
21113
- }
21114
- 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);
21115
- if (isActive) {
21116
- let entryX = pillXOff + pillWidth2 + 4;
21117
- for (const entry of group.entries) {
21118
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21594
+ const groupKey = group.name.toLowerCase();
21595
+ for (const entry of group.entries) {
21596
+ const entryEl = legendG.select(
21597
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21598
+ );
21599
+ if (!entryEl.empty()) {
21600
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21119
21601
  "data-legend-tag-group",
21120
21602
  group.type === "tag" ? group.tagKey ?? "" : null
21121
- ).style("cursor", "pointer");
21122
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21123
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21124
- 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);
21125
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21603
+ );
21126
21604
  }
21127
21605
  }
21128
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21129
21606
  }
21130
- if (playback) {
21131
- const isExpanded = playback.expanded;
21132
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21133
- const pillLabel = "Playback";
21134
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21135
- const fullW = computePlaybackWidth(playback);
21136
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21137
- if (isExpanded) {
21138
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21139
- }
21140
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21141
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21142
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21143
- 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);
21144
- if (isExpanded) {
21145
- 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);
21146
- }
21147
- 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);
21148
- if (isExpanded) {
21149
- let entryX = pillXOff + pillWidth2 + 8;
21150
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21151
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21152
- 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);
21153
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21154
- for (const s of playback.speedOptions) {
21155
- const label = `${s}x`;
21156
- const isActive = playback.speed === s;
21157
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21158
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21159
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21160
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21161
- 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");
21162
- 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);
21163
- entryX += slotW + SPEED_BADGE_GAP;
21164
- }
21165
- }
21166
- cursorX += fullW + LEGEND_GROUP_GAP;
21607
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21608
+ if (!playbackEl.empty()) {
21609
+ playbackEl.classed("infra-playback-pill", true);
21610
+ }
21611
+ if (playback && playback.expanded && !playbackEl.empty()) {
21612
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21613
+ let entryX = pillWidth3 + 8;
21614
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21615
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21616
+ playbackEl.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21617
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21618
+ for (const s of playback.speedOptions) {
21619
+ const label = `${s}x`;
21620
+ const isSpeedActive = playback.speed === s;
21621
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21622
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21623
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21624
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21625
+ speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isSpeedActive ? palette.primary : "transparent");
21626
+ speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isSpeedActive ? "600" : "400").attr("fill", isSpeedActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21627
+ entryX += slotW + SPEED_BADGE_GAP;
21628
+ }
21167
21629
  }
21168
21630
  }
21169
21631
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21294,7 +21756,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21294
21756
  "viewBox",
21295
21757
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21296
21758
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21297
- renderLegend4(
21759
+ renderLegend3(
21298
21760
  legendSvg,
21299
21761
  legendGroups,
21300
21762
  containerWidth,
@@ -21306,7 +21768,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21306
21768
  );
21307
21769
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21308
21770
  } else {
21309
- renderLegend4(
21771
+ renderLegend3(
21310
21772
  rootSvg,
21311
21773
  legendGroups,
21312
21774
  totalWidth,
@@ -21340,6 +21802,7 @@ var init_renderer8 = __esm({
21340
21802
  init_compute();
21341
21803
  init_layout8();
21342
21804
  init_legend_constants();
21805
+ init_legend_d3();
21343
21806
  init_title_constants();
21344
21807
  NODE_FONT_SIZE3 = 13;
21345
21808
  META_FONT_SIZE5 = 10;
@@ -22965,7 +23428,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22965
23428
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22966
23429
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22967
23430
  const showIcon = !legendViewMode && tagGroups.length > 0;
22968
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23431
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22969
23432
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22970
23433
  let groupW = pillW;
22971
23434
  if (isActive) {
@@ -22992,83 +23455,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22992
23455
  const legendX = (containerWidth - totalW) / 2;
22993
23456
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22994
23457
  let cursorX = 0;
22995
- for (let i = 0; i < visibleGroups.length; i++) {
22996
- const group = visibleGroups[i];
22997
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22998
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23458
+ if (visibleGroups.length > 0) {
22999
23459
  const showIcon = !legendViewMode && tagGroups.length > 0;
23000
23460
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23001
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
23002
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
23003
- const groupW = groupWidths[i];
23004
- 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", () => {
23005
- if (onToggle) onToggle(group.name);
23461
+ const legendGroups = visibleGroups.map((g) => {
23462
+ const key = g.name.toLowerCase();
23463
+ const entries = filteredEntries.get(key) ?? g.entries;
23464
+ return {
23465
+ name: g.name,
23466
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23467
+ };
23006
23468
  });
23007
- if (isActive) {
23008
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
23009
- }
23010
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
23011
- const pillYOff = LEGEND_CAPSULE_PAD;
23012
- 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);
23013
- if (isActive) {
23014
- 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);
23015
- }
23016
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23017
- 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);
23018
- if (showIcon) {
23019
- const iconX = pillXOff + textW + 3;
23020
- const iconY = (LEGEND_HEIGHT - 10) / 2;
23021
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
23022
- iconEl.append("title").text(`Group by ${group.name}`);
23023
- iconEl.style("cursor", "pointer").on("click", (event) => {
23024
- event.stopPropagation();
23025
- if (onSwimlaneChange) {
23026
- onSwimlaneChange(
23027
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
23028
- );
23029
- }
23030
- });
23031
- }
23032
- if (isActive) {
23033
- const tagKey = group.name.toLowerCase();
23034
- const entries = filteredEntries.get(tagKey) ?? group.entries;
23035
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
23036
- for (const entry of entries) {
23037
- const entryValue = entry.value.toLowerCase();
23038
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
23039
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
23040
- 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);
23041
- entryG.on("mouseenter", () => {
23469
+ const legendConfig = {
23470
+ groups: legendGroups,
23471
+ position: {
23472
+ placement: "top-center",
23473
+ titleRelation: "below-title"
23474
+ },
23475
+ mode: "fixed",
23476
+ capsulePillAddonWidth: iconReserve
23477
+ };
23478
+ const legendState = { activeGroup: activeGroupName };
23479
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23480
+ const tagGroupG = legendRow.append("g");
23481
+ const legendCallbacks = {
23482
+ onGroupToggle: onToggle,
23483
+ onEntryHover: (groupName, entryValue) => {
23484
+ const tagKey = groupName.toLowerCase();
23485
+ if (entryValue) {
23486
+ const ev = entryValue.toLowerCase();
23042
23487
  chartG.selectAll(".gantt-task").each(function() {
23043
23488
  const el = d3Selection10.select(this);
23044
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23045
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23489
+ el.attr(
23490
+ "opacity",
23491
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23492
+ );
23046
23493
  });
23047
23494
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
23048
23495
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
23049
23496
  svg.selectAll(".gantt-task-label").each(function() {
23050
23497
  const el = d3Selection10.select(this);
23051
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23052
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23498
+ el.attr(
23499
+ "opacity",
23500
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23501
+ );
23053
23502
  });
23054
23503
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
23055
23504
  svg.selectAll(".gantt-lane-header").each(function() {
23056
23505
  const el = d3Selection10.select(this);
23057
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23058
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23506
+ el.attr(
23507
+ "opacity",
23508
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23509
+ );
23059
23510
  });
23060
23511
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
23061
- }).on("mouseleave", () => {
23512
+ } else {
23062
23513
  if (criticalPathActive) {
23063
23514
  applyCriticalPathHighlight(svg, chartG);
23064
23515
  } else {
23065
23516
  resetHighlightAll(svg, chartG);
23066
23517
  }
23067
- });
23068
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23518
+ }
23519
+ },
23520
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23521
+ const group = visibleGroups.find((g) => g.name === groupName);
23522
+ if (group) {
23523
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23524
+ }
23525
+ if (showIcon && _isActive) {
23526
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23527
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23528
+ const pillXOff = LEGEND_CAPSULE_PAD;
23529
+ const iconX = pillXOff + textW + 3;
23530
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23531
+ const iconEl = drawSwimlaneIcon(
23532
+ groupEl,
23533
+ iconX,
23534
+ iconY,
23535
+ isSwimlane,
23536
+ palette
23537
+ );
23538
+ iconEl.append("title").text(`Group by ${groupName}`);
23539
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23540
+ event.stopPropagation();
23541
+ if (onSwimlaneChange) {
23542
+ onSwimlaneChange(
23543
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23544
+ );
23545
+ }
23546
+ });
23547
+ }
23069
23548
  }
23549
+ };
23550
+ renderLegendD3(
23551
+ tagGroupG,
23552
+ legendConfig,
23553
+ legendState,
23554
+ palette,
23555
+ isDark,
23556
+ legendCallbacks,
23557
+ tagGroupsW
23558
+ );
23559
+ for (let i = 0; i < visibleGroups.length; i++) {
23560
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
23070
23561
  }
23071
- cursorX += groupW + LEGEND_GROUP_GAP;
23072
23562
  }
23073
23563
  if (hasCriticalPath) {
23074
23564
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23697,6 +24187,7 @@ var init_renderer9 = __esm({
23697
24187
  init_tag_groups();
23698
24188
  init_d3();
23699
24189
  init_legend_constants();
24190
+ init_legend_d3();
23700
24191
  init_title_constants();
23701
24192
  BAR_H = 22;
23702
24193
  ROW_GAP = 6;
@@ -24866,57 +25357,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24866
25357
  }
24867
25358
  if (parsed.tagGroups.length > 0) {
24868
25359
  const legendY = TOP_MARGIN + titleOffset;
24869
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24870
- const legendItems = [];
24871
- for (const tg of parsed.tagGroups) {
24872
- if (tg.entries.length === 0) continue;
24873
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24874
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24875
- const entries = tg.entries.map((e) => ({
25360
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25361
+ name: tg.name,
25362
+ entries: tg.entries.map((e) => ({
24876
25363
  value: e.value,
24877
25364
  color: resolveColor(e.color) ?? e.color
24878
- }));
24879
- let totalWidth2 = pillWidth2;
24880
- if (isActive) {
24881
- let entriesWidth2 = 0;
24882
- for (const entry of entries) {
24883
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24884
- }
24885
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24886
- }
24887
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24888
- }
24889
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24890
- let legendX = (svgWidth - totalLegendWidth) / 2;
24891
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24892
- if (activeTagGroup) {
24893
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24894
- }
24895
- for (const item of legendItems) {
24896
- 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");
24897
- if (item.isActive) {
24898
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24899
- }
24900
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24901
- const pillYOff = LEGEND_CAPSULE_PAD;
24902
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24903
- 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);
24904
- if (item.isActive) {
24905
- 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);
24906
- }
24907
- 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);
24908
- if (item.isActive) {
24909
- let entryX = pillXOff + item.pillWidth + 4;
24910
- for (const entry of item.entries) {
24911
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24912
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24913
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24914
- 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);
24915
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24916
- }
24917
- }
24918
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24919
- }
25365
+ }))
25366
+ }));
25367
+ const legendConfig = {
25368
+ groups: resolvedGroups,
25369
+ position: { placement: "top-center", titleRelation: "below-title" },
25370
+ mode: "fixed"
25371
+ };
25372
+ const legendState = { activeGroup: activeTagGroup ?? null };
25373
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25374
+ renderLegendD3(
25375
+ legendG,
25376
+ legendConfig,
25377
+ legendState,
25378
+ palette,
25379
+ isDark,
25380
+ void 0,
25381
+ svgWidth
25382
+ );
24920
25383
  }
24921
25384
  for (const group of groups) {
24922
25385
  if (group.participantIds.length === 0) continue;
@@ -25476,6 +25939,7 @@ var init_renderer10 = __esm({
25476
25939
  init_parser();
25477
25940
  init_tag_resolution();
25478
25941
  init_legend_constants();
25942
+ init_legend_d3();
25479
25943
  init_title_constants();
25480
25944
  PARTICIPANT_GAP = 160;
25481
25945
  PARTICIPANT_BOX_WIDTH = 120;
@@ -28131,7 +28595,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28131
28595
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28132
28596
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28133
28597
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28134
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28135
28598
  const LG_ICON_W = 20;
28136
28599
  const mainSvg = d3Selection13.select(container).select("svg");
28137
28600
  const mainG = mainSvg.select("g");
@@ -28170,11 +28633,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28170
28633
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28171
28634
  ) : legendGroups;
28172
28635
  if (visibleGroups.length === 0) return;
28173
- const totalW = visibleGroups.reduce((s, lg) => {
28174
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28175
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28176
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28177
- let cx = (width - totalW) / 2;
28178
28636
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28179
28637
  if (currentActiveGroup) {
28180
28638
  legendContainer.attr(
@@ -28182,82 +28640,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28182
28640
  currentActiveGroup.toLowerCase()
28183
28641
  );
28184
28642
  }
28185
- for (const lg of visibleGroups) {
28186
- const groupKey = lg.group.name.toLowerCase();
28187
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28188
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28189
- const pillLabel = lg.group.name;
28190
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28191
- 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__");
28192
- if (!viewMode) {
28193
- gEl.style("cursor", "pointer").on("click", () => {
28194
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28195
- drawLegend2();
28196
- recolorEvents2();
28197
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28198
- });
28199
- }
28200
- if (isActive) {
28201
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28202
- }
28203
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28204
- const pillYOff = LG_CAPSULE_PAD;
28205
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28206
- 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);
28207
- if (isActive) {
28208
- 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);
28209
- }
28210
- 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);
28211
- if (isActive) {
28212
- let entryX;
28213
- if (!viewMode) {
28214
- const iconX = pillXOff + pillWidth2 + 5;
28643
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28644
+ const centralGroups = visibleGroups.map((lg) => ({
28645
+ name: lg.group.name,
28646
+ entries: lg.group.entries.map((e) => ({
28647
+ value: e.value,
28648
+ color: e.color
28649
+ }))
28650
+ }));
28651
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28652
+ const centralConfig = {
28653
+ groups: centralGroups,
28654
+ position: { placement: "top-center", titleRelation: "below-title" },
28655
+ mode: "fixed",
28656
+ capsulePillAddonWidth: iconAddon
28657
+ };
28658
+ const centralState = { activeGroup: centralActive };
28659
+ const centralCallbacks = viewMode ? {} : {
28660
+ onGroupToggle: (groupName) => {
28661
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28662
+ drawLegend2();
28663
+ recolorEvents2();
28664
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28665
+ },
28666
+ onEntryHover: (groupName, entryValue) => {
28667
+ const tagKey = groupName.toLowerCase();
28668
+ if (entryValue) {
28669
+ const tagVal = entryValue.toLowerCase();
28670
+ fadeToTagValue(mainG, tagKey, tagVal);
28671
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28672
+ const el = d3Selection13.select(this);
28673
+ const ev = el.attr("data-legend-entry");
28674
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28675
+ el.attr(
28676
+ "opacity",
28677
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28678
+ );
28679
+ });
28680
+ } else {
28681
+ fadeReset(mainG);
28682
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28683
+ }
28684
+ },
28685
+ onGroupRendered: (groupName, groupEl, isActive) => {
28686
+ const groupKey = groupName.toLowerCase();
28687
+ groupEl.attr("data-tag-group", groupKey);
28688
+ if (isActive && !viewMode) {
28689
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28690
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28691
+ const pillXOff = LG_CAPSULE_PAD;
28692
+ const iconX = pillXOff + pillWidth3 + 5;
28215
28693
  const iconY = (LG_HEIGHT - 10) / 2;
28216
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28694
+ const iconEl = drawSwimlaneIcon3(
28695
+ groupEl,
28696
+ iconX,
28697
+ iconY,
28698
+ isSwimActive
28699
+ );
28217
28700
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28218
28701
  event.stopPropagation();
28219
28702
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28220
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28703
+ onTagStateChange?.(
28704
+ currentActiveGroup,
28705
+ currentSwimlaneGroup
28706
+ );
28221
28707
  relayout2();
28222
28708
  });
28223
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28224
- } else {
28225
- entryX = pillXOff + pillWidth2 + 8;
28226
- }
28227
- for (const entry of lg.group.entries) {
28228
- const tagKey = lg.group.name.toLowerCase();
28229
- const tagVal = entry.value.toLowerCase();
28230
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28231
- if (!viewMode) {
28232
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28233
- event.stopPropagation();
28234
- fadeToTagValue(mainG, tagKey, tagVal);
28235
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28236
- const el = d3Selection13.select(this);
28237
- const ev = el.attr("data-legend-entry");
28238
- if (ev === "__group__") return;
28239
- const eg = el.attr("data-tag-group");
28240
- el.attr(
28241
- "opacity",
28242
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28243
- );
28244
- });
28245
- }).on("mouseleave", (event) => {
28246
- event.stopPropagation();
28247
- fadeReset(mainG);
28248
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28249
- }).on("click", (event) => {
28250
- event.stopPropagation();
28251
- });
28252
- }
28253
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28254
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28255
- 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);
28256
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28257
28709
  }
28258
28710
  }
28259
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28260
- }
28711
+ };
28712
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28713
+ renderLegendD3(
28714
+ legendInnerG,
28715
+ centralConfig,
28716
+ centralState,
28717
+ palette,
28718
+ isDark,
28719
+ centralCallbacks,
28720
+ width
28721
+ );
28261
28722
  }, recolorEvents2 = function() {
28262
28723
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28263
28724
  mainG.selectAll(".tl-event").each(function() {
@@ -28282,7 +28743,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28282
28743
  };
28283
28744
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28284
28745
  const legendY = title ? 50 : 10;
28285
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28286
28746
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28287
28747
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28288
28748
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -29529,6 +29989,7 @@ var init_d3 = __esm({
29529
29989
  init_parsing();
29530
29990
  init_tag_groups();
29531
29991
  init_legend_constants();
29992
+ init_legend_d3();
29532
29993
  init_title_constants();
29533
29994
  DEFAULT_CLOUD_OPTIONS = {
29534
29995
  rotate: "none",
@@ -29707,6 +30168,7 @@ __export(index_exports, {
29707
30168
  computeCardMove: () => computeCardMove,
29708
30169
  computeInfra: () => computeInfra,
29709
30170
  computeInfraLegendGroups: () => computeInfraLegendGroups,
30171
+ computeLegendLayout: () => computeLegendLayout,
29710
30172
  computeScatterLabelGraphics: () => computeScatterLabelGraphics,
29711
30173
  computeTimeTicks: () => computeTimeTicks,
29712
30174
  contrastText: () => contrastText,
@@ -29718,6 +30180,7 @@ __export(index_exports, {
29718
30180
  formatDgmoError: () => formatDgmoError,
29719
30181
  getAvailablePalettes: () => getAvailablePalettes,
29720
30182
  getExtendedChartLegendGroups: () => getExtendedChartLegendGroups,
30183
+ getLegendReservedHeight: () => getLegendReservedHeight,
29721
30184
  getPalette: () => getPalette,
29722
30185
  getRenderCategory: () => getRenderCategory,
29723
30186
  getSeriesColors: () => getSeriesColors,
@@ -29806,7 +30269,9 @@ __export(index_exports, {
29806
30269
  renderInfra: () => renderInfra,
29807
30270
  renderKanban: () => renderKanban,
29808
30271
  renderKanbanForExport: () => renderKanbanForExport,
30272
+ renderLegendD3: () => renderLegendD3,
29809
30273
  renderLegendSvg: () => renderLegendSvg,
30274
+ renderLegendSvgFromConfig: () => renderLegendSvgFromConfig,
29810
30275
  renderOrg: () => renderOrg,
29811
30276
  renderOrgForExport: () => renderOrgForExport,
29812
30277
  renderQuadrant: () => renderQuadrant,
@@ -29846,11 +30311,26 @@ async function ensureDom() {
29846
30311
  const { JSDOM } = await import("jsdom");
29847
30312
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29848
30313
  const win = dom.window;
29849
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29850
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29851
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29852
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29853
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30314
+ Object.defineProperty(globalThis, "document", {
30315
+ value: win.document,
30316
+ configurable: true
30317
+ });
30318
+ Object.defineProperty(globalThis, "window", {
30319
+ value: win,
30320
+ configurable: true
30321
+ });
30322
+ Object.defineProperty(globalThis, "navigator", {
30323
+ value: win.navigator,
30324
+ configurable: true
30325
+ });
30326
+ Object.defineProperty(globalThis, "HTMLElement", {
30327
+ value: win.HTMLElement,
30328
+ configurable: true
30329
+ });
30330
+ Object.defineProperty(globalThis, "SVGElement", {
30331
+ value: win.SVGElement,
30332
+ configurable: true
30333
+ });
29854
30334
  }
29855
30335
  async function render(content, options) {
29856
30336
  const theme = options?.theme ?? "light";
@@ -29859,11 +30339,17 @@ async function render(content, options) {
29859
30339
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29860
30340
  const chartType = parseDgmoChartType(content);
29861
30341
  const category = chartType ? getRenderCategory(chartType) : null;
30342
+ const legendExportState = options?.legendState ? {
30343
+ activeTagGroup: options.legendState.activeGroup ?? null,
30344
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30345
+ } : void 0;
29862
30346
  if (category === "data-chart") {
29863
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30347
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30348
+ branding
30349
+ });
29864
30350
  }
29865
30351
  await ensureDom();
29866
- return renderForExport(content, theme, paletteColors, void 0, {
30352
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29867
30353
  branding,
29868
30354
  c4Level: options?.c4Level,
29869
30355
  c4System: options?.c4System,
@@ -30586,6 +31072,8 @@ init_flowchart_renderer();
30586
31072
  init_echarts();
30587
31073
  init_legend_svg();
30588
31074
  init_legend_constants();
31075
+ init_legend_d3();
31076
+ init_legend_layout();
30589
31077
  init_d3();
30590
31078
  init_renderer10();
30591
31079
  init_colors();
@@ -31430,6 +31918,7 @@ init_branding();
31430
31918
  computeCardMove,
31431
31919
  computeInfra,
31432
31920
  computeInfraLegendGroups,
31921
+ computeLegendLayout,
31433
31922
  computeScatterLabelGraphics,
31434
31923
  computeTimeTicks,
31435
31924
  contrastText,
@@ -31441,6 +31930,7 @@ init_branding();
31441
31930
  formatDgmoError,
31442
31931
  getAvailablePalettes,
31443
31932
  getExtendedChartLegendGroups,
31933
+ getLegendReservedHeight,
31444
31934
  getPalette,
31445
31935
  getRenderCategory,
31446
31936
  getSeriesColors,
@@ -31529,7 +32019,9 @@ init_branding();
31529
32019
  renderInfra,
31530
32020
  renderKanban,
31531
32021
  renderKanbanForExport,
32022
+ renderLegendD3,
31532
32023
  renderLegendSvg,
32024
+ renderLegendSvgFromConfig,
31533
32025
  renderOrg,
31534
32026
  renderOrgForExport,
31535
32027
  renderQuadrant,