@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.js CHANGED
@@ -1901,6 +1901,493 @@ var init_legend_constants = __esm({
1901
1901
  }
1902
1902
  });
1903
1903
 
1904
+ // src/utils/legend-layout.ts
1905
+ function pillWidth(name) {
1906
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
1907
+ }
1908
+ function entriesWidth(entries) {
1909
+ let w = 0;
1910
+ for (const e of entries) {
1911
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1912
+ }
1913
+ return w;
1914
+ }
1915
+ function entryWidth(value) {
1916
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
1917
+ }
1918
+ function controlWidth(control) {
1919
+ let w = CONTROL_PILL_PAD;
1920
+ if (control.label) {
1921
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
1922
+ if (control.icon) w += CONTROL_ICON_GAP;
1923
+ }
1924
+ if (control.icon) w += 14;
1925
+ if (control.children) {
1926
+ for (const child of control.children) {
1927
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
1928
+ }
1929
+ }
1930
+ return w;
1931
+ }
1932
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
1933
+ const pw = pillWidth(name);
1934
+ const maxCapsuleW = containerWidth;
1935
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
1936
+ const ew = entriesWidth(entries);
1937
+ const singleRowW = baseW + ew;
1938
+ if (singleRowW <= maxCapsuleW) {
1939
+ return {
1940
+ width: singleRowW,
1941
+ entryRows: 1,
1942
+ moreCount: 0,
1943
+ visibleEntries: entries.length
1944
+ };
1945
+ }
1946
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
1947
+ let row = 1;
1948
+ let rowX = pw + 4;
1949
+ let visible = 0;
1950
+ for (let i = 0; i < entries.length; i++) {
1951
+ const ew2 = entryWidth(entries[i].value);
1952
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
1953
+ row++;
1954
+ rowX = 0;
1955
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
1956
+ return {
1957
+ width: maxCapsuleW,
1958
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
1959
+ moreCount: entries.length - visible,
1960
+ visibleEntries: visible
1961
+ };
1962
+ }
1963
+ }
1964
+ rowX += ew2;
1965
+ visible++;
1966
+ }
1967
+ return {
1968
+ width: maxCapsuleW,
1969
+ entryRows: row,
1970
+ moreCount: 0,
1971
+ visibleEntries: entries.length
1972
+ };
1973
+ }
1974
+ function computeLegendLayout(config, state, containerWidth) {
1975
+ const { groups, controls: configControls, mode } = config;
1976
+ const isExport = mode === "inline";
1977
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
1978
+ if (isExport && !activeGroupName) {
1979
+ return {
1980
+ height: 0,
1981
+ width: 0,
1982
+ rows: [],
1983
+ controls: [],
1984
+ pills: [],
1985
+ activeCapsule: void 0
1986
+ };
1987
+ }
1988
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
1989
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
1990
+ return {
1991
+ height: 0,
1992
+ width: 0,
1993
+ rows: [],
1994
+ controls: [],
1995
+ pills: [],
1996
+ activeCapsule: void 0
1997
+ };
1998
+ }
1999
+ const controlLayouts = [];
2000
+ let totalControlsW = 0;
2001
+ if (configControls && !isExport) {
2002
+ for (const ctrl of configControls) {
2003
+ const w = controlWidth(ctrl);
2004
+ controlLayouts.push({
2005
+ id: ctrl.id,
2006
+ x: 0,
2007
+ // positioned later
2008
+ y: 0,
2009
+ width: w,
2010
+ height: LEGEND_HEIGHT,
2011
+ icon: ctrl.icon,
2012
+ label: ctrl.label,
2013
+ exportBehavior: ctrl.exportBehavior,
2014
+ children: ctrl.children?.map((c) => ({
2015
+ id: c.id,
2016
+ label: c.label,
2017
+ x: 0,
2018
+ y: 0,
2019
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2020
+ isActive: c.isActive
2021
+ }))
2022
+ });
2023
+ totalControlsW += w + CONTROL_GAP;
2024
+ }
2025
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2026
+ } else if (configControls && isExport) {
2027
+ for (const ctrl of configControls) {
2028
+ if (ctrl.exportBehavior === "strip") continue;
2029
+ const w = controlWidth(ctrl);
2030
+ controlLayouts.push({
2031
+ id: ctrl.id,
2032
+ x: 0,
2033
+ y: 0,
2034
+ width: w,
2035
+ height: LEGEND_HEIGHT,
2036
+ icon: ctrl.icon,
2037
+ label: ctrl.label,
2038
+ exportBehavior: ctrl.exportBehavior,
2039
+ children: ctrl.children?.map((c) => ({
2040
+ id: c.id,
2041
+ label: c.label,
2042
+ x: 0,
2043
+ y: 0,
2044
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2045
+ isActive: c.isActive
2046
+ }))
2047
+ });
2048
+ totalControlsW += w + CONTROL_GAP;
2049
+ }
2050
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2051
+ }
2052
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2053
+ const groupAvailW = containerWidth - controlsSpace;
2054
+ const pills = [];
2055
+ let activeCapsule;
2056
+ for (const g of visibleGroups) {
2057
+ const isActive = activeGroupName === g.name.toLowerCase();
2058
+ if (isExport && !isActive) continue;
2059
+ if (isActive) {
2060
+ activeCapsule = buildCapsuleLayout(
2061
+ g,
2062
+ containerWidth,
2063
+ config.capsulePillAddonWidth ?? 0
2064
+ );
2065
+ } else {
2066
+ const pw = pillWidth(g.name);
2067
+ pills.push({
2068
+ groupName: g.name,
2069
+ x: 0,
2070
+ y: 0,
2071
+ width: pw,
2072
+ height: LEGEND_HEIGHT,
2073
+ isActive: false
2074
+ });
2075
+ }
2076
+ }
2077
+ const rows = layoutRows(
2078
+ activeCapsule,
2079
+ pills,
2080
+ controlLayouts,
2081
+ groupAvailW,
2082
+ containerWidth,
2083
+ totalControlsW
2084
+ );
2085
+ const height = rows.length * LEGEND_HEIGHT;
2086
+ const width = containerWidth;
2087
+ return {
2088
+ height,
2089
+ width,
2090
+ rows,
2091
+ activeCapsule,
2092
+ controls: controlLayouts,
2093
+ pills
2094
+ };
2095
+ }
2096
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2097
+ const pw = pillWidth(group.name);
2098
+ const info = capsuleWidth(
2099
+ group.name,
2100
+ group.entries,
2101
+ containerWidth,
2102
+ addonWidth
2103
+ );
2104
+ const pill = {
2105
+ groupName: group.name,
2106
+ x: LEGEND_CAPSULE_PAD,
2107
+ y: LEGEND_CAPSULE_PAD,
2108
+ width: pw,
2109
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2110
+ isActive: true
2111
+ };
2112
+ const entries = [];
2113
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2114
+ let ey = 0;
2115
+ let rowX = ex;
2116
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2117
+ let currentRow = 0;
2118
+ for (let i = 0; i < info.visibleEntries; i++) {
2119
+ const entry = group.entries[i];
2120
+ const ew = entryWidth(entry.value);
2121
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2122
+ currentRow++;
2123
+ rowX = 0;
2124
+ ey = currentRow * LEGEND_HEIGHT;
2125
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2126
+ }
2127
+ const dotCx = rowX + LEGEND_DOT_R;
2128
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2129
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2130
+ const textY = ey + LEGEND_HEIGHT / 2;
2131
+ entries.push({
2132
+ value: entry.value,
2133
+ color: entry.color,
2134
+ x: rowX,
2135
+ y: ey,
2136
+ dotCx,
2137
+ dotCy,
2138
+ textX,
2139
+ textY
2140
+ });
2141
+ rowX += ew;
2142
+ }
2143
+ const totalRows = info.entryRows;
2144
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2145
+ return {
2146
+ groupName: group.name,
2147
+ x: 0,
2148
+ y: 0,
2149
+ width: info.width,
2150
+ height: capsuleH,
2151
+ pill,
2152
+ entries,
2153
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2154
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2155
+ };
2156
+ }
2157
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2158
+ const rows = [];
2159
+ const groupItems = [];
2160
+ if (activeCapsule) groupItems.push(activeCapsule);
2161
+ groupItems.push(...pills);
2162
+ let currentRowItems = [];
2163
+ let currentRowW = 0;
2164
+ let rowY = 0;
2165
+ for (const item of groupItems) {
2166
+ const itemW = item.width + LEGEND_GROUP_GAP;
2167
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2168
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2169
+ rows.push({ y: rowY, items: currentRowItems });
2170
+ rowY += LEGEND_HEIGHT;
2171
+ currentRowItems = [];
2172
+ currentRowW = 0;
2173
+ }
2174
+ item.x = currentRowW;
2175
+ item.y = rowY;
2176
+ currentRowItems.push(item);
2177
+ currentRowW += itemW;
2178
+ }
2179
+ if (controls.length > 0) {
2180
+ let cx = containerWidth;
2181
+ for (let i = controls.length - 1; i >= 0; i--) {
2182
+ cx -= controls[i].width;
2183
+ controls[i].x = cx;
2184
+ controls[i].y = 0;
2185
+ cx -= CONTROL_GAP;
2186
+ }
2187
+ if (rows.length > 0) {
2188
+ rows[0].items.push(...controls);
2189
+ } else if (currentRowItems.length > 0) {
2190
+ currentRowItems.push(...controls);
2191
+ } else {
2192
+ currentRowItems.push(...controls);
2193
+ }
2194
+ }
2195
+ if (currentRowItems.length > 0) {
2196
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2197
+ rows.push({ y: rowY, items: currentRowItems });
2198
+ }
2199
+ if (rows.length === 0) {
2200
+ rows.push({ y: 0, items: [] });
2201
+ }
2202
+ return rows;
2203
+ }
2204
+ function centerRowItems(items, containerWidth, totalControlsW) {
2205
+ const groupItems = items.filter((it) => "groupName" in it);
2206
+ if (groupItems.length === 0) return;
2207
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2208
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2209
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2210
+ let x = offset;
2211
+ for (const item of groupItems) {
2212
+ item.x = x;
2213
+ x += item.width + LEGEND_GROUP_GAP;
2214
+ }
2215
+ }
2216
+ function getLegendReservedHeight(config, state, containerWidth) {
2217
+ const layout = computeLegendLayout(config, state, containerWidth);
2218
+ return layout.height;
2219
+ }
2220
+ var LEGEND_MAX_ENTRY_ROWS, CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2221
+ var init_legend_layout = __esm({
2222
+ "src/utils/legend-layout.ts"() {
2223
+ "use strict";
2224
+ init_legend_constants();
2225
+ LEGEND_MAX_ENTRY_ROWS = 3;
2226
+ CONTROL_PILL_PAD = 16;
2227
+ CONTROL_FONT_SIZE = 11;
2228
+ CONTROL_ICON_GAP = 4;
2229
+ CONTROL_GAP = 8;
2230
+ }
2231
+ });
2232
+
2233
+ // src/utils/legend-d3.ts
2234
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2235
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2236
+ let currentState = { ...state };
2237
+ let currentLayout;
2238
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2239
+ function render2() {
2240
+ currentLayout = computeLegendLayout(config, currentState, width);
2241
+ legendG.selectAll("*").remove();
2242
+ if (currentLayout.height === 0) return;
2243
+ if (currentState.activeGroup) {
2244
+ legendG.attr(
2245
+ "data-legend-active",
2246
+ currentState.activeGroup.toLowerCase()
2247
+ );
2248
+ } else {
2249
+ legendG.attr("data-legend-active", null);
2250
+ }
2251
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2252
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2253
+ if (currentLayout.activeCapsule) {
2254
+ renderCapsule(
2255
+ legendG,
2256
+ currentLayout.activeCapsule,
2257
+ palette,
2258
+ groupBg,
2259
+ pillBorder,
2260
+ isDark,
2261
+ callbacks
2262
+ );
2263
+ }
2264
+ for (const pill of currentLayout.pills) {
2265
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2266
+ }
2267
+ for (const ctrl of currentLayout.controls) {
2268
+ renderControl(
2269
+ legendG,
2270
+ ctrl,
2271
+ palette,
2272
+ groupBg,
2273
+ pillBorder,
2274
+ isDark,
2275
+ config.controls
2276
+ );
2277
+ }
2278
+ }
2279
+ render2();
2280
+ return {
2281
+ setState(newState) {
2282
+ currentState = { ...newState };
2283
+ render2();
2284
+ },
2285
+ destroy() {
2286
+ legendG.remove();
2287
+ },
2288
+ getHeight() {
2289
+ return currentLayout?.height ?? 0;
2290
+ },
2291
+ getLayout() {
2292
+ return currentLayout;
2293
+ }
2294
+ };
2295
+ }
2296
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2297
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2298
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2299
+ const pill = capsule.pill;
2300
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
2301
+ g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2302
+ g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
2303
+ for (const entry of capsule.entries) {
2304
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2305
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2306
+ entryG.append("text").attr("x", entry.textX).attr("y", entry.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.value);
2307
+ if (callbacks?.onEntryHover) {
2308
+ const groupName = capsule.groupName;
2309
+ const entryValue = entry.value;
2310
+ const onHover = callbacks.onEntryHover;
2311
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2312
+ }
2313
+ }
2314
+ if (capsule.moreCount) {
2315
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2316
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2317
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2318
+ g.append("text").attr("x", moreX).attr("y", moreY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-style", "italic").attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(`+${capsule.moreCount} more`);
2319
+ }
2320
+ if (callbacks?.onGroupToggle) {
2321
+ const cb = callbacks.onGroupToggle;
2322
+ const name = capsule.groupName;
2323
+ g.on("click", () => cb(name));
2324
+ }
2325
+ if (callbacks?.onGroupRendered) {
2326
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2327
+ }
2328
+ }
2329
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2330
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2331
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2332
+ g.append("text").attr("x", pill.width / 2).attr("y", pill.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(pill.groupName);
2333
+ if (callbacks?.onGroupToggle) {
2334
+ const cb = callbacks.onGroupToggle;
2335
+ const name = pill.groupName;
2336
+ g.on("click", () => cb(name));
2337
+ }
2338
+ if (callbacks?.onGroupRendered) {
2339
+ callbacks.onGroupRendered(pill.groupName, g, false);
2340
+ }
2341
+ }
2342
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2343
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2344
+ if (ctrl.exportBehavior === "strip") {
2345
+ g.attr("data-export-ignore", "true");
2346
+ }
2347
+ g.append("rect").attr("width", ctrl.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
2348
+ let textX = ctrl.width / 2;
2349
+ if (ctrl.icon && ctrl.label) {
2350
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2351
+ iconG.html(ctrl.icon);
2352
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2353
+ }
2354
+ if (ctrl.label) {
2355
+ g.append("text").attr("x", textX).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(ctrl.label);
2356
+ }
2357
+ if (ctrl.children) {
2358
+ let cx = ctrl.width + 4;
2359
+ for (const child of ctrl.children) {
2360
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2361
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2362
+ "fill",
2363
+ child.isActive ? palette.primary ?? palette.text : "none"
2364
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2365
+ childG.append("text").attr("x", child.width / 2).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", child.isActive ? palette.bg : palette.textMuted).attr("font-family", FONT_FAMILY).text(child.label);
2366
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2367
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2368
+ if (configChild?.onClick) {
2369
+ const onClick = configChild.onClick;
2370
+ childG.on("click", () => onClick());
2371
+ }
2372
+ cx += child.width + 4;
2373
+ }
2374
+ }
2375
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2376
+ if (configCtrl?.onClick) {
2377
+ const onClick = configCtrl.onClick;
2378
+ g.on("click", () => onClick());
2379
+ }
2380
+ }
2381
+ var init_legend_d3 = __esm({
2382
+ "src/utils/legend-d3.ts"() {
2383
+ "use strict";
2384
+ init_legend_constants();
2385
+ init_legend_layout();
2386
+ init_color_utils();
2387
+ init_fonts();
2388
+ }
2389
+ });
2390
+
1904
2391
  // src/utils/title-constants.ts
1905
2392
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1906
2393
  var init_title_constants = __esm({
@@ -4830,10 +5317,10 @@ var init_chart = __esm({
4830
5317
  function esc(s) {
4831
5318
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4832
5319
  }
4833
- function pillWidth(name) {
5320
+ function pillWidth2(name) {
4834
5321
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4835
5322
  }
4836
- function entriesWidth(entries) {
5323
+ function entriesWidth2(entries) {
4837
5324
  let w = 0;
4838
5325
  for (const e of entries) {
4839
5326
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4841,9 +5328,9 @@ function entriesWidth(entries) {
4841
5328
  return w;
4842
5329
  }
4843
5330
  function groupTotalWidth(name, entries, isActive) {
4844
- const pw = pillWidth(name);
5331
+ const pw = pillWidth2(name);
4845
5332
  if (!isActive) return pw;
4846
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5333
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4847
5334
  }
4848
5335
  function renderLegendSvg(groups, options) {
4849
5336
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4851,7 +5338,7 @@ function renderLegendSvg(groups, options) {
4851
5338
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4852
5339
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4853
5340
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4854
- const pw = pillWidth(g.name);
5341
+ const pw = pillWidth2(g.name);
4855
5342
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4856
5343
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4857
5344
  });
@@ -4905,6 +5392,19 @@ function renderLegendSvg(groups, options) {
4905
5392
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4906
5393
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4907
5394
  }
5395
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5396
+ return renderLegendSvg(config.groups, {
5397
+ palette: {
5398
+ bg: palette.bg,
5399
+ surface: palette.surface,
5400
+ text: palette.text,
5401
+ textMuted: palette.textMuted
5402
+ },
5403
+ isDark: palette.isDark,
5404
+ containerWidth,
5405
+ activeGroup: state.activeGroup
5406
+ });
5407
+ }
4908
5408
  var init_legend_svg = __esm({
4909
5409
  "src/utils/legend-svg.ts"() {
4910
5410
  "use strict";
@@ -5334,52 +5834,26 @@ function buildChartCommons(parsed, palette, isDark) {
5334
5834
  fontFamily: FONT_FAMILY
5335
5835
  }
5336
5836
  } : void 0;
5337
- const tooltipTheme = {
5338
- backgroundColor: palette.surface,
5339
- borderColor: palette.border,
5340
- textStyle: { color: palette.text }
5341
- };
5342
5837
  return {
5343
5838
  textColor,
5344
5839
  axisLineColor,
5345
5840
  splitLineColor,
5346
5841
  gridOpacity,
5347
5842
  colors,
5348
- titleConfig,
5349
- tooltipTheme
5843
+ titleConfig
5350
5844
  };
5351
5845
  }
5352
5846
  function buildExtendedChartOption(parsed, palette, isDark) {
5353
5847
  if (parsed.error) {
5354
5848
  return {};
5355
5849
  }
5356
- const {
5357
- textColor,
5358
- axisLineColor,
5359
- gridOpacity,
5360
- colors,
5361
- titleConfig,
5362
- tooltipTheme
5363
- } = buildChartCommons(parsed, palette, isDark);
5850
+ const { textColor, axisLineColor, gridOpacity, colors, titleConfig } = buildChartCommons(parsed, palette, isDark);
5364
5851
  if (parsed.type === "sankey") {
5365
- return buildSankeyOption(
5366
- parsed,
5367
- textColor,
5368
- colors,
5369
- titleConfig,
5370
- tooltipTheme
5371
- );
5852
+ return buildSankeyOption(parsed, textColor, colors, titleConfig);
5372
5853
  }
5373
5854
  if (parsed.type === "chord") {
5374
5855
  const bg = isDark ? palette.surface : palette.bg;
5375
- return buildChordOption(
5376
- parsed,
5377
- textColor,
5378
- colors,
5379
- bg,
5380
- titleConfig,
5381
- tooltipTheme
5382
- );
5856
+ return buildChordOption(parsed, textColor, colors, bg, titleConfig);
5383
5857
  }
5384
5858
  if (parsed.type === "function") {
5385
5859
  return buildFunctionOption(
@@ -5389,8 +5863,7 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5389
5863
  axisLineColor,
5390
5864
  gridOpacity,
5391
5865
  colors,
5392
- titleConfig,
5393
- tooltipTheme
5866
+ titleConfig
5394
5867
  );
5395
5868
  }
5396
5869
  if (parsed.type === "scatter") {
@@ -5403,20 +5876,12 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5403
5876
  gridOpacity,
5404
5877
  colors,
5405
5878
  bg,
5406
- titleConfig,
5407
- tooltipTheme
5879
+ titleConfig
5408
5880
  );
5409
5881
  }
5410
5882
  if (parsed.type === "funnel") {
5411
5883
  const bg = isDark ? palette.surface : palette.bg;
5412
- return buildFunnelOption(
5413
- parsed,
5414
- textColor,
5415
- colors,
5416
- bg,
5417
- titleConfig,
5418
- tooltipTheme
5419
- );
5884
+ return buildFunnelOption(parsed, textColor, colors, bg, titleConfig);
5420
5885
  }
5421
5886
  return buildHeatmapOption(
5422
5887
  parsed,
@@ -5424,11 +5889,10 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5424
5889
  isDark,
5425
5890
  textColor,
5426
5891
  axisLineColor,
5427
- titleConfig,
5428
- tooltipTheme
5892
+ titleConfig
5429
5893
  );
5430
5894
  }
5431
- function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme) {
5895
+ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5432
5896
  const nodeSet = /* @__PURE__ */ new Set();
5433
5897
  if (parsed.links) {
5434
5898
  for (const link of parsed.links) {
@@ -5447,17 +5911,15 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme)
5447
5911
  title: titleConfig,
5448
5912
  xAxis: { show: false },
5449
5913
  yAxis: { show: false },
5450
- tooltip: {
5451
- show: false,
5452
- ...tooltipTheme
5453
- },
5454
5914
  series: [
5455
5915
  {
5456
5916
  type: "sankey",
5457
5917
  emphasis: {
5458
5918
  focus: "adjacency",
5459
- blurScope: "global"
5919
+ blurScope: "global",
5920
+ itemStyle: { opacity: 1 }
5460
5921
  },
5922
+ blur: BLUR_DIM,
5461
5923
  nodeAlign: "left",
5462
5924
  nodeGap: 12,
5463
5925
  nodeWidth: 20,
@@ -5480,7 +5942,7 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig, tooltipTheme)
5480
5942
  ]
5481
5943
  };
5482
5944
  }
5483
- function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
5945
+ function buildChordOption(parsed, textColor, colors, bg, titleConfig) {
5484
5946
  const nodeSet = /* @__PURE__ */ new Set();
5485
5947
  if (parsed.links) {
5486
5948
  for (const link of parsed.links) {
@@ -5514,17 +5976,6 @@ function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipThe
5514
5976
  return {
5515
5977
  ...CHART_BASE,
5516
5978
  title: titleConfig,
5517
- tooltip: {
5518
- trigger: "item",
5519
- ...tooltipTheme,
5520
- formatter: (params) => {
5521
- const p = params;
5522
- if (p.data && p.data.source && p.data.target) {
5523
- return `${p.data.source} \u2192 ${p.data.target}: ${p.data.value}`;
5524
- }
5525
- return "";
5526
- }
5527
- },
5528
5979
  xAxis: { show: false },
5529
5980
  yAxis: { show: false },
5530
5981
  series: [
@@ -5583,11 +6034,13 @@ function buildChordOption(parsed, textColor, colors, bg, titleConfig, tooltipThe
5583
6034
  },
5584
6035
  emphasis: {
5585
6036
  focus: "adjacency",
6037
+ itemStyle: { opacity: 1 },
5586
6038
  lineStyle: {
5587
6039
  width: 5,
5588
6040
  opacity: 1
5589
6041
  }
5590
- }
6042
+ },
6043
+ blur: BLUR_DIM
5591
6044
  }
5592
6045
  ]
5593
6046
  };
@@ -5601,7 +6054,7 @@ function evaluateExpression(expr, x) {
5601
6054
  return NaN;
5602
6055
  }
5603
6056
  }
5604
- function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, titleConfig, tooltipTheme) {
6057
+ function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, titleConfig) {
5605
6058
  const xRange = parsed.xRange ?? { min: -10, max: 10 };
5606
6059
  const samples = 200;
5607
6060
  const step = (xRange.max - xRange.min) / samples;
@@ -5634,19 +6087,13 @@ function buildFunctionOption(parsed, palette, textColor, axisLineColor, gridOpac
5634
6087
  opacity: 0.15
5635
6088
  }
5636
6089
  },
5637
- emphasis: EMPHASIS_SELF
6090
+ emphasis: EMPHASIS_SELF,
6091
+ blur: BLUR_DIM
5638
6092
  };
5639
6093
  });
5640
6094
  return {
5641
6095
  ...CHART_BASE,
5642
6096
  title: titleConfig,
5643
- tooltip: {
5644
- trigger: "axis",
5645
- ...tooltipTheme,
5646
- axisPointer: {
5647
- type: "cross"
5648
- }
5649
- },
5650
6097
  legend: {
5651
6098
  data: (parsed.functions ?? []).map((fn) => fn.name),
5652
6099
  bottom: 10,
@@ -5895,7 +6342,7 @@ function dataToPixel(dataX, dataY, xMin, xMax, yMin, yMax, gridLeftPct, gridRigh
5895
6342
  const py = gridTopPx + (yMax - dataY) / (yMax - yMin) * plotHeight;
5896
6343
  return { px, py };
5897
6344
  }
5898
- function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme) {
6345
+ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpacity, colors, bg, titleConfig) {
5899
6346
  const points = parsed.scatterPoints ?? [];
5900
6347
  const defaultSize = 15;
5901
6348
  const hasCategories = points.some((p) => p.category !== void 0);
@@ -5911,11 +6358,9 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5911
6358
  };
5912
6359
  const emphasisConfig = {
5913
6360
  focus: "self",
5914
- itemStyle: {
5915
- shadowBlur: 10,
5916
- shadowColor: "rgba(0, 0, 0, 0.3)"
5917
- }
6361
+ itemStyle: { opacity: 1 }
5918
6362
  };
6363
+ const blurConfig = BLUR_DIM;
5919
6364
  let series;
5920
6365
  if (hasCategories) {
5921
6366
  const categories2 = [
@@ -5946,7 +6391,8 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5946
6391
  borderWidth: CHART_BORDER_WIDTH
5947
6392
  },
5948
6393
  label: labelConfig,
5949
- emphasis: emphasisConfig
6394
+ emphasis: emphasisConfig,
6395
+ blur: blurConfig
5950
6396
  };
5951
6397
  });
5952
6398
  } else {
@@ -5968,24 +6414,11 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
5968
6414
  type: "scatter",
5969
6415
  data,
5970
6416
  label: labelConfig,
5971
- emphasis: emphasisConfig
6417
+ emphasis: emphasisConfig,
6418
+ blur: blurConfig
5972
6419
  }
5973
6420
  ];
5974
6421
  }
5975
- const tooltip = {
5976
- trigger: "item",
5977
- ...tooltipTheme,
5978
- formatter: (params) => {
5979
- const p = params;
5980
- const xLabel = parsed.xlabel || "x";
5981
- const yLabel = parsed.ylabel || "y";
5982
- let html = `<strong>${p.name}</strong>`;
5983
- if (hasCategories) html += `<br/>${p.seriesName}`;
5984
- html += `<br/>${xLabel}: ${p.value[0]}<br/>${yLabel}: ${p.value[1]}`;
5985
- if (hasSize) html += `<br/>${parsed.sizelabel || "size"}: ${p.value[2]}`;
5986
- return html;
5987
- }
5988
- };
5989
6422
  const xValues = points.map((p) => p.x);
5990
6423
  const yValues = points.map((p) => p.y);
5991
6424
  const xMin = Math.min(...xValues);
@@ -6066,7 +6499,6 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
6066
6499
  ...CHART_BASE,
6067
6500
  title: titleConfig,
6068
6501
  ...legendConfig && { legend: legendConfig },
6069
- tooltip,
6070
6502
  grid: {
6071
6503
  left: `${gridLeft}%`,
6072
6504
  right: `${gridRight}%`,
@@ -6128,7 +6560,7 @@ function buildScatterOption(parsed, palette, textColor, axisLineColor, gridOpaci
6128
6560
  ...graphic && { graphic }
6129
6561
  };
6130
6562
  }
6131
- function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, titleConfig, tooltipTheme) {
6563
+ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, titleConfig) {
6132
6564
  const bg = isDark ? palette.surface : palette.bg;
6133
6565
  const heatmapRows = parsed.heatmapRows ?? [];
6134
6566
  const columns = parsed.columns ?? [];
@@ -6146,16 +6578,6 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6146
6578
  return {
6147
6579
  ...CHART_BASE,
6148
6580
  title: titleConfig,
6149
- tooltip: {
6150
- trigger: "item",
6151
- ...tooltipTheme,
6152
- formatter: (params) => {
6153
- const p = params;
6154
- const colName = columns[p.data[0]] ?? p.data[0];
6155
- const rowName = rowLabels[p.data[1]] ?? p.data[1];
6156
- return `${rowName} / ${colName}: <strong>${p.data[2]}</strong>`;
6157
- }
6158
- },
6159
6581
  grid: {
6160
6582
  left: "3%",
6161
6583
  right: "10%",
@@ -6225,19 +6647,15 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6225
6647
  fontWeight: "bold"
6226
6648
  },
6227
6649
  emphasis: {
6228
- ...EMPHASIS_SELF,
6229
- itemStyle: {
6230
- shadowBlur: 10,
6231
- shadowColor: "rgba(0, 0, 0, 0.5)"
6232
- }
6233
- }
6650
+ ...EMPHASIS_SELF
6651
+ },
6652
+ blur: BLUR_DIM
6234
6653
  }
6235
6654
  ]
6236
6655
  };
6237
6656
  }
6238
- function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
6657
+ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig) {
6239
6658
  const sorted = [...parsed.data].sort((a, b) => b.value - a.value);
6240
- const topValue = sorted.length > 0 ? sorted[0].value : 1;
6241
6659
  const data = sorted.map((d) => {
6242
6660
  const stroke2 = d.color ?? colors[parsed.data.indexOf(d) % colors.length];
6243
6661
  return {
@@ -6272,25 +6690,6 @@ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTh
6272
6690
  title: titleConfig,
6273
6691
  xAxis: { show: false },
6274
6692
  yAxis: { show: false },
6275
- tooltip: {
6276
- trigger: "item",
6277
- ...tooltipTheme,
6278
- formatter: (params) => {
6279
- const p = params;
6280
- const val = p.value;
6281
- const prev = prevValueMap.get(p.name) ?? val;
6282
- const isFirst = p.dataIndex === 0;
6283
- if (isFirst) return "";
6284
- const parts = [];
6285
- const stepDrop = ((1 - val / prev) * 100).toFixed(1);
6286
- parts.push(`Step drop-off: ${stepDrop}%`);
6287
- if (topValue > 0) {
6288
- const totalDrop = ((1 - val / topValue) * 100).toFixed(1);
6289
- parts.push(`Overall drop-off: ${totalDrop}%`);
6290
- }
6291
- return parts.join("<br/>");
6292
- }
6293
- },
6294
6693
  series: [
6295
6694
  {
6296
6695
  type: "funnel",
@@ -6308,11 +6707,9 @@ function buildFunnelOption(parsed, textColor, colors, bg, titleConfig, tooltipTh
6308
6707
  lineStyle: { color: textColor, opacity: 0.3 }
6309
6708
  },
6310
6709
  emphasis: {
6311
- ...EMPHASIS_SELF,
6312
- label: {
6313
- fontSize: 15
6314
- }
6710
+ ...EMPHASIS_SELF
6315
6711
  },
6712
+ blur: BLUR_DIM,
6316
6713
  data
6317
6714
  },
6318
6715
  {
@@ -6406,8 +6803,7 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6406
6803
  splitLineColor,
6407
6804
  gridOpacity,
6408
6805
  colors,
6409
- titleConfig,
6410
- tooltipTheme
6806
+ titleConfig
6411
6807
  } = buildChartCommons(parsed, palette, isDark);
6412
6808
  const bg = isDark ? palette.surface : palette.bg;
6413
6809
  switch (parsed.type) {
@@ -6421,7 +6817,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6421
6817
  colors,
6422
6818
  bg,
6423
6819
  titleConfig,
6424
- tooltipTheme,
6425
6820
  chartWidth
6426
6821
  );
6427
6822
  case "bar-stacked":
@@ -6434,7 +6829,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6434
6829
  colors,
6435
6830
  bg,
6436
6831
  titleConfig,
6437
- tooltipTheme,
6438
6832
  chartWidth
6439
6833
  );
6440
6834
  case "line":
@@ -6447,7 +6841,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6447
6841
  gridOpacity,
6448
6842
  colors,
6449
6843
  titleConfig,
6450
- tooltipTheme,
6451
6844
  chartWidth
6452
6845
  ) : buildLineOption(
6453
6846
  parsed,
@@ -6457,7 +6850,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6457
6850
  splitLineColor,
6458
6851
  gridOpacity,
6459
6852
  titleConfig,
6460
- tooltipTheme,
6461
6853
  chartWidth
6462
6854
  );
6463
6855
  case "area":
@@ -6469,7 +6861,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6469
6861
  splitLineColor,
6470
6862
  gridOpacity,
6471
6863
  titleConfig,
6472
- tooltipTheme,
6473
6864
  chartWidth
6474
6865
  );
6475
6866
  case "pie":
@@ -6479,7 +6870,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6479
6870
  getSegmentColors(palette, parsed.data.length),
6480
6871
  bg,
6481
6872
  titleConfig,
6482
- tooltipTheme,
6483
6873
  false
6484
6874
  );
6485
6875
  case "doughnut":
@@ -6489,7 +6879,6 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6489
6879
  getSegmentColors(palette, parsed.data.length),
6490
6880
  bg,
6491
6881
  titleConfig,
6492
- tooltipTheme,
6493
6882
  true
6494
6883
  );
6495
6884
  case "radar":
@@ -6499,8 +6888,7 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6499
6888
  isDark,
6500
6889
  textColor,
6501
6890
  gridOpacity,
6502
- titleConfig,
6503
- tooltipTheme
6891
+ titleConfig
6504
6892
  );
6505
6893
  case "polar-area":
6506
6894
  return buildPolarAreaOption(
@@ -6508,21 +6896,36 @@ function buildSimpleChartOption(parsed, palette, isDark, chartWidth) {
6508
6896
  textColor,
6509
6897
  getSegmentColors(palette, parsed.data.length),
6510
6898
  bg,
6511
- titleConfig,
6512
- tooltipTheme
6899
+ titleConfig
6513
6900
  );
6514
6901
  }
6515
6902
  }
6516
6903
  function makeChartGrid(options) {
6904
+ const left = options.yLabel ? "12%" : "3%";
6517
6905
  return {
6518
- left: options.yLabel ? "12%" : "3%",
6906
+ left,
6519
6907
  right: "4%",
6520
6908
  bottom: options.hasLegend ? "15%" : options.xLabel ? "10%" : "3%",
6521
6909
  top: options.hasTitle ? "15%" : "5%",
6522
6910
  containLabel: true
6523
6911
  };
6524
6912
  }
6525
- function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth) {
6913
+ function wrapLabel(text, maxChars) {
6914
+ const words = text.split(" ");
6915
+ const lines = [];
6916
+ let current = "";
6917
+ for (const word of words) {
6918
+ if (current && current.length + 1 + word.length > maxChars) {
6919
+ lines.push(current);
6920
+ current = word;
6921
+ } else {
6922
+ current = current ? current + " " + word : word;
6923
+ }
6924
+ }
6925
+ if (current) lines.push(current);
6926
+ return lines.join("\n");
6927
+ }
6928
+ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, chartWidth) {
6526
6929
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6527
6930
  const isHorizontal = parsed.orientation === "horizontal";
6528
6931
  const labels = parsed.data.map((d) => d.label);
@@ -6537,7 +6940,11 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
6537
6940
  }
6538
6941
  };
6539
6942
  });
6540
- const hCatGap = isHorizontal && yLabel ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16) : void 0;
6943
+ const catLabels = isHorizontal ? labels.map((l) => wrapLabel(l, 12)) : labels;
6944
+ const maxVisibleLen = Math.max(
6945
+ ...catLabels.map((l) => Math.max(...l.split("\n").map((seg) => seg.length)))
6946
+ );
6947
+ const hCatGap = isHorizontal && yLabel ? Math.max(40, maxVisibleLen * 8 + 16) : void 0;
6541
6948
  const categoryAxis = makeGridAxis(
6542
6949
  "category",
6543
6950
  textColor,
@@ -6545,29 +6952,38 @@ function buildBarOption(parsed, textColor, axisLineColor, splitLineColor, gridOp
6545
6952
  splitLineColor,
6546
6953
  gridOpacity,
6547
6954
  isHorizontal ? yLabel : xLabel,
6548
- labels,
6955
+ catLabels,
6549
6956
  hCatGap,
6550
6957
  !isHorizontal ? chartWidth : void 0
6551
6958
  );
6959
+ const hValueGap = isHorizontal && xLabel ? 40 : void 0;
6552
6960
  const valueAxis = makeGridAxis(
6553
6961
  "value",
6554
6962
  textColor,
6555
6963
  axisLineColor,
6556
6964
  splitLineColor,
6557
6965
  gridOpacity,
6558
- isHorizontal ? xLabel : yLabel
6966
+ isHorizontal ? xLabel : yLabel,
6967
+ void 0,
6968
+ hValueGap
6559
6969
  );
6560
6970
  return {
6561
6971
  ...CHART_BASE,
6562
6972
  title: titleConfig,
6563
- grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6973
+ grid: makeChartGrid({
6974
+ xLabel,
6975
+ yLabel,
6976
+ hasTitle: !!parsed.title,
6977
+ isHorizontal
6978
+ }),
6564
6979
  xAxis: isHorizontal ? valueAxis : categoryAxis,
6565
- yAxis: isHorizontal ? categoryAxis : valueAxis,
6980
+ yAxis: isHorizontal ? { ...categoryAxis, inverse: true } : valueAxis,
6566
6981
  series: [
6567
6982
  {
6568
6983
  type: "bar",
6569
6984
  data,
6570
- emphasis: EMPHASIS_SELF
6985
+ emphasis: EMPHASIS_SELF,
6986
+ blur: BLUR_DIM
6571
6987
  }
6572
6988
  ]
6573
6989
  };
@@ -6584,7 +7000,7 @@ function buildMarkArea(eras, labels, textColor, defaultColor) {
6584
7000
  if (eras.length === 0) return void 0;
6585
7001
  return {
6586
7002
  silent: false,
6587
- tooltip: { show: true },
7003
+ tooltip: { show: false },
6588
7004
  data: eras.map((era) => {
6589
7005
  const startIdx = labels.indexOf(era.start);
6590
7006
  const endIdx = labels.indexOf(era.end);
@@ -6607,7 +7023,7 @@ function buildMarkArea(eras, labels, textColor, defaultColor) {
6607
7023
  })
6608
7024
  };
6609
7025
  }
6610
- function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth) {
7026
+ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, chartWidth) {
6611
7027
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6612
7028
  const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6613
7029
  const labels = parsed.data.map((d) => d.label);
@@ -6618,11 +7034,6 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
6618
7034
  return {
6619
7035
  ...CHART_BASE,
6620
7036
  title: titleConfig,
6621
- tooltip: {
6622
- trigger: "axis",
6623
- ...tooltipTheme,
6624
- axisPointer: { type: "line" }
6625
- },
6626
7037
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6627
7038
  xAxis: makeGridAxis(
6628
7039
  "category",
@@ -6653,12 +7064,13 @@ function buildLineOption(parsed, palette, textColor, axisLineColor, splitLineCol
6653
7064
  lineStyle: { color: lineColor, width: 3 },
6654
7065
  itemStyle: { color: lineColor },
6655
7066
  emphasis: EMPHASIS_LINE,
7067
+ blur: BLUR_DIM,
6656
7068
  ...markArea && { markArea }
6657
7069
  }
6658
7070
  ]
6659
7071
  };
6660
7072
  }
6661
- function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, tooltipTheme, chartWidth) {
7073
+ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, colors, titleConfig, chartWidth) {
6662
7074
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6663
7075
  const seriesNames = parsed.seriesNames ?? [];
6664
7076
  const labels = parsed.data.map((d) => d.label);
@@ -6679,17 +7091,13 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
6679
7091
  lineStyle: { color, width: 3 },
6680
7092
  itemStyle: { color },
6681
7093
  emphasis: EMPHASIS_LINE,
7094
+ blur: BLUR_DIM,
6682
7095
  ...idx === 0 && markArea && { markArea }
6683
7096
  };
6684
7097
  });
6685
7098
  return {
6686
7099
  ...CHART_BASE,
6687
7100
  title: titleConfig,
6688
- tooltip: {
6689
- trigger: "axis",
6690
- ...tooltipTheme,
6691
- axisPointer: { type: "line" }
6692
- },
6693
7101
  legend: {
6694
7102
  data: seriesNames,
6695
7103
  bottom: 10,
@@ -6724,7 +7132,7 @@ function buildMultiLineOption(parsed, palette, textColor, axisLineColor, splitLi
6724
7132
  series
6725
7133
  };
6726
7134
  }
6727
- function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, tooltipTheme, chartWidth) {
7135
+ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineColor, gridOpacity, titleConfig, chartWidth) {
6728
7136
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6729
7137
  const lineColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6730
7138
  const labels = parsed.data.map((d) => d.label);
@@ -6735,11 +7143,6 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
6735
7143
  return {
6736
7144
  ...CHART_BASE,
6737
7145
  title: titleConfig,
6738
- tooltip: {
6739
- trigger: "axis",
6740
- ...tooltipTheme,
6741
- axisPointer: { type: "line" }
6742
- },
6743
7146
  grid: makeChartGrid({ xLabel, yLabel, hasTitle: !!parsed.title }),
6744
7147
  xAxis: makeGridAxis(
6745
7148
  "category",
@@ -6771,6 +7174,7 @@ function buildAreaOption(parsed, palette, textColor, axisLineColor, splitLineCol
6771
7174
  itemStyle: { color: lineColor },
6772
7175
  areaStyle: { opacity: 0.25 },
6773
7176
  emphasis: EMPHASIS_LINE,
7177
+ blur: BLUR_DIM,
6774
7178
  ...markArea && { markArea }
6775
7179
  }
6776
7180
  ]
@@ -6802,7 +7206,7 @@ function pieLabelLayout(parsed) {
6802
7206
  if (maxLen > 18) return { outerRadius: 55, fontSize: 13 };
6803
7207
  return { outerRadius: 70, fontSize: 14 };
6804
7208
  }
6805
- function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme, isDoughnut) {
7209
+ function buildPieOption(parsed, textColor, colors, bg, titleConfig, isDoughnut) {
6806
7210
  const HIDE_AXES = { xAxis: { show: false }, yAxis: { show: false } };
6807
7211
  const data = parsed.data.map((d, i) => {
6808
7212
  const stroke2 = d.color ?? colors[i % colors.length];
@@ -6821,10 +7225,6 @@ function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme
6821
7225
  ...CHART_BASE,
6822
7226
  ...HIDE_AXES,
6823
7227
  title: titleConfig,
6824
- tooltip: {
6825
- trigger: "item",
6826
- ...tooltipTheme
6827
- },
6828
7228
  series: [
6829
7229
  {
6830
7230
  type: "pie",
@@ -6838,12 +7238,13 @@ function buildPieOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme
6838
7238
  fontSize
6839
7239
  },
6840
7240
  labelLine: { show: true },
6841
- emphasis: EMPHASIS_SELF
7241
+ emphasis: EMPHASIS_SELF,
7242
+ blur: BLUR_DIM
6842
7243
  }
6843
7244
  ]
6844
7245
  };
6845
7246
  }
6846
- function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig, tooltipTheme) {
7247
+ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, titleConfig) {
6847
7248
  const bg = isDark ? palette.surface : palette.bg;
6848
7249
  const radarColor = parsed.color ?? parsed.seriesNameColors?.[0] ?? palette.primary;
6849
7250
  const values = parsed.data.map((d) => d.value);
@@ -6857,10 +7258,6 @@ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, title
6857
7258
  title: titleConfig,
6858
7259
  xAxis: { show: false },
6859
7260
  yAxis: { show: false },
6860
- tooltip: {
6861
- trigger: "item",
6862
- ...tooltipTheme
6863
- },
6864
7261
  radar: {
6865
7262
  indicator,
6866
7263
  axisName: {
@@ -6897,12 +7294,13 @@ function buildRadarOption(parsed, palette, isDark, textColor, gridOpacity, title
6897
7294
  }
6898
7295
  }
6899
7296
  ],
6900
- emphasis: EMPHASIS_SELF
7297
+ emphasis: EMPHASIS_SELF,
7298
+ blur: BLUR_DIM
6901
7299
  }
6902
7300
  ]
6903
7301
  };
6904
7302
  }
6905
- function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, tooltipTheme) {
7303
+ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig) {
6906
7304
  const data = parsed.data.map((d, i) => {
6907
7305
  const stroke2 = d.color ?? colors[i % colors.length];
6908
7306
  return {
@@ -6920,10 +7318,6 @@ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, toolti
6920
7318
  title: titleConfig,
6921
7319
  xAxis: { show: false },
6922
7320
  yAxis: { show: false },
6923
- tooltip: {
6924
- trigger: "item",
6925
- ...tooltipTheme
6926
- },
6927
7321
  series: [
6928
7322
  {
6929
7323
  type: "pie",
@@ -6941,12 +7335,13 @@ function buildPolarAreaOption(parsed, textColor, colors, bg, titleConfig, toolti
6941
7335
  fontSize: pieLabelLayout(parsed).fontSize
6942
7336
  },
6943
7337
  labelLine: { show: true },
6944
- emphasis: EMPHASIS_SELF
7338
+ emphasis: EMPHASIS_SELF,
7339
+ blur: BLUR_DIM
6945
7340
  }
6946
7341
  ]
6947
7342
  };
6948
7343
  }
6949
- function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, tooltipTheme, chartWidth) {
7344
+ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor, gridOpacity, colors, bg, titleConfig, chartWidth) {
6950
7345
  const { xLabel, yLabel } = resolveAxisLabels(parsed);
6951
7346
  const isHorizontal = parsed.orientation === "horizontal";
6952
7347
  const seriesNames = parsed.seriesNames ?? [];
@@ -6975,7 +7370,8 @@ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor,
6975
7370
  fontWeight: "bold",
6976
7371
  fontFamily: FONT_FAMILY
6977
7372
  },
6978
- emphasis: EMPHASIS_SELF
7373
+ emphasis: EMPHASIS_SERIES,
7374
+ blur: BLUR_DIM
6979
7375
  };
6980
7376
  });
6981
7377
  const hCatGap = isHorizontal && yLabel ? Math.max(40, Math.max(...labels.map((l) => l.length)) * 8 + 16) : void 0;
@@ -7016,7 +7412,7 @@ function buildBarStackedOption(parsed, textColor, axisLineColor, splitLineColor,
7016
7412
  hasLegend: true
7017
7413
  }),
7018
7414
  xAxis: isHorizontal ? valueAxis : categoryAxis,
7019
- yAxis: isHorizontal ? categoryAxis : valueAxis,
7415
+ yAxis: isHorizontal ? { ...categoryAxis, inverse: true } : valueAxis,
7020
7416
  series
7021
7417
  };
7022
7418
  }
@@ -7100,7 +7496,7 @@ async function renderExtendedChartForExport(content, theme, palette, options) {
7100
7496
  chart.dispose();
7101
7497
  }
7102
7498
  }
7103
- var EMPHASIS_SELF, EMPHASIS_LINE, CHART_BASE, CHART_BORDER_WIDTH, VALID_EXTENDED_TYPES, KNOWN_EXTENDED_OPTIONS, ECHART_EXPORT_WIDTH, ECHART_EXPORT_HEIGHT, STANDARD_CHART_TYPES;
7499
+ var 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;
7104
7500
  var init_echarts = __esm({
7105
7501
  "src/echarts.ts"() {
7106
7502
  "use strict";
@@ -7114,20 +7510,22 @@ var init_echarts = __esm({
7114
7510
  init_colors();
7115
7511
  init_parsing();
7116
7512
  init_chart();
7117
- EMPHASIS_SELF = { focus: "self", blurScope: "global" };
7118
- EMPHASIS_LINE = {
7119
- ...EMPHASIS_SELF,
7120
- scale: 2.5,
7121
- itemStyle: {
7122
- borderWidth: 2,
7123
- borderColor: "#fff",
7124
- shadowBlur: 8,
7125
- shadowColor: "rgba(0,0,0,0.4)"
7126
- }
7513
+ EMPHASIS_SELF = {
7514
+ focus: "self",
7515
+ blurScope: "global",
7516
+ itemStyle: { opacity: 1 }
7517
+ };
7518
+ EMPHASIS_SERIES = {
7519
+ focus: "series",
7520
+ blurScope: "global",
7521
+ itemStyle: { opacity: 1 }
7127
7522
  };
7523
+ BLUR_DIM = { itemStyle: { opacity: 0.15 }, lineStyle: { opacity: 0.15 } };
7524
+ EMPHASIS_LINE = { ...EMPHASIS_SELF };
7128
7525
  CHART_BASE = {
7129
7526
  backgroundColor: "transparent",
7130
- animation: false
7527
+ animation: false,
7528
+ tooltip: { show: false }
7131
7529
  };
7132
7530
  CHART_BORDER_WIDTH = 2;
7133
7531
  VALID_EXTENDED_TYPES = /* @__PURE__ */ new Set([
@@ -8459,7 +8857,12 @@ __export(parser_exports7, {
8459
8857
  function parseArrowLine(trimmed, palette) {
8460
8858
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8461
8859
  if (bareMatch) {
8462
- return { target: bareMatch[1].trim() };
8860
+ const rawTarget = bareMatch[1].trim();
8861
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8862
+ return {
8863
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8864
+ targetIsGroup: !!groupMatch
8865
+ };
8463
8866
  }
8464
8867
  const arrowMatch = trimmed.match(ARROW_RE);
8465
8868
  if (arrowMatch) {
@@ -8468,8 +8871,14 @@ function parseArrowLine(trimmed, palette) {
8468
8871
  if (label && !color) {
8469
8872
  color = inferArrowColor(label);
8470
8873
  }
8471
- const target = arrowMatch[3].trim();
8472
- return { label, color, target };
8874
+ const rawTarget = arrowMatch[3].trim();
8875
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
8876
+ return {
8877
+ label,
8878
+ color,
8879
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
8880
+ targetIsGroup: !!groupMatch
8881
+ };
8473
8882
  }
8474
8883
  return null;
8475
8884
  }
@@ -8531,6 +8940,7 @@ function parseSitemap(content, palette) {
8531
8940
  const aliasMap = /* @__PURE__ */ new Map();
8532
8941
  const indentStack = [];
8533
8942
  const labelToNode = /* @__PURE__ */ new Map();
8943
+ const labelToContainer = /* @__PURE__ */ new Map();
8534
8944
  const deferredArrows = [];
8535
8945
  for (let i = 0; i < lines.length; i++) {
8536
8946
  const line10 = lines[i];
@@ -8632,6 +9042,7 @@ function parseSitemap(content, palette) {
8632
9042
  deferredArrows.push({
8633
9043
  sourceNode: source,
8634
9044
  targetLabel: arrowInfo.target,
9045
+ targetIsGroup: arrowInfo.targetIsGroup,
8635
9046
  label: arrowInfo.label,
8636
9047
  color: arrowInfo.color,
8637
9048
  lineNumber
@@ -8665,6 +9076,7 @@ function parseSitemap(content, palette) {
8665
9076
  color
8666
9077
  };
8667
9078
  attachNode2(node, indent, indentStack, result);
9079
+ labelToContainer.set(label.toLowerCase(), node);
8668
9080
  } else if (metadataMatch && indentStack.length > 0) {
8669
9081
  const rawKey = metadataMatch[1].trim().toLowerCase();
8670
9082
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8705,22 +9117,41 @@ function parseSitemap(content, palette) {
8705
9117
  }
8706
9118
  for (const arrow of deferredArrows) {
8707
9119
  const targetKey = arrow.targetLabel.toLowerCase();
8708
- const targetNode = labelToNode.get(targetKey);
8709
- if (!targetNode) {
8710
- const allLabels = Array.from(labelToNode.keys());
8711
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8712
- const hint = suggest(targetKey, allLabels);
8713
- if (hint) msg += `. ${hint}`;
8714
- pushError(arrow.lineNumber, msg);
8715
- continue;
9120
+ if (arrow.targetIsGroup) {
9121
+ const targetContainer = labelToContainer.get(targetKey);
9122
+ if (!targetContainer) {
9123
+ const allLabels = Array.from(labelToContainer.keys());
9124
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9125
+ const hint = suggest(targetKey, allLabels);
9126
+ if (hint) msg += `. ${hint}`;
9127
+ pushError(arrow.lineNumber, msg);
9128
+ continue;
9129
+ }
9130
+ result.edges.push({
9131
+ sourceId: arrow.sourceNode.id,
9132
+ targetId: targetContainer.id,
9133
+ label: arrow.label,
9134
+ color: arrow.color,
9135
+ lineNumber: arrow.lineNumber
9136
+ });
9137
+ } else {
9138
+ const targetNode = labelToNode.get(targetKey);
9139
+ if (!targetNode) {
9140
+ const allLabels = Array.from(labelToNode.keys());
9141
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9142
+ const hint = suggest(targetKey, allLabels);
9143
+ if (hint) msg += `. ${hint}`;
9144
+ pushError(arrow.lineNumber, msg);
9145
+ continue;
9146
+ }
9147
+ result.edges.push({
9148
+ sourceId: arrow.sourceNode.id,
9149
+ targetId: targetNode.id,
9150
+ label: arrow.label,
9151
+ color: arrow.color,
9152
+ lineNumber: arrow.lineNumber
9153
+ });
8716
9154
  }
8717
- result.edges.push({
8718
- sourceId: arrow.sourceNode.id,
8719
- targetId: targetNode.id,
8720
- label: arrow.label,
8721
- color: arrow.color,
8722
- lineNumber: arrow.lineNumber
8723
- });
8724
9155
  }
8725
9156
  if (result.tagGroups.length > 0) {
8726
9157
  const allNodes = [];
@@ -10434,6 +10865,7 @@ function parseBoxesAndLines(content) {
10434
10865
  const nodeLabels = /* @__PURE__ */ new Set();
10435
10866
  const groupLabels = /* @__PURE__ */ new Set();
10436
10867
  let lastNodeLabel = null;
10868
+ let lastSourceIsGroup = false;
10437
10869
  const groupStack = [];
10438
10870
  let contentStarted = false;
10439
10871
  let currentTagGroup = null;
@@ -10672,6 +11104,8 @@ function parseBoxesAndLines(content) {
10672
11104
  };
10673
11105
  groupLabels.add(label);
10674
11106
  groupStack.push({ group, indent, depth: currentDepth });
11107
+ lastNodeLabel = label;
11108
+ lastSourceIsGroup = true;
10675
11109
  continue;
10676
11110
  }
10677
11111
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10689,7 +11123,8 @@ function parseBoxesAndLines(content) {
10689
11123
  );
10690
11124
  continue;
10691
11125
  }
10692
- edgeText = `${lastNodeLabel} ${trimmed}`;
11126
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11127
+ edgeText = `${sourcePrefix} ${trimmed}`;
10693
11128
  }
10694
11129
  const edge = parseEdgeLine(
10695
11130
  edgeText,
@@ -10712,6 +11147,7 @@ function parseBoxesAndLines(content) {
10712
11147
  continue;
10713
11148
  }
10714
11149
  lastNodeLabel = node.label;
11150
+ lastSourceIsGroup = false;
10715
11151
  const gs = currentGroupState();
10716
11152
  const isGroupChild = gs && indent > gs.indent;
10717
11153
  if (nodeLabels.has(node.label)) {
@@ -10739,14 +11175,42 @@ function parseBoxesAndLines(content) {
10739
11175
  const gs = groupStack.pop();
10740
11176
  result.groups.push(gs.group);
10741
11177
  }
11178
+ const validEdges = [];
10742
11179
  for (const edge of result.edges) {
10743
- if (!edge.source.startsWith("__group_")) {
11180
+ let valid = true;
11181
+ if (edge.source.startsWith("__group_")) {
11182
+ const label = edge.source.slice("__group_".length);
11183
+ const found = [...groupLabels].some(
11184
+ (g) => g.toLowerCase() === label.toLowerCase()
11185
+ );
11186
+ if (!found) {
11187
+ result.diagnostics.push(
11188
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11189
+ );
11190
+ valid = false;
11191
+ }
11192
+ } else {
10744
11193
  ensureNode(edge.source, edge.lineNumber);
10745
11194
  }
10746
- if (!edge.target.startsWith("__group_")) {
11195
+ if (edge.target.startsWith("__group_")) {
11196
+ const label = edge.target.slice("__group_".length);
11197
+ const found = [...groupLabels].some(
11198
+ (g) => g.toLowerCase() === label.toLowerCase()
11199
+ );
11200
+ if (!found) {
11201
+ result.diagnostics.push(
11202
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11203
+ );
11204
+ valid = false;
11205
+ }
11206
+ } else {
10747
11207
  ensureNode(edge.target, edge.lineNumber);
10748
11208
  }
11209
+ if (valid) {
11210
+ validEdges.push(edge);
11211
+ }
10749
11212
  }
11213
+ result.edges = validEdges;
10750
11214
  if (result.tagGroups.length > 0) {
10751
11215
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10752
11216
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10775,10 +11239,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10775
11239
  description
10776
11240
  };
10777
11241
  }
11242
+ function resolveEndpoint(name) {
11243
+ const m = name.match(/^\[(.+)\]$/);
11244
+ return m ? groupId2(m[1].trim()) : name;
11245
+ }
10778
11246
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10779
11247
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10780
11248
  if (biLabeledMatch) {
10781
- const source2 = biLabeledMatch[1].trim();
11249
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10782
11250
  const label = biLabeledMatch[2].trim();
10783
11251
  let rest2 = biLabeledMatch[3].trim();
10784
11252
  let metadata2 = {};
@@ -10799,7 +11267,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10799
11267
  }
10800
11268
  return {
10801
11269
  source: source2,
10802
- target: rest2,
11270
+ target: resolveEndpoint(rest2),
10803
11271
  label: label || void 0,
10804
11272
  bidirectional: true,
10805
11273
  lineNumber: lineNum,
@@ -10808,7 +11276,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10808
11276
  }
10809
11277
  const biIdx = trimmed.indexOf("<->");
10810
11278
  if (biIdx >= 0) {
10811
- const source2 = trimmed.slice(0, biIdx).trim();
11279
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10812
11280
  let rest2 = trimmed.slice(biIdx + 3).trim();
10813
11281
  let metadata2 = {};
10814
11282
  const pipeIdx2 = rest2.indexOf("|");
@@ -10828,7 +11296,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10828
11296
  }
10829
11297
  return {
10830
11298
  source: source2,
10831
- target: rest2,
11299
+ target: resolveEndpoint(rest2),
10832
11300
  bidirectional: true,
10833
11301
  lineNumber: lineNum,
10834
11302
  metadata: metadata2
@@ -10836,7 +11304,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10836
11304
  }
10837
11305
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10838
11306
  if (labeledMatch) {
10839
- const source2 = labeledMatch[1].trim();
11307
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10840
11308
  const label = labeledMatch[2].trim();
10841
11309
  let rest2 = labeledMatch[3].trim();
10842
11310
  if (label) {
@@ -10858,7 +11326,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10858
11326
  }
10859
11327
  return {
10860
11328
  source: source2,
10861
- target: rest2,
11329
+ target: resolveEndpoint(rest2),
10862
11330
  label,
10863
11331
  bidirectional: false,
10864
11332
  lineNumber: lineNum,
@@ -10868,7 +11336,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10868
11336
  }
10869
11337
  const arrowIdx = trimmed.indexOf("->");
10870
11338
  if (arrowIdx < 0) return null;
10871
- const source = trimmed.slice(0, arrowIdx).trim();
11339
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10872
11340
  let rest = trimmed.slice(arrowIdx + 2).trim();
10873
11341
  if (!source || !rest) {
10874
11342
  diagnostics.push(
@@ -10889,7 +11357,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10889
11357
  }
10890
11358
  return {
10891
11359
  source,
10892
- target: rest,
11360
+ target: resolveEndpoint(rest),
10893
11361
  bidirectional: false,
10894
11362
  lineNumber: lineNum,
10895
11363
  metadata
@@ -11204,14 +11672,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11204
11672
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11205
11673
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11206
11674
  if (visibleEntries.length === 0) continue;
11207
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11208
- const minPillWidth = pillWidth2;
11209
- let entriesWidth2 = 0;
11675
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11676
+ const minPillWidth = pillWidth3;
11677
+ let entriesWidth3 = 0;
11210
11678
  for (const entry of visibleEntries) {
11211
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11679
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11212
11680
  }
11213
11681
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11214
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11682
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11215
11683
  groups.push({
11216
11684
  name: group.name,
11217
11685
  alias: group.alias,
@@ -11221,7 +11689,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11221
11689
  })),
11222
11690
  x: 0,
11223
11691
  y: 0,
11224
- width: capsuleWidth,
11692
+ width: capsuleWidth2,
11225
11693
  height: LEGEND_HEIGHT2,
11226
11694
  minifiedWidth: minPillWidth,
11227
11695
  minifiedHeight: LEGEND_HEIGHT2
@@ -12162,66 +12630,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12162
12630
  }
12163
12631
  }
12164
12632
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12165
- const visibleGroups = layout.legend.filter((group) => {
12166
- if (legendOnly) return true;
12167
- if (activeTagGroup == null) return true;
12168
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12169
- });
12170
- let fixedPositions;
12171
- if (fixedLegend && visibleGroups.length > 0) {
12172
- fixedPositions = /* @__PURE__ */ new Map();
12173
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12174
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12175
- let cx = (width - totalW) / 2;
12176
- for (const g of visibleGroups) {
12177
- fixedPositions.set(g.name, cx);
12178
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12179
- }
12180
- }
12181
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12182
- const legendParent = legendParentBase;
12183
- if (fixedLegend && activeTagGroup) {
12184
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12633
+ const groups = layout.legend.map((g) => ({
12634
+ name: g.name,
12635
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12636
+ }));
12637
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12638
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12639
+ let legendHandle;
12640
+ if (legendOnly) {
12641
+ for (const lg of layout.legend) {
12642
+ const singleConfig = {
12643
+ groups: [
12644
+ {
12645
+ name: lg.name,
12646
+ entries: lg.entries.map((e) => ({
12647
+ value: e.value,
12648
+ color: e.color
12649
+ }))
12650
+ }
12651
+ ],
12652
+ position: { placement: "top-center", titleRelation: "below-title" },
12653
+ mode: "fixed"
12654
+ };
12655
+ const singleState = { activeGroup: lg.name };
12656
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12657
+ renderLegendD3(
12658
+ groupG,
12659
+ singleConfig,
12660
+ singleState,
12661
+ palette,
12662
+ isDark,
12663
+ void 0,
12664
+ lg.width
12665
+ );
12666
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12667
+ }
12668
+ legendHandle = null;
12669
+ } else {
12670
+ const legendConfig = {
12671
+ groups,
12672
+ position: { placement: "top-center", titleRelation: "below-title" },
12673
+ mode: "fixed",
12674
+ capsulePillAddonWidth: eyeAddonWidth
12675
+ };
12676
+ const legendState = { activeGroup: activeTagGroup ?? null };
12677
+ legendHandle = renderLegendD3(
12678
+ legendParentBase,
12679
+ legendConfig,
12680
+ legendState,
12681
+ palette,
12682
+ isDark,
12683
+ void 0,
12684
+ fixedLegend ? width : layout.width
12685
+ );
12686
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12185
12687
  }
12186
- for (const group of visibleGroups) {
12187
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12188
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12189
- const pillLabel = group.name;
12190
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12191
- const gX = fixedPositions?.get(group.name) ?? group.x;
12192
- const gY = fixedPositions ? 0 : group.y;
12193
- 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");
12194
- if (isActive) {
12195
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12196
- }
12197
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12198
- const pillYOff = LEGEND_CAPSULE_PAD;
12199
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12200
- 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);
12201
- if (isActive) {
12202
- 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);
12203
- }
12204
- 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);
12205
- if (isActive && fixedLegend) {
12206
- const groupKey = group.name.toLowerCase();
12688
+ if (fixedLegend && legendHandle) {
12689
+ const computedLayout = legendHandle.getLayout();
12690
+ if (computedLayout.activeCapsule?.addonX != null) {
12691
+ const capsule = computedLayout.activeCapsule;
12692
+ const groupKey = capsule.groupName.toLowerCase();
12207
12693
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12208
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12209
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12210
- const hitPad = 6;
12211
- 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);
12212
- 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");
12213
- 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");
12214
- }
12215
- if (isActive) {
12216
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12217
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12218
- for (const entry of group.entries) {
12219
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12220
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12221
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12222
- const entryLabel = entry.value;
12223
- 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);
12224
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12694
+ const activeGroupEl = legendParentBase.select(
12695
+ `[data-legend-group="${groupKey}"]`
12696
+ );
12697
+ if (!activeGroupEl.empty()) {
12698
+ const eyeX = capsule.addonX;
12699
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12700
+ const hitPad = 6;
12701
+ const eyeG = activeGroupEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12702
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12703
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12225
12704
  }
12226
12705
  }
12227
12706
  }
@@ -12255,6 +12734,7 @@ var init_renderer = __esm({
12255
12734
  init_parser4();
12256
12735
  init_layout();
12257
12736
  init_legend_constants();
12737
+ init_legend_d3();
12258
12738
  init_title_constants();
12259
12739
  DIAGRAM_PADDING = 20;
12260
12740
  MAX_SCALE = 3;
@@ -12284,6 +12764,17 @@ __export(layout_exports2, {
12284
12764
  layoutSitemap: () => layoutSitemap
12285
12765
  });
12286
12766
  import dagre from "@dagrejs/dagre";
12767
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12768
+ const dx = tx - cx;
12769
+ const dy = ty - cy;
12770
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12771
+ const hw = w / 2;
12772
+ const hh = h / 2;
12773
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12774
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12775
+ const s = Math.min(sx, sy);
12776
+ return { x: cx + dx * s, y: cy + dy * s };
12777
+ }
12287
12778
  function filterMetadata2(metadata, hiddenAttributes) {
12288
12779
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12289
12780
  const filtered = {};
@@ -12300,7 +12791,10 @@ function computeCardWidth2(label, meta) {
12300
12791
  const lineChars = key.length + 2 + value.length;
12301
12792
  if (lineChars > maxChars) maxChars = lineChars;
12302
12793
  }
12303
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12794
+ return Math.max(
12795
+ MIN_CARD_WIDTH2,
12796
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
12797
+ );
12304
12798
  }
12305
12799
  function computeCardHeight2(meta) {
12306
12800
  const metaCount = Object.keys(meta).length;
@@ -12309,7 +12803,12 @@ function computeCardHeight2(meta) {
12309
12803
  }
12310
12804
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12311
12805
  if (node.color) return node.color;
12312
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
12806
+ return resolveTagColor(
12807
+ node.metadata,
12808
+ tagGroups,
12809
+ activeGroupName,
12810
+ node.isContainer
12811
+ );
12313
12812
  }
12314
12813
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12315
12814
  const groups = [];
@@ -12318,21 +12817,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12318
12817
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12319
12818
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12320
12819
  if (visibleEntries.length === 0) continue;
12321
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12322
- const minPillWidth = pillWidth2;
12323
- let entriesWidth2 = 0;
12820
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12821
+ const minPillWidth = pillWidth3;
12822
+ let entriesWidth3 = 0;
12324
12823
  for (const entry of visibleEntries) {
12325
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12824
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12326
12825
  }
12327
12826
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12328
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
12827
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12329
12828
  groups.push({
12330
12829
  name: group.name,
12331
12830
  alias: group.alias,
12332
12831
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12333
12832
  x: 0,
12334
12833
  y: 0,
12335
- width: capsuleWidth,
12834
+ width: capsuleWidth2,
12336
12835
  height: LEGEND_HEIGHT3,
12337
12836
  minifiedWidth: minPillWidth,
12338
12837
  minifiedHeight: LEGEND_HEIGHT3
@@ -12352,10 +12851,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12352
12851
  parentPageId,
12353
12852
  meta,
12354
12853
  fullMeta: { ...node.metadata },
12355
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
12854
+ width: Math.max(
12855
+ MIN_CARD_WIDTH2,
12856
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
12857
+ ),
12356
12858
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12357
12859
  });
12358
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
12860
+ flattenNodes(
12861
+ node.children,
12862
+ node.id,
12863
+ parentPageId,
12864
+ hiddenCounts,
12865
+ hiddenAttributes,
12866
+ result
12867
+ );
12359
12868
  } else {
12360
12869
  result.push({
12361
12870
  sitemapNode: node,
@@ -12367,14 +12876,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12367
12876
  height: computeCardHeight2(meta)
12368
12877
  });
12369
12878
  if (node.children.length > 0) {
12370
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
12879
+ flattenNodes(
12880
+ node.children,
12881
+ parentContainerId,
12882
+ node.id,
12883
+ hiddenCounts,
12884
+ hiddenAttributes,
12885
+ result
12886
+ );
12371
12887
  }
12372
12888
  }
12373
12889
  }
12374
12890
  }
12375
12891
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12376
12892
  if (parsed.roots.length === 0) {
12377
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
12893
+ return {
12894
+ nodes: [],
12895
+ edges: [],
12896
+ containers: [],
12897
+ legend: [],
12898
+ width: 0,
12899
+ height: 0
12900
+ };
12378
12901
  }
12379
12902
  const allNodes = [];
12380
12903
  const collect = (node) => {
@@ -12382,9 +12905,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12382
12905
  for (const child of node.children) collect(child);
12383
12906
  };
12384
12907
  for (const root of parsed.roots) collect(root);
12385
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
12908
+ injectDefaultTagMetadata(
12909
+ allNodes,
12910
+ parsed.tagGroups,
12911
+ (e) => e.isContainer
12912
+ );
12386
12913
  const flatNodes = [];
12387
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
12914
+ flattenNodes(
12915
+ parsed.roots,
12916
+ null,
12917
+ null,
12918
+ hiddenCounts,
12919
+ hiddenAttributes,
12920
+ flatNodes
12921
+ );
12388
12922
  const nodeMap = /* @__PURE__ */ new Map();
12389
12923
  for (const flat of flatNodes) {
12390
12924
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12446,14 +12980,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12446
12980
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12447
12981
  }
12448
12982
  }
12983
+ const expandedContainerIds = /* @__PURE__ */ new Set();
12984
+ for (const cid of containerIds) {
12985
+ if (!collapsedContainerIds.has(cid)) {
12986
+ expandedContainerIds.add(cid);
12987
+ }
12988
+ }
12989
+ const deferredEdgeIndices = [];
12449
12990
  for (let i = 0; i < parsed.edges.length; i++) {
12450
12991
  const edge = parsed.edges[i];
12451
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12452
- g.setEdge(edge.sourceId, edge.targetId, {
12992
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12993
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
12994
+ deferredEdgeIndices.push(i);
12995
+ continue;
12996
+ }
12997
+ g.setEdge(
12998
+ edge.sourceId,
12999
+ edge.targetId,
13000
+ {
12453
13001
  label: edge.label ?? "",
12454
13002
  minlen: 1
12455
- }, `e${i}`);
12456
- }
13003
+ },
13004
+ `e${i}`
13005
+ );
12457
13006
  }
12458
13007
  dagre.layout(g);
12459
13008
  const layoutNodes = [];
@@ -12521,19 +13070,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12521
13070
  });
12522
13071
  }
12523
13072
  }
13073
+ const deferredSet = new Set(deferredEdgeIndices);
12524
13074
  const layoutEdges = [];
12525
13075
  for (let i = 0; i < parsed.edges.length; i++) {
12526
13076
  const edge = parsed.edges[i];
12527
13077
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12528
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12529
- if (!edgeData) continue;
13078
+ let points;
13079
+ if (deferredSet.has(i)) {
13080
+ const srcNode = g.node(edge.sourceId);
13081
+ const tgtNode = g.node(edge.targetId);
13082
+ if (!srcNode || !tgtNode) continue;
13083
+ const srcPt = clipToRectBorder(
13084
+ srcNode.x,
13085
+ srcNode.y,
13086
+ srcNode.width,
13087
+ srcNode.height,
13088
+ tgtNode.x,
13089
+ tgtNode.y
13090
+ );
13091
+ const tgtPt = clipToRectBorder(
13092
+ tgtNode.x,
13093
+ tgtNode.y,
13094
+ tgtNode.width,
13095
+ tgtNode.height,
13096
+ srcNode.x,
13097
+ srcNode.y
13098
+ );
13099
+ const midX = (srcPt.x + tgtPt.x) / 2;
13100
+ const midY = (srcPt.y + tgtPt.y) / 2;
13101
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13102
+ } else {
13103
+ const edgeData = g.edge({
13104
+ v: edge.sourceId,
13105
+ w: edge.targetId,
13106
+ name: `e${i}`
13107
+ });
13108
+ if (!edgeData) continue;
13109
+ points = edgeData.points ?? [];
13110
+ }
12530
13111
  layoutEdges.push({
12531
13112
  sourceId: edge.sourceId,
12532
13113
  targetId: edge.targetId,
12533
- points: edgeData.points ?? [],
13114
+ points,
12534
13115
  label: edge.label,
12535
13116
  color: edge.color,
12536
- lineNumber: edge.lineNumber
13117
+ lineNumber: edge.lineNumber,
13118
+ deferred: deferredSet.has(i) || void 0
12537
13119
  });
12538
13120
  }
12539
13121
  {
@@ -12694,7 +13276,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12694
13276
  usedValuesByGroup.set(key, used);
12695
13277
  }
12696
13278
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12697
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13279
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13280
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13281
+ ) : legendGroups;
12698
13282
  const allExpanded = expandAllLegend && activeTagGroup == null;
12699
13283
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12700
13284
  if (visibleGroups.length > 0) {
@@ -13010,7 +13594,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13010
13594
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
13011
13595
  const edgeColor3 = edge.color ?? palette.textMuted;
13012
13596
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
13013
- const pathD = lineGenerator(edge.points);
13597
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13598
+ const pathD = gen(edge.points);
13014
13599
  if (pathD) {
13015
13600
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor3).attr("stroke-width", EDGE_STROKE_WIDTH2).attr("marker-end", `url(#${markerId})`).attr("class", "sitemap-edge");
13016
13601
  }
@@ -13101,57 +13686,44 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13101
13686
  }
13102
13687
  function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13103
13688
  if (legendGroups.length === 0) return;
13104
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13105
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13106
- ) : legendGroups;
13107
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13108
- let fixedPositions;
13109
- if (fixedWidth != null && visibleGroups.length > 0) {
13110
- fixedPositions = /* @__PURE__ */ new Map();
13111
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13112
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13113
- let cx = (fixedWidth - totalW) / 2;
13114
- for (const g of visibleGroups) {
13115
- fixedPositions.set(g.name, cx);
13116
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13117
- }
13118
- }
13119
- for (const group of visibleGroups) {
13120
- const isActive = activeTagGroup != null;
13121
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13122
- const gX = fixedPositions?.get(group.name) ?? group.x;
13123
- const gY = fixedPositions ? 0 : group.y;
13124
- 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");
13125
- if (isActive) {
13126
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13127
- }
13128
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13129
- const pillYOff = LEGEND_CAPSULE_PAD;
13130
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13131
- 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);
13132
- if (isActive) {
13133
- 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);
13134
- }
13135
- 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);
13136
- if (isActive && fixedWidth != null) {
13137
- const groupKey = group.name.toLowerCase();
13689
+ const groups = legendGroups.map((g) => ({
13690
+ name: g.name,
13691
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13692
+ }));
13693
+ const isFixedMode = fixedWidth != null;
13694
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13695
+ const legendConfig = {
13696
+ groups,
13697
+ position: { placement: "top-center", titleRelation: "below-title" },
13698
+ mode: "fixed",
13699
+ capsulePillAddonWidth: eyeAddonWidth
13700
+ };
13701
+ const legendState = { activeGroup: activeTagGroup ?? null };
13702
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13703
+ const legendHandle = renderLegendD3(
13704
+ parent,
13705
+ legendConfig,
13706
+ legendState,
13707
+ palette,
13708
+ isDark,
13709
+ void 0,
13710
+ containerWidth
13711
+ );
13712
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13713
+ if (isFixedMode) {
13714
+ const computedLayout = legendHandle.getLayout();
13715
+ if (computedLayout.activeCapsule?.addonX != null) {
13716
+ const capsule = computedLayout.activeCapsule;
13717
+ const groupKey = capsule.groupName.toLowerCase();
13138
13718
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13139
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13140
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13141
- const hitPad = 6;
13142
- 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);
13143
- 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");
13144
- 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");
13145
- }
13146
- if (isActive) {
13147
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13148
- let entryX = pillXOff + pillW + 4 + eyeShift;
13149
- for (const entry of group.entries) {
13150
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13151
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13152
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13153
- 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);
13154
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13719
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13720
+ if (!activeGroupEl.empty()) {
13721
+ const eyeX = capsule.addonX;
13722
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13723
+ const hitPad = 6;
13724
+ const eyeG = activeGroupEl.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13725
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13726
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13155
13727
  }
13156
13728
  }
13157
13729
  }
@@ -13205,13 +13777,14 @@ async function renderSitemapForExport(content, theme, palette) {
13205
13777
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13206
13778
  return injectBranding2(svgHtml, brandColor);
13207
13779
  }
13208
- var DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
13780
+ var DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator, lineGeneratorLinear;
13209
13781
  var init_renderer2 = __esm({
13210
13782
  "src/sitemap/renderer.ts"() {
13211
13783
  "use strict";
13212
13784
  init_fonts();
13213
13785
  init_color_utils();
13214
13786
  init_legend_constants();
13787
+ init_legend_d3();
13215
13788
  init_title_constants();
13216
13789
  DIAGRAM_PADDING2 = 20;
13217
13790
  MAX_SCALE2 = 3;
@@ -13235,6 +13808,7 @@ var init_renderer2 = __esm({
13235
13808
  COLLAPSE_BAR_HEIGHT2 = 6;
13236
13809
  LEGEND_FIXED_GAP2 = 8;
13237
13810
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
13811
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13238
13812
  }
13239
13813
  });
13240
13814
 
@@ -13512,53 +14086,22 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13512
14086
  }
13513
14087
  if (parsed.tagGroups.length > 0) {
13514
14088
  const legendY = height - LEGEND_HEIGHT;
13515
- let legendX = DIAGRAM_PADDING3;
13516
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13517
- const capsulePad = LEGEND_CAPSULE_PAD;
13518
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13519
- if (activeTagGroup) {
13520
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13521
- }
13522
- for (const group of parsed.tagGroups) {
13523
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13524
- if (activeTagGroup != null && !isActive) continue;
13525
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13526
- const pillWidth2 = pillTextWidth + 16;
13527
- let capsuleContentWidth = pillWidth2;
13528
- if (isActive) {
13529
- capsuleContentWidth += 4;
13530
- for (const entry of group.entries) {
13531
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13532
- }
13533
- }
13534
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13535
- if (isActive) {
13536
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13537
- }
13538
- const pillX = legendX + (isActive ? capsulePad : 0);
13539
- const pillBg = isActive ? palette.bg : groupBg;
13540
- 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());
13541
- if (isActive) {
13542
- 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);
13543
- }
13544
- 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);
13545
- if (isActive) {
13546
- let entryX = pillX + pillWidth2 + 4;
13547
- for (const entry of group.entries) {
13548
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13549
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13550
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13551
- entryG.append("text").attr("x", entryTextX).attr(
13552
- "y",
13553
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13554
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13555
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13556
- }
13557
- legendX += capsuleWidth + 12;
13558
- } else {
13559
- legendX += pillWidth2 + 12;
13560
- }
13561
- }
14089
+ const legendConfig = {
14090
+ groups: parsed.tagGroups,
14091
+ position: { placement: "top-center", titleRelation: "below-title" },
14092
+ mode: exportDims ? "inline" : "fixed"
14093
+ };
14094
+ const legendState = { activeGroup: activeTagGroup ?? null };
14095
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${DIAGRAM_PADDING3},${legendY})`);
14096
+ renderLegendD3(
14097
+ legendG,
14098
+ legendConfig,
14099
+ legendState,
14100
+ palette,
14101
+ isDark,
14102
+ void 0,
14103
+ width - DIAGRAM_PADDING3 * 2
14104
+ );
13562
14105
  }
13563
14106
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13564
14107
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13653,6 +14196,7 @@ var init_renderer3 = __esm({
13653
14196
  init_parser5();
13654
14197
  init_mutations();
13655
14198
  init_legend_constants();
14199
+ init_legend_d3();
13656
14200
  init_title_constants();
13657
14201
  DIAGRAM_PADDING3 = 20;
13658
14202
  COLUMN_GAP = 16;
@@ -13848,14 +14392,9 @@ function collectClassTypes(parsed) {
13848
14392
  if (c.color) continue;
13849
14393
  present.add(c.modifier ?? "class");
13850
14394
  }
13851
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13852
- }
13853
- function legendEntriesWidth(entries) {
13854
- let w = 0;
13855
- for (const e of entries) {
13856
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13857
- }
13858
- return w;
14395
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14396
+ (k) => CLASS_TYPE_MAP[k]
14397
+ );
13859
14398
  }
13860
14399
  function classTypeKey(modifier) {
13861
14400
  return modifier ?? "class";
@@ -13924,7 +14463,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13924
14463
  defs.append("marker").attr("id", "cd-arrow-depend").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13925
14464
  defs.append("marker").attr("id", "cd-arrow-assoc").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13926
14465
  if (parsed.title) {
13927
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
14466
+ const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style(
14467
+ "cursor",
14468
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14469
+ ).text(parsed.title);
13928
14470
  if (parsed.titleLineNumber) {
13929
14471
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13930
14472
  if (onClickItem) {
@@ -13938,32 +14480,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13938
14480
  }
13939
14481
  const isLegendExpanded = legendActive !== false;
13940
14482
  if (hasLegend) {
13941
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13942
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13943
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13944
- const entriesW = legendEntriesWidth(legendEntries);
13945
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13946
- const legendX = (width - totalW) / 2;
13947
- const legendY = titleHeight;
13948
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13949
- if (isLegendExpanded) {
13950
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13951
- 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);
13952
- 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);
13953
- 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);
13954
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13955
- for (const entry of legendEntries) {
13956
- const color = palette.colors[entry.colorKey];
13957
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13958
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13959
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13960
- 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);
13961
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14483
+ const legendGroups = [
14484
+ {
14485
+ name: LEGEND_GROUP_NAME,
14486
+ entries: legendEntries.map((entry) => ({
14487
+ value: entry.label,
14488
+ color: palette.colors[entry.colorKey]
14489
+ }))
13962
14490
  }
13963
- } else {
13964
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13965
- 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);
13966
- }
14491
+ ];
14492
+ const legendConfig = {
14493
+ groups: legendGroups,
14494
+ position: { placement: "top-center", titleRelation: "below-title" },
14495
+ mode: "fixed"
14496
+ };
14497
+ const legendState = {
14498
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14499
+ };
14500
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14501
+ renderLegendD3(
14502
+ legendG,
14503
+ legendConfig,
14504
+ legendState,
14505
+ palette,
14506
+ isDark,
14507
+ void 0,
14508
+ width
14509
+ );
13967
14510
  }
13968
14511
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13969
14512
  for (const edge of layout.edges) {
@@ -14007,7 +14550,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
14007
14550
  const colorOff = !!parsed.options?.["no-auto-color"];
14008
14551
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
14009
14552
  const effectiveColor = neutralize ? palette.primary : node.color;
14010
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14553
+ const fill2 = nodeFill3(
14554
+ palette,
14555
+ isDark,
14556
+ node.modifier,
14557
+ effectiveColor,
14558
+ colorOff
14559
+ );
14011
14560
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
14012
14561
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", 3).attr("ry", 3).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", NODE_STROKE_WIDTH3);
14013
14562
  let yPos = -h / 2;
@@ -14076,15 +14625,10 @@ function renderClassDiagramForExport(content, theme, palette) {
14076
14625
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
14077
14626
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
14078
14627
  return runInExportContainer(exportWidth, exportHeight, (container) => {
14079
- renderClassDiagram(
14080
- container,
14081
- parsed,
14082
- layout,
14083
- palette,
14084
- isDark,
14085
- void 0,
14086
- { width: exportWidth, height: exportHeight }
14087
- );
14628
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14629
+ width: exportWidth,
14630
+ height: exportHeight
14631
+ });
14088
14632
  return extractExportSvg(container, theme);
14089
14633
  });
14090
14634
  }
@@ -14095,6 +14639,7 @@ var init_renderer4 = __esm({
14095
14639
  init_fonts();
14096
14640
  init_export_container();
14097
14641
  init_legend_constants();
14642
+ init_legend_d3();
14098
14643
  init_title_constants();
14099
14644
  init_color_utils();
14100
14645
  init_parser2();
@@ -14703,35 +15248,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14703
15248
  }
14704
15249
  }
14705
15250
  if (parsed.tagGroups.length > 0) {
14706
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14707
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14708
- const LEGEND_GAP = 8;
14709
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14710
- if (activeTagGroup) {
14711
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14712
- }
14713
- let legendX = DIAGRAM_PADDING5;
14714
15251
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14715
- for (const group of parsed.tagGroups) {
14716
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14717
- 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}:`);
14718
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14719
- legendX += labelWidth;
14720
- for (const entry of group.entries) {
14721
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14722
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14723
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14724
- tmpText.remove();
14725
- const pillW = textW + LEGEND_PILL_PAD * 2;
14726
- 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(
14727
- "fill",
14728
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14729
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14730
- 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);
14731
- legendX += pillW + LEGEND_GAP;
14732
- }
14733
- legendX += LEGEND_GROUP_GAP;
14734
- }
15252
+ const legendConfig = {
15253
+ groups: parsed.tagGroups,
15254
+ position: { placement: "top-center", titleRelation: "below-title" },
15255
+ mode: "fixed"
15256
+ };
15257
+ const legendState = { activeGroup: activeTagGroup ?? null };
15258
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15259
+ renderLegendD3(
15260
+ legendG,
15261
+ legendConfig,
15262
+ legendState,
15263
+ palette,
15264
+ isDark,
15265
+ void 0,
15266
+ viewW
15267
+ );
15268
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14735
15269
  }
14736
15270
  if (semanticRoles) {
14737
15271
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14741,55 +15275,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14741
15275
  return false;
14742
15276
  });
14743
15277
  if (presentRoles.length > 0) {
14744
- const measureLabelW = (text, fontSize) => {
14745
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14746
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14747
- dummy.remove();
14748
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14749
- };
14750
- const labelWidths = /* @__PURE__ */ new Map();
14751
- for (const role of presentRoles) {
14752
- labelWidths.set(
14753
- role,
14754
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14755
- );
14756
- }
14757
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14758
- const groupName = "Role";
14759
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14760
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14761
- let totalWidth;
14762
- let entriesWidth2 = 0;
14763
- if (semanticActive) {
14764
- for (const role of presentRoles) {
14765
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14766
- }
14767
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14768
- } else {
14769
- totalWidth = pillWidth2;
14770
- }
14771
- const legendX = (viewW - totalWidth) / 2;
14772
15278
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14773
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14774
- if (semanticActive) {
14775
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14776
- 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);
14777
- 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);
14778
- 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);
14779
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14780
- for (const role of presentRoles) {
14781
- const label = ROLE_LABELS[role];
14782
- const roleColor = palette.colors[ROLE_COLORS[role]];
14783
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14784
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14785
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14786
- 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);
14787
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15279
+ const semanticGroups = [
15280
+ {
15281
+ name: "Role",
15282
+ entries: presentRoles.map((role) => ({
15283
+ value: ROLE_LABELS[role],
15284
+ color: palette.colors[ROLE_COLORS[role]]
15285
+ }))
14788
15286
  }
14789
- } else {
14790
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14791
- 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);
14792
- }
15287
+ ];
15288
+ const legendConfig = {
15289
+ groups: semanticGroups,
15290
+ position: { placement: "top-center", titleRelation: "below-title" },
15291
+ mode: "fixed"
15292
+ };
15293
+ const legendState = {
15294
+ activeGroup: semanticActive ? "Role" : null
15295
+ };
15296
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15297
+ renderLegendD3(
15298
+ legendG,
15299
+ legendConfig,
15300
+ legendState,
15301
+ palette,
15302
+ isDark,
15303
+ void 0,
15304
+ viewW
15305
+ );
15306
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14793
15307
  }
14794
15308
  }
14795
15309
  }
@@ -14832,6 +15346,7 @@ var init_renderer5 = __esm({
14832
15346
  init_palettes();
14833
15347
  init_tag_groups();
14834
15348
  init_legend_constants();
15349
+ init_legend_d3();
14835
15350
  init_title_constants();
14836
15351
  init_parser3();
14837
15352
  init_layout4();
@@ -14856,6 +15371,17 @@ __export(layout_exports5, {
14856
15371
  layoutBoxesAndLines: () => layoutBoxesAndLines
14857
15372
  });
14858
15373
  import dagre4 from "@dagrejs/dagre";
15374
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15375
+ const dx = tx - cx;
15376
+ const dy = ty - cy;
15377
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15378
+ const hw = w / 2;
15379
+ const hh = h / 2;
15380
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15381
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15382
+ const s = Math.min(sx, sy);
15383
+ return { x: cx + dx * s, y: cy + dy * s };
15384
+ }
14859
15385
  function computeNodeSize(_node) {
14860
15386
  const PHI = 1.618;
14861
15387
  const NODE_HEIGHT = 60;
@@ -15008,13 +15534,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
15008
15534
  const srcNode = g.node(edge.source);
15009
15535
  const tgtNode = g.node(edge.target);
15010
15536
  if (!srcNode || !tgtNode) continue;
15011
- const midX = (srcNode.x + tgtNode.x) / 2;
15012
- const midY = (srcNode.y + tgtNode.y) / 2;
15013
- points = [
15014
- { x: srcNode.x, y: srcNode.y },
15015
- { x: midX, y: midY },
15016
- { x: tgtNode.x, y: tgtNode.y }
15017
- ];
15537
+ const srcPt = clipToRectBorder2(
15538
+ srcNode.x,
15539
+ srcNode.y,
15540
+ srcNode.width,
15541
+ srcNode.height,
15542
+ tgtNode.x,
15543
+ tgtNode.y
15544
+ );
15545
+ const tgtPt = clipToRectBorder2(
15546
+ tgtNode.x,
15547
+ tgtNode.y,
15548
+ tgtNode.width,
15549
+ tgtNode.height,
15550
+ srcNode.x,
15551
+ srcNode.y
15552
+ );
15553
+ const midX = (srcPt.x + tgtPt.x) / 2;
15554
+ const midY = (srcPt.y + tgtPt.y) / 2;
15555
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
15018
15556
  } else {
15019
15557
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
15020
15558
  points = dagreEdge?.points ?? [];
@@ -15037,7 +15575,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
15037
15575
  labelY,
15038
15576
  yOffset: edgeYOffsets[i],
15039
15577
  parallelCount: edgeParallelCounts[i],
15040
- metadata: edge.metadata
15578
+ metadata: edge.metadata,
15579
+ deferred: deferredSet.has(i) || void 0
15041
15580
  });
15042
15581
  }
15043
15582
  let maxX = 0;
@@ -15307,12 +15846,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15307
15846
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15308
15847
  edgeGroups.set(i, edgeG);
15309
15848
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15310
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15311
- "d",
15312
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15313
- points
15314
- ) ?? ""
15315
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15849
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
15850
+ const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15316
15851
  if (le.bidirectional) {
15317
15852
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15318
15853
  path.attr("marker-start", `url(#${revId})`);
@@ -15393,50 +15928,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15393
15928
  }
15394
15929
  }
15395
15930
  if (parsed.tagGroups.length > 0) {
15396
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15397
- }
15398
- }
15399
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15400
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15401
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15402
- let totalW = 0;
15403
- for (const tg of parsed.tagGroups) {
15404
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15405
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15406
- if (isActive) {
15407
- totalW += 6;
15408
- for (const entry of tg.entries) {
15409
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15410
- }
15411
- }
15412
- totalW += LEGEND_GROUP_GAP;
15413
- }
15414
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15415
- const legendY = titleOffset + 4;
15416
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15417
- let x = 0;
15418
- for (const tg of parsed.tagGroups) {
15419
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15420
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15421
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15422
- 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);
15423
- if (isActiveGroup) {
15424
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15425
- }
15426
- 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);
15427
- x += nameW;
15428
- if (isActiveGroup) {
15429
- x += 6;
15430
- for (const entry of tg.entries) {
15431
- const entryColor = entry.color || palette.textMuted;
15432
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15433
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15434
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15435
- 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);
15436
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15437
- }
15438
- }
15439
- x += LEGEND_GROUP_GAP;
15931
+ const legendConfig = {
15932
+ groups: parsed.tagGroups,
15933
+ position: { placement: "top-center", titleRelation: "below-title" },
15934
+ mode: "fixed"
15935
+ };
15936
+ const legendState = { activeGroup };
15937
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
15938
+ renderLegendD3(
15939
+ legendG,
15940
+ legendConfig,
15941
+ legendState,
15942
+ palette,
15943
+ isDark,
15944
+ void 0,
15945
+ width
15946
+ );
15947
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15440
15948
  }
15441
15949
  }
15442
15950
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15444,12 +15952,13 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15444
15952
  exportDims: options?.exportDims
15445
15953
  });
15446
15954
  }
15447
- var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
15955
+ var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB, lineGeneratorLinear2;
15448
15956
  var init_renderer6 = __esm({
15449
15957
  "src/boxes-and-lines/renderer.ts"() {
15450
15958
  "use strict";
15451
15959
  init_fonts();
15452
15960
  init_legend_constants();
15961
+ init_legend_d3();
15453
15962
  init_title_constants();
15454
15963
  init_color_utils();
15455
15964
  init_tag_groups();
@@ -15470,6 +15979,7 @@ var init_renderer6 = __esm({
15470
15979
  GROUP_LABEL_FONT_SIZE = 14;
15471
15980
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15472
15981
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
15982
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15473
15983
  }
15474
15984
  });
15475
15985
 
@@ -17378,7 +17888,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17378
17888
  if (activeTagGroup) {
17379
17889
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17380
17890
  }
17381
- renderLegend3(
17891
+ renderLegend2(
17382
17892
  legendParent,
17383
17893
  layout,
17384
17894
  palette,
@@ -17739,52 +18249,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17739
18249
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17740
18250
  }
17741
18251
  }
17742
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17743
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17744
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17745
- ) : layout.legend;
17746
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17747
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17748
- let fixedPositions = null;
17749
- if (fixedWidth != null && visibleGroups.length > 0) {
17750
- fixedPositions = /* @__PURE__ */ new Map();
17751
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17752
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17753
- for (const g of visibleGroups) {
17754
- fixedPositions.set(g.name, cx);
17755
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17756
- }
17757
- }
17758
- for (const group of visibleGroups) {
17759
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17760
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17761
- const pillLabel = group.name;
17762
- const pillWidth2 = pillWidthOf(group);
17763
- const gX = fixedPositions?.get(group.name) ?? group.x;
17764
- const gY = fixedPositions != null ? 0 : group.y;
17765
- 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");
17766
- if (isActive) {
17767
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17768
- }
17769
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17770
- const pillY = LEGEND_CAPSULE_PAD;
17771
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17772
- 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);
17773
- if (isActive) {
17774
- 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);
17775
- }
17776
- 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);
17777
- if (isActive) {
17778
- let entryX = pillX + pillWidth2 + 4;
17779
- for (const entry of group.entries) {
17780
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17781
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17782
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17783
- 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);
17784
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17785
- }
17786
- }
17787
- }
18252
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18253
+ const groups = layout.legend.map((g) => ({
18254
+ name: g.name,
18255
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18256
+ }));
18257
+ const legendConfig = {
18258
+ groups,
18259
+ position: { placement: "top-center", titleRelation: "below-title" },
18260
+ mode: "fixed"
18261
+ };
18262
+ const legendState = { activeGroup: activeTagGroup ?? null };
18263
+ const containerWidth = fixedWidth ?? layout.width;
18264
+ renderLegendD3(
18265
+ parent,
18266
+ legendConfig,
18267
+ legendState,
18268
+ palette,
18269
+ isDark,
18270
+ void 0,
18271
+ containerWidth
18272
+ );
18273
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17788
18274
  }
17789
18275
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17790
18276
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -17995,7 +18481,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
17995
18481
  if (activeTagGroup) {
17996
18482
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17997
18483
  }
17998
- renderLegend3(
18484
+ renderLegend2(
17999
18485
  legendParent,
18000
18486
  layout,
18001
18487
  palette,
@@ -18123,6 +18609,7 @@ var init_renderer7 = __esm({
18123
18609
  init_parser6();
18124
18610
  init_layout6();
18125
18611
  init_legend_constants();
18612
+ init_legend_d3();
18126
18613
  init_title_constants();
18127
18614
  DIAGRAM_PADDING7 = 20;
18128
18615
  MAX_SCALE5 = 3;
@@ -21012,17 +21499,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
21012
21499
  color: r.color,
21013
21500
  key: r.name.toLowerCase().replace(/\s+/g, "-")
21014
21501
  }));
21015
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21016
- let entriesWidth2 = 0;
21502
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21503
+ let entriesWidth3 = 0;
21017
21504
  for (const e of entries) {
21018
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21505
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21019
21506
  }
21020
21507
  groups.push({
21021
21508
  name: "Capabilities",
21022
21509
  type: "role",
21023
21510
  entries,
21024
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
21025
- minifiedWidth: pillWidth2
21511
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21512
+ minifiedWidth: pillWidth3
21026
21513
  });
21027
21514
  }
21028
21515
  for (const tg of tagGroups) {
@@ -21037,113 +21524,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
21037
21524
  }
21038
21525
  }
21039
21526
  if (entries.length === 0) continue;
21040
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21041
- let entriesWidth2 = 0;
21527
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21528
+ let entriesWidth3 = 0;
21042
21529
  for (const e of entries) {
21043
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21530
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21044
21531
  }
21045
21532
  groups.push({
21046
21533
  name: tg.name,
21047
21534
  type: "tag",
21048
21535
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
21049
21536
  entries,
21050
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
21051
- minifiedWidth: pillWidth2
21537
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21538
+ minifiedWidth: pillWidth3
21052
21539
  });
21053
21540
  }
21054
21541
  return groups;
21055
21542
  }
21056
- function computePlaybackWidth(playback) {
21057
- if (!playback) return 0;
21058
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21059
- if (!playback.expanded) return pillWidth2;
21060
- let entriesW = 8;
21061
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21062
- for (const s of playback.speedOptions) {
21063
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
21064
- }
21065
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
21066
- }
21067
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21543
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21068
21544
  if (legendGroups.length === 0 && !playback) return;
21069
21545
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
21070
21546
  if (activeGroup) {
21071
21547
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
21072
21548
  }
21073
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
21074
- const playbackW = computePlaybackWidth(playback);
21075
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
21076
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
21077
- let cursorX = (totalWidth - totalLegendW) / 2;
21549
+ const allGroups = legendGroups.map((g) => ({
21550
+ name: g.name,
21551
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21552
+ }));
21553
+ if (playback) {
21554
+ allGroups.push({ name: "Playback", entries: [] });
21555
+ }
21556
+ const legendConfig = {
21557
+ groups: allGroups,
21558
+ position: { placement: "top-center", titleRelation: "below-title" },
21559
+ mode: "fixed",
21560
+ showEmptyGroups: true
21561
+ };
21562
+ const legendState = { activeGroup };
21563
+ renderLegendD3(
21564
+ legendG,
21565
+ legendConfig,
21566
+ legendState,
21567
+ palette,
21568
+ isDark,
21569
+ void 0,
21570
+ totalWidth
21571
+ );
21572
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
21078
21573
  for (const group of legendGroups) {
21079
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
21080
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
21081
- const pillLabel = group.name;
21082
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21083
- 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");
21084
- if (isActive) {
21085
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21086
- }
21087
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
21088
- const pillYOff = LEGEND_CAPSULE_PAD;
21089
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
21090
- 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);
21091
- if (isActive) {
21092
- 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);
21093
- }
21094
- 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);
21095
- if (isActive) {
21096
- let entryX = pillXOff + pillWidth2 + 4;
21097
- for (const entry of group.entries) {
21098
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21574
+ const groupKey = group.name.toLowerCase();
21575
+ for (const entry of group.entries) {
21576
+ const entryEl = legendG.select(
21577
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21578
+ );
21579
+ if (!entryEl.empty()) {
21580
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21099
21581
  "data-legend-tag-group",
21100
21582
  group.type === "tag" ? group.tagKey ?? "" : null
21101
- ).style("cursor", "pointer");
21102
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21103
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21104
- 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);
21105
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21583
+ );
21106
21584
  }
21107
21585
  }
21108
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21109
21586
  }
21110
- if (playback) {
21111
- const isExpanded = playback.expanded;
21112
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21113
- const pillLabel = "Playback";
21114
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21115
- const fullW = computePlaybackWidth(playback);
21116
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21117
- if (isExpanded) {
21118
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21119
- }
21120
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21121
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21122
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21123
- 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);
21124
- if (isExpanded) {
21125
- 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);
21126
- }
21127
- 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);
21128
- if (isExpanded) {
21129
- let entryX = pillXOff + pillWidth2 + 8;
21130
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21131
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21132
- 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);
21133
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21134
- for (const s of playback.speedOptions) {
21135
- const label = `${s}x`;
21136
- const isActive = playback.speed === s;
21137
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21138
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21139
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21140
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21141
- 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");
21142
- 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);
21143
- entryX += slotW + SPEED_BADGE_GAP;
21144
- }
21145
- }
21146
- cursorX += fullW + LEGEND_GROUP_GAP;
21587
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21588
+ if (!playbackEl.empty()) {
21589
+ playbackEl.classed("infra-playback-pill", true);
21590
+ }
21591
+ if (playback && playback.expanded && !playbackEl.empty()) {
21592
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21593
+ let entryX = pillWidth3 + 8;
21594
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21595
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21596
+ playbackEl.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21597
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21598
+ for (const s of playback.speedOptions) {
21599
+ const label = `${s}x`;
21600
+ const isSpeedActive = playback.speed === s;
21601
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21602
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21603
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21604
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21605
+ speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isSpeedActive ? palette.primary : "transparent");
21606
+ speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isSpeedActive ? "600" : "400").attr("fill", isSpeedActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21607
+ entryX += slotW + SPEED_BADGE_GAP;
21608
+ }
21147
21609
  }
21148
21610
  }
21149
21611
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21274,7 +21736,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21274
21736
  "viewBox",
21275
21737
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21276
21738
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21277
- renderLegend4(
21739
+ renderLegend3(
21278
21740
  legendSvg,
21279
21741
  legendGroups,
21280
21742
  containerWidth,
@@ -21286,7 +21748,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21286
21748
  );
21287
21749
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21288
21750
  } else {
21289
- renderLegend4(
21751
+ renderLegend3(
21290
21752
  rootSvg,
21291
21753
  legendGroups,
21292
21754
  totalWidth,
@@ -21318,6 +21780,7 @@ var init_renderer8 = __esm({
21318
21780
  init_compute();
21319
21781
  init_layout8();
21320
21782
  init_legend_constants();
21783
+ init_legend_d3();
21321
21784
  init_title_constants();
21322
21785
  NODE_FONT_SIZE3 = 13;
21323
21786
  META_FONT_SIZE5 = 10;
@@ -22945,7 +23408,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22945
23408
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22946
23409
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22947
23410
  const showIcon = !legendViewMode && tagGroups.length > 0;
22948
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23411
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22949
23412
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22950
23413
  let groupW = pillW;
22951
23414
  if (isActive) {
@@ -22972,83 +23435,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22972
23435
  const legendX = (containerWidth - totalW) / 2;
22973
23436
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22974
23437
  let cursorX = 0;
22975
- for (let i = 0; i < visibleGroups.length; i++) {
22976
- const group = visibleGroups[i];
22977
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22978
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23438
+ if (visibleGroups.length > 0) {
22979
23439
  const showIcon = !legendViewMode && tagGroups.length > 0;
22980
23440
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
22981
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22982
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
22983
- const groupW = groupWidths[i];
22984
- 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", () => {
22985
- if (onToggle) onToggle(group.name);
23441
+ const legendGroups = visibleGroups.map((g) => {
23442
+ const key = g.name.toLowerCase();
23443
+ const entries = filteredEntries.get(key) ?? g.entries;
23444
+ return {
23445
+ name: g.name,
23446
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23447
+ };
22986
23448
  });
22987
- if (isActive) {
22988
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
22989
- }
22990
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
22991
- const pillYOff = LEGEND_CAPSULE_PAD;
22992
- 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);
22993
- if (isActive) {
22994
- 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);
22995
- }
22996
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
22997
- 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);
22998
- if (showIcon) {
22999
- const iconX = pillXOff + textW + 3;
23000
- const iconY = (LEGEND_HEIGHT - 10) / 2;
23001
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
23002
- iconEl.append("title").text(`Group by ${group.name}`);
23003
- iconEl.style("cursor", "pointer").on("click", (event) => {
23004
- event.stopPropagation();
23005
- if (onSwimlaneChange) {
23006
- onSwimlaneChange(
23007
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
23008
- );
23009
- }
23010
- });
23011
- }
23012
- if (isActive) {
23013
- const tagKey = group.name.toLowerCase();
23014
- const entries = filteredEntries.get(tagKey) ?? group.entries;
23015
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
23016
- for (const entry of entries) {
23017
- const entryValue = entry.value.toLowerCase();
23018
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
23019
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
23020
- 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);
23021
- entryG.on("mouseenter", () => {
23449
+ const legendConfig = {
23450
+ groups: legendGroups,
23451
+ position: {
23452
+ placement: "top-center",
23453
+ titleRelation: "below-title"
23454
+ },
23455
+ mode: "fixed",
23456
+ capsulePillAddonWidth: iconReserve
23457
+ };
23458
+ const legendState = { activeGroup: activeGroupName };
23459
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23460
+ const tagGroupG = legendRow.append("g");
23461
+ const legendCallbacks = {
23462
+ onGroupToggle: onToggle,
23463
+ onEntryHover: (groupName, entryValue) => {
23464
+ const tagKey = groupName.toLowerCase();
23465
+ if (entryValue) {
23466
+ const ev = entryValue.toLowerCase();
23022
23467
  chartG.selectAll(".gantt-task").each(function() {
23023
23468
  const el = d3Selection10.select(this);
23024
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23025
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23469
+ el.attr(
23470
+ "opacity",
23471
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23472
+ );
23026
23473
  });
23027
23474
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
23028
23475
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
23029
23476
  svg.selectAll(".gantt-task-label").each(function() {
23030
23477
  const el = d3Selection10.select(this);
23031
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23032
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23478
+ el.attr(
23479
+ "opacity",
23480
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23481
+ );
23033
23482
  });
23034
23483
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
23035
23484
  svg.selectAll(".gantt-lane-header").each(function() {
23036
23485
  const el = d3Selection10.select(this);
23037
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
23038
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23486
+ el.attr(
23487
+ "opacity",
23488
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23489
+ );
23039
23490
  });
23040
23491
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
23041
- }).on("mouseleave", () => {
23492
+ } else {
23042
23493
  if (criticalPathActive) {
23043
23494
  applyCriticalPathHighlight(svg, chartG);
23044
23495
  } else {
23045
23496
  resetHighlightAll(svg, chartG);
23046
23497
  }
23047
- });
23048
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23498
+ }
23499
+ },
23500
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23501
+ const group = visibleGroups.find((g) => g.name === groupName);
23502
+ if (group) {
23503
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23504
+ }
23505
+ if (showIcon && _isActive) {
23506
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23507
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23508
+ const pillXOff = LEGEND_CAPSULE_PAD;
23509
+ const iconX = pillXOff + textW + 3;
23510
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23511
+ const iconEl = drawSwimlaneIcon(
23512
+ groupEl,
23513
+ iconX,
23514
+ iconY,
23515
+ isSwimlane,
23516
+ palette
23517
+ );
23518
+ iconEl.append("title").text(`Group by ${groupName}`);
23519
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23520
+ event.stopPropagation();
23521
+ if (onSwimlaneChange) {
23522
+ onSwimlaneChange(
23523
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23524
+ );
23525
+ }
23526
+ });
23527
+ }
23049
23528
  }
23529
+ };
23530
+ renderLegendD3(
23531
+ tagGroupG,
23532
+ legendConfig,
23533
+ legendState,
23534
+ palette,
23535
+ isDark,
23536
+ legendCallbacks,
23537
+ tagGroupsW
23538
+ );
23539
+ for (let i = 0; i < visibleGroups.length; i++) {
23540
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
23050
23541
  }
23051
- cursorX += groupW + LEGEND_GROUP_GAP;
23052
23542
  }
23053
23543
  if (hasCriticalPath) {
23054
23544
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23675,6 +24165,7 @@ var init_renderer9 = __esm({
23675
24165
  init_tag_groups();
23676
24166
  init_d3();
23677
24167
  init_legend_constants();
24168
+ init_legend_d3();
23678
24169
  init_title_constants();
23679
24170
  BAR_H = 22;
23680
24171
  ROW_GAP = 6;
@@ -24845,57 +25336,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24845
25336
  }
24846
25337
  if (parsed.tagGroups.length > 0) {
24847
25338
  const legendY = TOP_MARGIN + titleOffset;
24848
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24849
- const legendItems = [];
24850
- for (const tg of parsed.tagGroups) {
24851
- if (tg.entries.length === 0) continue;
24852
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24853
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24854
- const entries = tg.entries.map((e) => ({
25339
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25340
+ name: tg.name,
25341
+ entries: tg.entries.map((e) => ({
24855
25342
  value: e.value,
24856
25343
  color: resolveColor(e.color) ?? e.color
24857
- }));
24858
- let totalWidth2 = pillWidth2;
24859
- if (isActive) {
24860
- let entriesWidth2 = 0;
24861
- for (const entry of entries) {
24862
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24863
- }
24864
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24865
- }
24866
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24867
- }
24868
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24869
- let legendX = (svgWidth - totalLegendWidth) / 2;
24870
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24871
- if (activeTagGroup) {
24872
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24873
- }
24874
- for (const item of legendItems) {
24875
- 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");
24876
- if (item.isActive) {
24877
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24878
- }
24879
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24880
- const pillYOff = LEGEND_CAPSULE_PAD;
24881
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24882
- 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);
24883
- if (item.isActive) {
24884
- 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);
24885
- }
24886
- 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);
24887
- if (item.isActive) {
24888
- let entryX = pillXOff + item.pillWidth + 4;
24889
- for (const entry of item.entries) {
24890
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24891
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24892
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24893
- 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);
24894
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24895
- }
24896
- }
24897
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24898
- }
25344
+ }))
25345
+ }));
25346
+ const legendConfig = {
25347
+ groups: resolvedGroups,
25348
+ position: { placement: "top-center", titleRelation: "below-title" },
25349
+ mode: "fixed"
25350
+ };
25351
+ const legendState = { activeGroup: activeTagGroup ?? null };
25352
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25353
+ renderLegendD3(
25354
+ legendG,
25355
+ legendConfig,
25356
+ legendState,
25357
+ palette,
25358
+ isDark,
25359
+ void 0,
25360
+ svgWidth
25361
+ );
24899
25362
  }
24900
25363
  for (const group of groups) {
24901
25364
  if (group.participantIds.length === 0) continue;
@@ -25454,6 +25917,7 @@ var init_renderer10 = __esm({
25454
25917
  init_parser();
25455
25918
  init_tag_resolution();
25456
25919
  init_legend_constants();
25920
+ init_legend_d3();
25457
25921
  init_title_constants();
25458
25922
  PARTICIPANT_GAP = 160;
25459
25923
  PARTICIPANT_BOX_WIDTH = 120;
@@ -28114,7 +28578,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28114
28578
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28115
28579
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28116
28580
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28117
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28118
28581
  const LG_ICON_W = 20;
28119
28582
  const mainSvg = d3Selection13.select(container).select("svg");
28120
28583
  const mainG = mainSvg.select("g");
@@ -28153,11 +28616,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28153
28616
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28154
28617
  ) : legendGroups;
28155
28618
  if (visibleGroups.length === 0) return;
28156
- const totalW = visibleGroups.reduce((s, lg) => {
28157
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28158
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28159
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28160
- let cx = (width - totalW) / 2;
28161
28619
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28162
28620
  if (currentActiveGroup) {
28163
28621
  legendContainer.attr(
@@ -28165,82 +28623,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28165
28623
  currentActiveGroup.toLowerCase()
28166
28624
  );
28167
28625
  }
28168
- for (const lg of visibleGroups) {
28169
- const groupKey = lg.group.name.toLowerCase();
28170
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28171
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28172
- const pillLabel = lg.group.name;
28173
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28174
- 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__");
28175
- if (!viewMode) {
28176
- gEl.style("cursor", "pointer").on("click", () => {
28177
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28178
- drawLegend2();
28179
- recolorEvents2();
28180
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28181
- });
28182
- }
28183
- if (isActive) {
28184
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28185
- }
28186
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28187
- const pillYOff = LG_CAPSULE_PAD;
28188
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28189
- 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);
28190
- if (isActive) {
28191
- 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);
28192
- }
28193
- 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);
28194
- if (isActive) {
28195
- let entryX;
28196
- if (!viewMode) {
28197
- const iconX = pillXOff + pillWidth2 + 5;
28626
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28627
+ const centralGroups = visibleGroups.map((lg) => ({
28628
+ name: lg.group.name,
28629
+ entries: lg.group.entries.map((e) => ({
28630
+ value: e.value,
28631
+ color: e.color
28632
+ }))
28633
+ }));
28634
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28635
+ const centralConfig = {
28636
+ groups: centralGroups,
28637
+ position: { placement: "top-center", titleRelation: "below-title" },
28638
+ mode: "fixed",
28639
+ capsulePillAddonWidth: iconAddon
28640
+ };
28641
+ const centralState = { activeGroup: centralActive };
28642
+ const centralCallbacks = viewMode ? {} : {
28643
+ onGroupToggle: (groupName) => {
28644
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28645
+ drawLegend2();
28646
+ recolorEvents2();
28647
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28648
+ },
28649
+ onEntryHover: (groupName, entryValue) => {
28650
+ const tagKey = groupName.toLowerCase();
28651
+ if (entryValue) {
28652
+ const tagVal = entryValue.toLowerCase();
28653
+ fadeToTagValue(mainG, tagKey, tagVal);
28654
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28655
+ const el = d3Selection13.select(this);
28656
+ const ev = el.attr("data-legend-entry");
28657
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28658
+ el.attr(
28659
+ "opacity",
28660
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28661
+ );
28662
+ });
28663
+ } else {
28664
+ fadeReset(mainG);
28665
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28666
+ }
28667
+ },
28668
+ onGroupRendered: (groupName, groupEl, isActive) => {
28669
+ const groupKey = groupName.toLowerCase();
28670
+ groupEl.attr("data-tag-group", groupKey);
28671
+ if (isActive && !viewMode) {
28672
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28673
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28674
+ const pillXOff = LG_CAPSULE_PAD;
28675
+ const iconX = pillXOff + pillWidth3 + 5;
28198
28676
  const iconY = (LG_HEIGHT - 10) / 2;
28199
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28677
+ const iconEl = drawSwimlaneIcon3(
28678
+ groupEl,
28679
+ iconX,
28680
+ iconY,
28681
+ isSwimActive
28682
+ );
28200
28683
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28201
28684
  event.stopPropagation();
28202
28685
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28203
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28686
+ onTagStateChange?.(
28687
+ currentActiveGroup,
28688
+ currentSwimlaneGroup
28689
+ );
28204
28690
  relayout2();
28205
28691
  });
28206
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28207
- } else {
28208
- entryX = pillXOff + pillWidth2 + 8;
28209
- }
28210
- for (const entry of lg.group.entries) {
28211
- const tagKey = lg.group.name.toLowerCase();
28212
- const tagVal = entry.value.toLowerCase();
28213
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28214
- if (!viewMode) {
28215
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28216
- event.stopPropagation();
28217
- fadeToTagValue(mainG, tagKey, tagVal);
28218
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28219
- const el = d3Selection13.select(this);
28220
- const ev = el.attr("data-legend-entry");
28221
- if (ev === "__group__") return;
28222
- const eg = el.attr("data-tag-group");
28223
- el.attr(
28224
- "opacity",
28225
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28226
- );
28227
- });
28228
- }).on("mouseleave", (event) => {
28229
- event.stopPropagation();
28230
- fadeReset(mainG);
28231
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28232
- }).on("click", (event) => {
28233
- event.stopPropagation();
28234
- });
28235
- }
28236
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28237
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28238
- 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);
28239
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28240
28692
  }
28241
28693
  }
28242
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28243
- }
28694
+ };
28695
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28696
+ renderLegendD3(
28697
+ legendInnerG,
28698
+ centralConfig,
28699
+ centralState,
28700
+ palette,
28701
+ isDark,
28702
+ centralCallbacks,
28703
+ width
28704
+ );
28244
28705
  }, recolorEvents2 = function() {
28245
28706
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28246
28707
  mainG.selectAll(".tl-event").each(function() {
@@ -28265,7 +28726,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28265
28726
  };
28266
28727
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28267
28728
  const legendY = title ? 50 : 10;
28268
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28269
28729
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28270
28730
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28271
28731
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -29507,6 +29967,7 @@ var init_d3 = __esm({
29507
29967
  init_parsing();
29508
29968
  init_tag_groups();
29509
29969
  init_legend_constants();
29970
+ init_legend_d3();
29510
29971
  init_title_constants();
29511
29972
  DEFAULT_CLOUD_OPTIONS = {
29512
29973
  rotate: "none",
@@ -29661,11 +30122,26 @@ async function ensureDom() {
29661
30122
  const { JSDOM } = await import("jsdom");
29662
30123
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29663
30124
  const win = dom.window;
29664
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29665
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29666
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29667
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29668
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30125
+ Object.defineProperty(globalThis, "document", {
30126
+ value: win.document,
30127
+ configurable: true
30128
+ });
30129
+ Object.defineProperty(globalThis, "window", {
30130
+ value: win,
30131
+ configurable: true
30132
+ });
30133
+ Object.defineProperty(globalThis, "navigator", {
30134
+ value: win.navigator,
30135
+ configurable: true
30136
+ });
30137
+ Object.defineProperty(globalThis, "HTMLElement", {
30138
+ value: win.HTMLElement,
30139
+ configurable: true
30140
+ });
30141
+ Object.defineProperty(globalThis, "SVGElement", {
30142
+ value: win.SVGElement,
30143
+ configurable: true
30144
+ });
29669
30145
  }
29670
30146
  async function render(content, options) {
29671
30147
  const theme = options?.theme ?? "light";
@@ -29674,11 +30150,17 @@ async function render(content, options) {
29674
30150
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29675
30151
  const chartType = parseDgmoChartType(content);
29676
30152
  const category = chartType ? getRenderCategory(chartType) : null;
30153
+ const legendExportState = options?.legendState ? {
30154
+ activeTagGroup: options.legendState.activeGroup ?? null,
30155
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30156
+ } : void 0;
29677
30157
  if (category === "data-chart") {
29678
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30158
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30159
+ branding
30160
+ });
29679
30161
  }
29680
30162
  await ensureDom();
29681
- return renderForExport(content, theme, paletteColors, void 0, {
30163
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29682
30164
  branding,
29683
30165
  c4Level: options?.c4Level,
29684
30166
  c4System: options?.c4System,
@@ -30401,6 +30883,8 @@ init_flowchart_renderer();
30401
30883
  init_echarts();
30402
30884
  init_legend_svg();
30403
30885
  init_legend_constants();
30886
+ init_legend_d3();
30887
+ init_legend_layout();
30404
30888
  init_d3();
30405
30889
  init_renderer10();
30406
30890
  init_colors();
@@ -31247,6 +31731,7 @@ export {
31247
31731
  computeCardMove,
31248
31732
  computeInfra,
31249
31733
  computeInfraLegendGroups,
31734
+ computeLegendLayout,
31250
31735
  computeScatterLabelGraphics,
31251
31736
  computeTimeTicks,
31252
31737
  contrastText,
@@ -31258,6 +31743,7 @@ export {
31258
31743
  formatDgmoError,
31259
31744
  getAvailablePalettes,
31260
31745
  getExtendedChartLegendGroups,
31746
+ getLegendReservedHeight,
31261
31747
  getPalette,
31262
31748
  getRenderCategory,
31263
31749
  getSeriesColors,
@@ -31346,7 +31832,9 @@ export {
31346
31832
  renderInfra,
31347
31833
  renderKanban,
31348
31834
  renderKanbanForExport,
31835
+ renderLegendD3,
31349
31836
  renderLegendSvg,
31837
+ renderLegendSvgFromConfig,
31350
31838
  renderOrg,
31351
31839
  renderOrgForExport,
31352
31840
  renderQuadrant,