@diagrammo/dgmo 0.8.9 → 0.8.11

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.
Files changed (46) hide show
  1. package/AGENTS.md +3 -0
  2. package/dist/cli.cjs +245 -672
  3. package/dist/editor.cjs.map +1 -1
  4. package/dist/editor.d.cts +2 -3
  5. package/dist/editor.d.ts +2 -3
  6. package/dist/editor.js.map +1 -1
  7. package/dist/index.cjs +1623 -800
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +153 -1
  10. package/dist/index.d.ts +153 -1
  11. package/dist/index.js +1619 -802
  12. package/dist/index.js.map +1 -1
  13. package/docs/language-reference.md +28 -2
  14. package/gallery/fixtures/sitemap-full.dgmo +1 -0
  15. package/package.json +14 -17
  16. package/src/boxes-and-lines/layout.ts +48 -8
  17. package/src/boxes-and-lines/parser.ts +59 -13
  18. package/src/boxes-and-lines/renderer.ts +34 -138
  19. package/src/c4/layout.ts +31 -10
  20. package/src/c4/renderer.ts +25 -138
  21. package/src/class/renderer.ts +185 -186
  22. package/src/d3.ts +194 -222
  23. package/src/echarts.ts +56 -57
  24. package/src/editor/index.ts +1 -2
  25. package/src/er/renderer.ts +52 -245
  26. package/src/gantt/renderer.ts +140 -182
  27. package/src/gantt/resolver.ts +19 -14
  28. package/src/index.ts +23 -1
  29. package/src/infra/renderer.ts +91 -244
  30. package/src/kanban/renderer.ts +29 -133
  31. package/src/label-layout.ts +286 -0
  32. package/src/org/renderer.ts +103 -170
  33. package/src/render.ts +39 -9
  34. package/src/sequence/parser.ts +4 -0
  35. package/src/sequence/renderer.ts +47 -154
  36. package/src/sitemap/layout.ts +180 -38
  37. package/src/sitemap/parser.ts +64 -23
  38. package/src/sitemap/renderer.ts +73 -161
  39. package/src/utils/arrows.ts +1 -1
  40. package/src/utils/legend-constants.ts +6 -0
  41. package/src/utils/legend-d3.ts +400 -0
  42. package/src/utils/legend-layout.ts +491 -0
  43. package/src/utils/legend-svg.ts +28 -2
  44. package/src/utils/legend-types.ts +166 -0
  45. package/src/utils/parsing.ts +1 -1
  46. package/src/utils/tag-groups.ts +1 -1
package/dist/index.js CHANGED
@@ -122,6 +122,183 @@ var init_branding = __esm({
122
122
  }
123
123
  });
124
124
 
125
+ // src/label-layout.ts
126
+ function rectsOverlap(a, b) {
127
+ return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
128
+ }
129
+ function rectCircleOverlap(rect, circle) {
130
+ const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
131
+ const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
132
+ const dx = nearestX - circle.cx;
133
+ const dy = nearestY - circle.cy;
134
+ return dx * dx + dy * dy < circle.r * circle.r;
135
+ }
136
+ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius, fontSize) {
137
+ const labelHeight = fontSize + 4;
138
+ const stepSize = labelHeight + 2;
139
+ const minGap = pointRadius + 4;
140
+ const pointCircles = points.map((p) => ({
141
+ cx: p.cx,
142
+ cy: p.cy,
143
+ r: pointRadius
144
+ }));
145
+ const placedLabels = [];
146
+ const results = [];
147
+ for (let i = 0; i < points.length; i++) {
148
+ const pt = points[i];
149
+ const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
150
+ let best = null;
151
+ const directions = [
152
+ {
153
+ // Above
154
+ gen: (offset) => {
155
+ const lx = pt.cx - labelWidth / 2;
156
+ const ly = pt.cy - offset - labelHeight;
157
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
158
+ return null;
159
+ return {
160
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
161
+ textX: pt.cx,
162
+ textY: ly + labelHeight / 2,
163
+ anchor: "middle"
164
+ };
165
+ }
166
+ },
167
+ {
168
+ // Below
169
+ gen: (offset) => {
170
+ const lx = pt.cx - labelWidth / 2;
171
+ const ly = pt.cy + offset;
172
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
173
+ return null;
174
+ return {
175
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
176
+ textX: pt.cx,
177
+ textY: ly + labelHeight / 2,
178
+ anchor: "middle"
179
+ };
180
+ }
181
+ },
182
+ {
183
+ // Right
184
+ gen: (offset) => {
185
+ const lx = pt.cx + offset;
186
+ const ly = pt.cy - labelHeight / 2;
187
+ if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
188
+ return null;
189
+ return {
190
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
191
+ textX: lx,
192
+ textY: pt.cy,
193
+ anchor: "start"
194
+ };
195
+ }
196
+ },
197
+ {
198
+ // Left
199
+ gen: (offset) => {
200
+ const lx = pt.cx - offset - labelWidth;
201
+ const ly = pt.cy - labelHeight / 2;
202
+ if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
203
+ return null;
204
+ return {
205
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
206
+ textX: lx + labelWidth,
207
+ textY: pt.cy,
208
+ anchor: "end"
209
+ };
210
+ }
211
+ }
212
+ ];
213
+ for (const { gen } of directions) {
214
+ for (let offset = minGap; ; offset += stepSize) {
215
+ const cand = gen(offset);
216
+ if (!cand) break;
217
+ let collision = false;
218
+ for (const pl of placedLabels) {
219
+ if (rectsOverlap(cand.rect, pl)) {
220
+ collision = true;
221
+ break;
222
+ }
223
+ }
224
+ if (!collision) {
225
+ for (const circle of pointCircles) {
226
+ if (rectCircleOverlap(cand.rect, circle)) {
227
+ collision = true;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (!collision) {
233
+ for (const obs of obstacles) {
234
+ if (rectsOverlap(cand.rect, obs)) {
235
+ collision = true;
236
+ break;
237
+ }
238
+ }
239
+ }
240
+ if (!collision) {
241
+ const dist = offset;
242
+ if (!best || dist < best.dist) {
243
+ best = {
244
+ rect: cand.rect,
245
+ textX: cand.textX,
246
+ textY: cand.textY,
247
+ anchor: cand.anchor,
248
+ dist
249
+ };
250
+ }
251
+ break;
252
+ }
253
+ }
254
+ }
255
+ if (!best) {
256
+ const lx = pt.cx - labelWidth / 2;
257
+ const ly = pt.cy - minGap - labelHeight;
258
+ best = {
259
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
260
+ textX: pt.cx,
261
+ textY: ly + labelHeight / 2,
262
+ anchor: "middle",
263
+ dist: minGap
264
+ };
265
+ }
266
+ placedLabels.push(best.rect);
267
+ let connectorLine;
268
+ if (best.dist > minGap + stepSize) {
269
+ const dx = best.textX - pt.cx;
270
+ const dy = best.textY - pt.cy;
271
+ const angle = Math.atan2(dy, dx);
272
+ const x1 = pt.cx + Math.cos(angle) * pointRadius;
273
+ const y1 = pt.cy + Math.sin(angle) * pointRadius;
274
+ const x2 = Math.max(
275
+ best.rect.x,
276
+ Math.min(pt.cx, best.rect.x + best.rect.w)
277
+ );
278
+ const y2 = Math.max(
279
+ best.rect.y,
280
+ Math.min(pt.cy, best.rect.y + best.rect.h)
281
+ );
282
+ connectorLine = { x1, y1, x2, y2 };
283
+ }
284
+ results.push({
285
+ label: pt.label,
286
+ x: best.textX,
287
+ y: best.textY,
288
+ anchor: best.anchor,
289
+ connectorLine
290
+ });
291
+ }
292
+ return results;
293
+ }
294
+ var CHAR_WIDTH_RATIO;
295
+ var init_label_layout = __esm({
296
+ "src/label-layout.ts"() {
297
+ "use strict";
298
+ CHAR_WIDTH_RATIO = 0.6;
299
+ }
300
+ });
301
+
125
302
  // src/colors.ts
126
303
  function resolveColor(color, palette) {
127
304
  if (color.startsWith("#")) return null;
@@ -1792,7 +1969,7 @@ function measureLegendText(text, fontSize) {
1792
1969
  }
1793
1970
  return w;
1794
1971
  }
1795
- var LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_SIZE, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, LEGEND_ICON_W, CHAR_W, DEFAULT_W, EYE_OPEN_PATH, EYE_CLOSED_PATH;
1972
+ var LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_SIZE, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, LEGEND_ICON_W, LEGEND_MAX_ENTRY_ROWS, CHAR_W, DEFAULT_W, EYE_OPEN_PATH, EYE_CLOSED_PATH;
1796
1973
  var init_legend_constants = __esm({
1797
1974
  "src/utils/legend-constants.ts"() {
1798
1975
  "use strict";
@@ -1808,6 +1985,7 @@ var init_legend_constants = __esm({
1808
1985
  LEGEND_EYE_SIZE = 14;
1809
1986
  LEGEND_EYE_GAP = 6;
1810
1987
  LEGEND_ICON_W = 20;
1988
+ LEGEND_MAX_ENTRY_ROWS = 3;
1811
1989
  CHAR_W = {
1812
1990
  " ": 0.28,
1813
1991
  "!": 0.28,
@@ -1901,6 +2079,492 @@ var init_legend_constants = __esm({
1901
2079
  }
1902
2080
  });
1903
2081
 
2082
+ // src/utils/legend-layout.ts
2083
+ function pillWidth(name) {
2084
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
2085
+ }
2086
+ function entriesWidth(entries) {
2087
+ let w = 0;
2088
+ for (const e of entries) {
2089
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2090
+ }
2091
+ return w;
2092
+ }
2093
+ function entryWidth(value) {
2094
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2095
+ }
2096
+ function controlWidth(control) {
2097
+ let w = CONTROL_PILL_PAD;
2098
+ if (control.label) {
2099
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
2100
+ if (control.icon) w += CONTROL_ICON_GAP;
2101
+ }
2102
+ if (control.icon) w += 14;
2103
+ if (control.children) {
2104
+ for (const child of control.children) {
2105
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
2106
+ }
2107
+ }
2108
+ return w;
2109
+ }
2110
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
2111
+ const pw = pillWidth(name);
2112
+ const maxCapsuleW = containerWidth;
2113
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
2114
+ const ew = entriesWidth(entries);
2115
+ const singleRowW = baseW + ew;
2116
+ if (singleRowW <= maxCapsuleW) {
2117
+ return {
2118
+ width: singleRowW,
2119
+ entryRows: 1,
2120
+ moreCount: 0,
2121
+ visibleEntries: entries.length
2122
+ };
2123
+ }
2124
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
2125
+ let row = 1;
2126
+ let rowX = pw + 4;
2127
+ let visible = 0;
2128
+ for (let i = 0; i < entries.length; i++) {
2129
+ const ew2 = entryWidth(entries[i].value);
2130
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
2131
+ row++;
2132
+ rowX = 0;
2133
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
2134
+ return {
2135
+ width: maxCapsuleW,
2136
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
2137
+ moreCount: entries.length - visible,
2138
+ visibleEntries: visible
2139
+ };
2140
+ }
2141
+ }
2142
+ rowX += ew2;
2143
+ visible++;
2144
+ }
2145
+ return {
2146
+ width: maxCapsuleW,
2147
+ entryRows: row,
2148
+ moreCount: 0,
2149
+ visibleEntries: entries.length
2150
+ };
2151
+ }
2152
+ function computeLegendLayout(config, state, containerWidth) {
2153
+ const { groups, controls: configControls, mode } = config;
2154
+ const isExport = mode === "inline";
2155
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
2156
+ if (isExport && !activeGroupName) {
2157
+ return {
2158
+ height: 0,
2159
+ width: 0,
2160
+ rows: [],
2161
+ controls: [],
2162
+ pills: [],
2163
+ activeCapsule: void 0
2164
+ };
2165
+ }
2166
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2167
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2168
+ return {
2169
+ height: 0,
2170
+ width: 0,
2171
+ rows: [],
2172
+ controls: [],
2173
+ pills: [],
2174
+ activeCapsule: void 0
2175
+ };
2176
+ }
2177
+ const controlLayouts = [];
2178
+ let totalControlsW = 0;
2179
+ if (configControls && !isExport) {
2180
+ for (const ctrl of configControls) {
2181
+ const w = controlWidth(ctrl);
2182
+ controlLayouts.push({
2183
+ id: ctrl.id,
2184
+ x: 0,
2185
+ // positioned later
2186
+ y: 0,
2187
+ width: w,
2188
+ height: LEGEND_HEIGHT,
2189
+ icon: ctrl.icon,
2190
+ label: ctrl.label,
2191
+ exportBehavior: ctrl.exportBehavior,
2192
+ children: ctrl.children?.map((c) => ({
2193
+ id: c.id,
2194
+ label: c.label,
2195
+ x: 0,
2196
+ y: 0,
2197
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2198
+ isActive: c.isActive
2199
+ }))
2200
+ });
2201
+ totalControlsW += w + CONTROL_GAP;
2202
+ }
2203
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2204
+ } else if (configControls && isExport) {
2205
+ for (const ctrl of configControls) {
2206
+ if (ctrl.exportBehavior === "strip") continue;
2207
+ const w = controlWidth(ctrl);
2208
+ controlLayouts.push({
2209
+ id: ctrl.id,
2210
+ x: 0,
2211
+ y: 0,
2212
+ width: w,
2213
+ height: LEGEND_HEIGHT,
2214
+ icon: ctrl.icon,
2215
+ label: ctrl.label,
2216
+ exportBehavior: ctrl.exportBehavior,
2217
+ children: ctrl.children?.map((c) => ({
2218
+ id: c.id,
2219
+ label: c.label,
2220
+ x: 0,
2221
+ y: 0,
2222
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2223
+ isActive: c.isActive
2224
+ }))
2225
+ });
2226
+ totalControlsW += w + CONTROL_GAP;
2227
+ }
2228
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2229
+ }
2230
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2231
+ const groupAvailW = containerWidth - controlsSpace;
2232
+ const pills = [];
2233
+ let activeCapsule;
2234
+ for (const g of visibleGroups) {
2235
+ const isActive = activeGroupName === g.name.toLowerCase();
2236
+ if (isExport && !isActive) continue;
2237
+ if (isActive) {
2238
+ activeCapsule = buildCapsuleLayout(
2239
+ g,
2240
+ containerWidth,
2241
+ config.capsulePillAddonWidth ?? 0
2242
+ );
2243
+ } else {
2244
+ const pw = pillWidth(g.name);
2245
+ pills.push({
2246
+ groupName: g.name,
2247
+ x: 0,
2248
+ y: 0,
2249
+ width: pw,
2250
+ height: LEGEND_HEIGHT,
2251
+ isActive: false
2252
+ });
2253
+ }
2254
+ }
2255
+ const rows = layoutRows(
2256
+ activeCapsule,
2257
+ pills,
2258
+ controlLayouts,
2259
+ groupAvailW,
2260
+ containerWidth,
2261
+ totalControlsW
2262
+ );
2263
+ const height = rows.length * LEGEND_HEIGHT;
2264
+ const width = containerWidth;
2265
+ return {
2266
+ height,
2267
+ width,
2268
+ rows,
2269
+ activeCapsule,
2270
+ controls: controlLayouts,
2271
+ pills
2272
+ };
2273
+ }
2274
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2275
+ const pw = pillWidth(group.name);
2276
+ const info = capsuleWidth(
2277
+ group.name,
2278
+ group.entries,
2279
+ containerWidth,
2280
+ addonWidth
2281
+ );
2282
+ const pill = {
2283
+ groupName: group.name,
2284
+ x: LEGEND_CAPSULE_PAD,
2285
+ y: LEGEND_CAPSULE_PAD,
2286
+ width: pw,
2287
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2288
+ isActive: true
2289
+ };
2290
+ const entries = [];
2291
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2292
+ let ey = 0;
2293
+ let rowX = ex;
2294
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2295
+ let currentRow = 0;
2296
+ for (let i = 0; i < info.visibleEntries; i++) {
2297
+ const entry = group.entries[i];
2298
+ const ew = entryWidth(entry.value);
2299
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2300
+ currentRow++;
2301
+ rowX = 0;
2302
+ ey = currentRow * LEGEND_HEIGHT;
2303
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2304
+ }
2305
+ const dotCx = rowX + LEGEND_DOT_R;
2306
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2307
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2308
+ const textY = ey + LEGEND_HEIGHT / 2;
2309
+ entries.push({
2310
+ value: entry.value,
2311
+ color: entry.color,
2312
+ x: rowX,
2313
+ y: ey,
2314
+ dotCx,
2315
+ dotCy,
2316
+ textX,
2317
+ textY
2318
+ });
2319
+ rowX += ew;
2320
+ }
2321
+ const totalRows = info.entryRows;
2322
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2323
+ return {
2324
+ groupName: group.name,
2325
+ x: 0,
2326
+ y: 0,
2327
+ width: info.width,
2328
+ height: capsuleH,
2329
+ pill,
2330
+ entries,
2331
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2332
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2333
+ };
2334
+ }
2335
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2336
+ const rows = [];
2337
+ const groupItems = [];
2338
+ if (activeCapsule) groupItems.push(activeCapsule);
2339
+ groupItems.push(...pills);
2340
+ let currentRowItems = [];
2341
+ let currentRowW = 0;
2342
+ let rowY = 0;
2343
+ for (const item of groupItems) {
2344
+ const itemW = item.width + LEGEND_GROUP_GAP;
2345
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2346
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2347
+ rows.push({ y: rowY, items: currentRowItems });
2348
+ rowY += LEGEND_HEIGHT;
2349
+ currentRowItems = [];
2350
+ currentRowW = 0;
2351
+ }
2352
+ item.x = currentRowW;
2353
+ item.y = rowY;
2354
+ currentRowItems.push(item);
2355
+ currentRowW += itemW;
2356
+ }
2357
+ if (controls.length > 0) {
2358
+ let cx = containerWidth;
2359
+ for (let i = controls.length - 1; i >= 0; i--) {
2360
+ cx -= controls[i].width;
2361
+ controls[i].x = cx;
2362
+ controls[i].y = 0;
2363
+ cx -= CONTROL_GAP;
2364
+ }
2365
+ if (rows.length > 0) {
2366
+ rows[0].items.push(...controls);
2367
+ } else if (currentRowItems.length > 0) {
2368
+ currentRowItems.push(...controls);
2369
+ } else {
2370
+ currentRowItems.push(...controls);
2371
+ }
2372
+ }
2373
+ if (currentRowItems.length > 0) {
2374
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2375
+ rows.push({ y: rowY, items: currentRowItems });
2376
+ }
2377
+ if (rows.length === 0) {
2378
+ rows.push({ y: 0, items: [] });
2379
+ }
2380
+ return rows;
2381
+ }
2382
+ function centerRowItems(items, containerWidth, totalControlsW) {
2383
+ const groupItems = items.filter((it) => "groupName" in it);
2384
+ if (groupItems.length === 0) return;
2385
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2386
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2387
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2388
+ let x = offset;
2389
+ for (const item of groupItems) {
2390
+ item.x = x;
2391
+ x += item.width + LEGEND_GROUP_GAP;
2392
+ }
2393
+ }
2394
+ function getLegendReservedHeight(config, state, containerWidth) {
2395
+ const layout = computeLegendLayout(config, state, containerWidth);
2396
+ return layout.height;
2397
+ }
2398
+ var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2399
+ var init_legend_layout = __esm({
2400
+ "src/utils/legend-layout.ts"() {
2401
+ "use strict";
2402
+ init_legend_constants();
2403
+ CONTROL_PILL_PAD = 16;
2404
+ CONTROL_FONT_SIZE = 11;
2405
+ CONTROL_ICON_GAP = 4;
2406
+ CONTROL_GAP = 8;
2407
+ }
2408
+ });
2409
+
2410
+ // src/utils/legend-d3.ts
2411
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2412
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2413
+ let currentState = { ...state };
2414
+ let currentLayout;
2415
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2416
+ function render2() {
2417
+ currentLayout = computeLegendLayout(config, currentState, width);
2418
+ legendG.selectAll("*").remove();
2419
+ if (currentLayout.height === 0) return;
2420
+ if (currentState.activeGroup) {
2421
+ legendG.attr(
2422
+ "data-legend-active",
2423
+ currentState.activeGroup.toLowerCase()
2424
+ );
2425
+ } else {
2426
+ legendG.attr("data-legend-active", null);
2427
+ }
2428
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2429
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2430
+ if (currentLayout.activeCapsule) {
2431
+ renderCapsule(
2432
+ legendG,
2433
+ currentLayout.activeCapsule,
2434
+ palette,
2435
+ groupBg,
2436
+ pillBorder,
2437
+ isDark,
2438
+ callbacks
2439
+ );
2440
+ }
2441
+ for (const pill of currentLayout.pills) {
2442
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2443
+ }
2444
+ for (const ctrl of currentLayout.controls) {
2445
+ renderControl(
2446
+ legendG,
2447
+ ctrl,
2448
+ palette,
2449
+ groupBg,
2450
+ pillBorder,
2451
+ isDark,
2452
+ config.controls
2453
+ );
2454
+ }
2455
+ }
2456
+ render2();
2457
+ return {
2458
+ setState(newState) {
2459
+ currentState = { ...newState };
2460
+ render2();
2461
+ },
2462
+ destroy() {
2463
+ legendG.remove();
2464
+ },
2465
+ getHeight() {
2466
+ return currentLayout?.height ?? 0;
2467
+ },
2468
+ getLayout() {
2469
+ return currentLayout;
2470
+ }
2471
+ };
2472
+ }
2473
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2474
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2475
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2476
+ const pill = capsule.pill;
2477
+ 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);
2478
+ 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);
2479
+ 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);
2480
+ for (const entry of capsule.entries) {
2481
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2482
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2483
+ 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);
2484
+ if (callbacks?.onEntryHover) {
2485
+ const groupName = capsule.groupName;
2486
+ const entryValue = entry.value;
2487
+ const onHover = callbacks.onEntryHover;
2488
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2489
+ }
2490
+ }
2491
+ if (capsule.moreCount) {
2492
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2493
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2494
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2495
+ 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`);
2496
+ }
2497
+ if (callbacks?.onGroupToggle) {
2498
+ const cb = callbacks.onGroupToggle;
2499
+ const name = capsule.groupName;
2500
+ g.on("click", () => cb(name));
2501
+ }
2502
+ if (callbacks?.onGroupRendered) {
2503
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2504
+ }
2505
+ }
2506
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2507
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2508
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2509
+ 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);
2510
+ if (callbacks?.onGroupToggle) {
2511
+ const cb = callbacks.onGroupToggle;
2512
+ const name = pill.groupName;
2513
+ g.on("click", () => cb(name));
2514
+ }
2515
+ if (callbacks?.onGroupRendered) {
2516
+ callbacks.onGroupRendered(pill.groupName, g, false);
2517
+ }
2518
+ }
2519
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2520
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2521
+ if (ctrl.exportBehavior === "strip") {
2522
+ g.attr("data-export-ignore", "true");
2523
+ }
2524
+ 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);
2525
+ let textX = ctrl.width / 2;
2526
+ if (ctrl.icon && ctrl.label) {
2527
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2528
+ iconG.html(ctrl.icon);
2529
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2530
+ }
2531
+ if (ctrl.label) {
2532
+ 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);
2533
+ }
2534
+ if (ctrl.children) {
2535
+ let cx = ctrl.width + 4;
2536
+ for (const child of ctrl.children) {
2537
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2538
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2539
+ "fill",
2540
+ child.isActive ? palette.primary ?? palette.text : "none"
2541
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2542
+ 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);
2543
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2544
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2545
+ if (configChild?.onClick) {
2546
+ const onClick = configChild.onClick;
2547
+ childG.on("click", () => onClick());
2548
+ }
2549
+ cx += child.width + 4;
2550
+ }
2551
+ }
2552
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2553
+ if (configCtrl?.onClick) {
2554
+ const onClick = configCtrl.onClick;
2555
+ g.on("click", () => onClick());
2556
+ }
2557
+ }
2558
+ var init_legend_d3 = __esm({
2559
+ "src/utils/legend-d3.ts"() {
2560
+ "use strict";
2561
+ init_legend_constants();
2562
+ init_legend_layout();
2563
+ init_color_utils();
2564
+ init_fonts();
2565
+ }
2566
+ });
2567
+
1904
2568
  // src/utils/title-constants.ts
1905
2569
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1906
2570
  var init_title_constants = __esm({
@@ -2946,7 +3610,8 @@ function parseSequenceDgmo(content) {
2946
3610
  if (top.block.type === "if") {
2947
3611
  const branch = {
2948
3612
  label: elseIfMatch[1].trim(),
2949
- children: []
3613
+ children: [],
3614
+ lineNumber
2950
3615
  };
2951
3616
  if (!top.block.elseIfBranches) top.block.elseIfBranches = [];
2952
3617
  top.block.elseIfBranches.push(branch);
@@ -2969,6 +3634,7 @@ function parseSequenceDgmo(content) {
2969
3634
  if (top.block.type === "if") {
2970
3635
  top.inElse = true;
2971
3636
  top.activeElseIfBranch = void 0;
3637
+ top.block.elseLineNumber = lineNumber;
2972
3638
  }
2973
3639
  }
2974
3640
  continue;
@@ -4830,10 +5496,10 @@ var init_chart = __esm({
4830
5496
  function esc(s) {
4831
5497
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4832
5498
  }
4833
- function pillWidth(name) {
5499
+ function pillWidth2(name) {
4834
5500
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4835
5501
  }
4836
- function entriesWidth(entries) {
5502
+ function entriesWidth2(entries) {
4837
5503
  let w = 0;
4838
5504
  for (const e of entries) {
4839
5505
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4841,9 +5507,9 @@ function entriesWidth(entries) {
4841
5507
  return w;
4842
5508
  }
4843
5509
  function groupTotalWidth(name, entries, isActive) {
4844
- const pw = pillWidth(name);
5510
+ const pw = pillWidth2(name);
4845
5511
  if (!isActive) return pw;
4846
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5512
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4847
5513
  }
4848
5514
  function renderLegendSvg(groups, options) {
4849
5515
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4851,7 +5517,7 @@ function renderLegendSvg(groups, options) {
4851
5517
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4852
5518
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4853
5519
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4854
- const pw = pillWidth(g.name);
5520
+ const pw = pillWidth2(g.name);
4855
5521
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4856
5522
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4857
5523
  });
@@ -4905,6 +5571,19 @@ function renderLegendSvg(groups, options) {
4905
5571
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4906
5572
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4907
5573
  }
5574
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5575
+ return renderLegendSvg(config.groups, {
5576
+ palette: {
5577
+ bg: palette.bg,
5578
+ surface: palette.surface,
5579
+ text: palette.text,
5580
+ textMuted: palette.textMuted
5581
+ },
5582
+ isDark: palette.isDark,
5583
+ containerWidth,
5584
+ activeGroup: state.activeGroup
5585
+ });
5586
+ }
4908
5587
  var init_legend_svg = __esm({
4909
5588
  "src/utils/legend-svg.ts"() {
4910
5589
  "use strict";
@@ -5349,7 +6028,8 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5349
6028
  }
5350
6029
  const { textColor, axisLineColor, gridOpacity, colors, titleConfig } = buildChartCommons(parsed, palette, isDark);
5351
6030
  if (parsed.type === "sankey") {
5352
- return buildSankeyOption(parsed, textColor, colors, titleConfig);
6031
+ const bg = isDark ? palette.surface : palette.bg;
6032
+ return buildSankeyOption(parsed, textColor, colors, bg, titleConfig);
5353
6033
  }
5354
6034
  if (parsed.type === "chord") {
5355
6035
  const bg = isDark ? palette.surface : palette.bg;
@@ -5392,7 +6072,7 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5392
6072
  titleConfig
5393
6073
  );
5394
6074
  }
5395
- function buildSankeyOption(parsed, textColor, colors, titleConfig) {
6075
+ function buildSankeyOption(parsed, textColor, colors, bg, titleConfig) {
5396
6076
  const nodeSet = /* @__PURE__ */ new Set();
5397
6077
  if (parsed.links) {
5398
6078
  for (const link of parsed.links) {
@@ -5400,12 +6080,15 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5400
6080
  nodeSet.add(link.target);
5401
6081
  }
5402
6082
  }
5403
- const nodes = Array.from(nodeSet).map((name, index) => ({
5404
- name,
5405
- itemStyle: {
5406
- color: parsed.nodeColors?.[name] ?? colors[index % colors.length]
5407
- }
5408
- }));
6083
+ const tintNode = (c) => mix(c, bg, 75);
6084
+ const tintLink = (c) => mix(c, bg, 45);
6085
+ const nodeColorMap = /* @__PURE__ */ new Map();
6086
+ const nodes = Array.from(nodeSet).map((name, index) => {
6087
+ const raw = parsed.nodeColors?.[name] ?? colors[index % colors.length];
6088
+ const tinted = tintNode(raw);
6089
+ nodeColorMap.set(name, tintLink(raw));
6090
+ return { name, itemStyle: { color: tinted } };
6091
+ });
5409
6092
  return {
5410
6093
  ...CHART_BASE,
5411
6094
  title: titleConfig,
@@ -5428,11 +6111,13 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5428
6111
  source: link.source,
5429
6112
  target: link.target,
5430
6113
  value: link.value,
5431
- ...link.color && { lineStyle: { color: link.color } }
6114
+ lineStyle: {
6115
+ color: link.color ? tintLink(link.color) : nodeColorMap.get(link.source)
6116
+ }
5432
6117
  })),
5433
6118
  lineStyle: {
5434
- color: "gradient",
5435
- curveness: 0.5
6119
+ curveness: 0.5,
6120
+ opacity: 0.6
5436
6121
  },
5437
6122
  label: {
5438
6123
  color: textColor,
@@ -5689,16 +6374,6 @@ function getExtendedChartLegendGroups(parsed, colors) {
5689
6374
  }
5690
6375
  return [];
5691
6376
  }
5692
- function rectsOverlap(a, b) {
5693
- return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
5694
- }
5695
- function rectCircleOverlap(rect, circle) {
5696
- const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
5697
- const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
5698
- const dx = nearestX - circle.cx;
5699
- const dy = nearestY - circle.cy;
5700
- return dx * dx + dy * dy < circle.r * circle.r;
5701
- }
5702
6377
  function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize, bg) {
5703
6378
  const labelHeight = fontSize + 4;
5704
6379
  const stepSize = labelHeight + 2;
@@ -6075,12 +6750,17 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6075
6750
  maxValue = Math.max(maxValue, value);
6076
6751
  });
6077
6752
  });
6753
+ const CHAR_WIDTH7 = 7;
6754
+ const ESTIMATED_CHART_WIDTH = 900;
6755
+ const longestCol = Math.max(...columns.map((c) => c.length), 0);
6756
+ const slotWidth = columns.length > 0 ? ESTIMATED_CHART_WIDTH / columns.length : Infinity;
6757
+ const needsRotation = longestCol * CHAR_WIDTH7 > slotWidth * 0.85;
6078
6758
  return {
6079
6759
  ...CHART_BASE,
6080
6760
  title: titleConfig,
6081
6761
  grid: {
6082
6762
  left: "3%",
6083
- right: "10%",
6763
+ right: "3%",
6084
6764
  bottom: "3%",
6085
6765
  top: parsed.title ? "15%" : "5%",
6086
6766
  containLabel: true
@@ -6088,6 +6768,7 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6088
6768
  xAxis: {
6089
6769
  type: "category",
6090
6770
  data: columns,
6771
+ position: "top",
6091
6772
  splitArea: {
6092
6773
  show: true
6093
6774
  },
@@ -6096,12 +6777,19 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6096
6777
  },
6097
6778
  axisLabel: {
6098
6779
  color: textColor,
6099
- fontSize: 16
6780
+ fontSize: 12,
6781
+ interval: 0,
6782
+ ...needsRotation && {
6783
+ rotate: -45,
6784
+ width: 200,
6785
+ overflow: "none"
6786
+ }
6100
6787
  }
6101
6788
  },
6102
6789
  yAxis: {
6103
6790
  type: "category",
6104
6791
  data: rowLabels,
6792
+ inverse: true,
6105
6793
  splitArea: {
6106
6794
  show: true
6107
6795
  },
@@ -6110,16 +6798,14 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6110
6798
  },
6111
6799
  axisLabel: {
6112
6800
  color: textColor,
6113
- fontSize: 16
6801
+ fontSize: 12,
6802
+ interval: 0
6114
6803
  }
6115
6804
  },
6116
6805
  visualMap: {
6806
+ show: false,
6117
6807
  min: minValue,
6118
6808
  max: maxValue,
6119
- calculable: true,
6120
- orient: "vertical",
6121
- right: "2%",
6122
- top: "center",
6123
6809
  inRange: {
6124
6810
  color: [
6125
6811
  mix(palette.primary, bg, 30),
@@ -6127,9 +6813,6 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6127
6813
  mix(palette.colors.yellow, bg, 30),
6128
6814
  mix(palette.colors.orange, bg, 30)
6129
6815
  ]
6130
- },
6131
- textStyle: {
6132
- color: textColor
6133
6816
  }
6134
6817
  },
6135
6818
  series: [
@@ -6147,9 +6830,8 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6147
6830
  fontWeight: "bold"
6148
6831
  },
6149
6832
  emphasis: {
6150
- ...EMPHASIS_SELF
6151
- },
6152
- blur: BLUR_DIM
6833
+ disabled: true
6834
+ }
6153
6835
  }
6154
6836
  ]
6155
6837
  };
@@ -7003,6 +7685,7 @@ var init_echarts = __esm({
7003
7685
  init_fonts();
7004
7686
  init_branding();
7005
7687
  init_legend_svg();
7688
+ init_label_layout();
7006
7689
  init_palettes();
7007
7690
  init_color_utils();
7008
7691
  init_chart();
@@ -8357,7 +9040,12 @@ __export(parser_exports7, {
8357
9040
  function parseArrowLine(trimmed, palette) {
8358
9041
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8359
9042
  if (bareMatch) {
8360
- return { target: bareMatch[1].trim() };
9043
+ const rawTarget = bareMatch[1].trim();
9044
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
9045
+ return {
9046
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
9047
+ targetIsGroup: !!groupMatch
9048
+ };
8361
9049
  }
8362
9050
  const arrowMatch = trimmed.match(ARROW_RE);
8363
9051
  if (arrowMatch) {
@@ -8366,8 +9054,14 @@ function parseArrowLine(trimmed, palette) {
8366
9054
  if (label && !color) {
8367
9055
  color = inferArrowColor(label);
8368
9056
  }
8369
- const target = arrowMatch[3].trim();
8370
- return { label, color, target };
9057
+ const rawTarget = arrowMatch[3].trim();
9058
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
9059
+ return {
9060
+ label,
9061
+ color,
9062
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
9063
+ targetIsGroup: !!groupMatch
9064
+ };
8371
9065
  }
8372
9066
  return null;
8373
9067
  }
@@ -8429,6 +9123,7 @@ function parseSitemap(content, palette) {
8429
9123
  const aliasMap = /* @__PURE__ */ new Map();
8430
9124
  const indentStack = [];
8431
9125
  const labelToNode = /* @__PURE__ */ new Map();
9126
+ const labelToContainer = /* @__PURE__ */ new Map();
8432
9127
  const deferredArrows = [];
8433
9128
  for (let i = 0; i < lines.length; i++) {
8434
9129
  const line10 = lines[i];
@@ -8530,6 +9225,7 @@ function parseSitemap(content, palette) {
8530
9225
  deferredArrows.push({
8531
9226
  sourceNode: source,
8532
9227
  targetLabel: arrowInfo.target,
9228
+ targetIsGroup: arrowInfo.targetIsGroup,
8533
9229
  label: arrowInfo.label,
8534
9230
  color: arrowInfo.color,
8535
9231
  lineNumber
@@ -8563,6 +9259,7 @@ function parseSitemap(content, palette) {
8563
9259
  color
8564
9260
  };
8565
9261
  attachNode2(node, indent, indentStack, result);
9262
+ labelToContainer.set(label.toLowerCase(), node);
8566
9263
  } else if (metadataMatch && indentStack.length > 0) {
8567
9264
  const rawKey = metadataMatch[1].trim().toLowerCase();
8568
9265
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8603,22 +9300,41 @@ function parseSitemap(content, palette) {
8603
9300
  }
8604
9301
  for (const arrow of deferredArrows) {
8605
9302
  const targetKey = arrow.targetLabel.toLowerCase();
8606
- const targetNode = labelToNode.get(targetKey);
8607
- if (!targetNode) {
8608
- const allLabels = Array.from(labelToNode.keys());
8609
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8610
- const hint = suggest(targetKey, allLabels);
8611
- if (hint) msg += `. ${hint}`;
8612
- pushError(arrow.lineNumber, msg);
8613
- continue;
9303
+ if (arrow.targetIsGroup) {
9304
+ const targetContainer = labelToContainer.get(targetKey);
9305
+ if (!targetContainer) {
9306
+ const allLabels = Array.from(labelToContainer.keys());
9307
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9308
+ const hint = suggest(targetKey, allLabels);
9309
+ if (hint) msg += `. ${hint}`;
9310
+ pushError(arrow.lineNumber, msg);
9311
+ continue;
9312
+ }
9313
+ result.edges.push({
9314
+ sourceId: arrow.sourceNode.id,
9315
+ targetId: targetContainer.id,
9316
+ label: arrow.label,
9317
+ color: arrow.color,
9318
+ lineNumber: arrow.lineNumber
9319
+ });
9320
+ } else {
9321
+ const targetNode = labelToNode.get(targetKey);
9322
+ if (!targetNode) {
9323
+ const allLabels = Array.from(labelToNode.keys());
9324
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9325
+ const hint = suggest(targetKey, allLabels);
9326
+ if (hint) msg += `. ${hint}`;
9327
+ pushError(arrow.lineNumber, msg);
9328
+ continue;
9329
+ }
9330
+ result.edges.push({
9331
+ sourceId: arrow.sourceNode.id,
9332
+ targetId: targetNode.id,
9333
+ label: arrow.label,
9334
+ color: arrow.color,
9335
+ lineNumber: arrow.lineNumber
9336
+ });
8614
9337
  }
8615
- result.edges.push({
8616
- sourceId: arrow.sourceNode.id,
8617
- targetId: targetNode.id,
8618
- label: arrow.label,
8619
- color: arrow.color,
8620
- lineNumber: arrow.lineNumber
8621
- });
8622
9338
  }
8623
9339
  if (result.tagGroups.length > 0) {
8624
9340
  const allNodes = [];
@@ -10332,6 +11048,7 @@ function parseBoxesAndLines(content) {
10332
11048
  const nodeLabels = /* @__PURE__ */ new Set();
10333
11049
  const groupLabels = /* @__PURE__ */ new Set();
10334
11050
  let lastNodeLabel = null;
11051
+ let lastSourceIsGroup = false;
10335
11052
  const groupStack = [];
10336
11053
  let contentStarted = false;
10337
11054
  let currentTagGroup = null;
@@ -10570,6 +11287,8 @@ function parseBoxesAndLines(content) {
10570
11287
  };
10571
11288
  groupLabels.add(label);
10572
11289
  groupStack.push({ group, indent, depth: currentDepth });
11290
+ lastNodeLabel = label;
11291
+ lastSourceIsGroup = true;
10573
11292
  continue;
10574
11293
  }
10575
11294
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10587,7 +11306,8 @@ function parseBoxesAndLines(content) {
10587
11306
  );
10588
11307
  continue;
10589
11308
  }
10590
- edgeText = `${lastNodeLabel} ${trimmed}`;
11309
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11310
+ edgeText = `${sourcePrefix} ${trimmed}`;
10591
11311
  }
10592
11312
  const edge = parseEdgeLine(
10593
11313
  edgeText,
@@ -10610,6 +11330,7 @@ function parseBoxesAndLines(content) {
10610
11330
  continue;
10611
11331
  }
10612
11332
  lastNodeLabel = node.label;
11333
+ lastSourceIsGroup = false;
10613
11334
  const gs = currentGroupState();
10614
11335
  const isGroupChild = gs && indent > gs.indent;
10615
11336
  if (nodeLabels.has(node.label)) {
@@ -10637,14 +11358,42 @@ function parseBoxesAndLines(content) {
10637
11358
  const gs = groupStack.pop();
10638
11359
  result.groups.push(gs.group);
10639
11360
  }
11361
+ const validEdges = [];
10640
11362
  for (const edge of result.edges) {
10641
- if (!edge.source.startsWith("__group_")) {
11363
+ let valid = true;
11364
+ if (edge.source.startsWith("__group_")) {
11365
+ const label = edge.source.slice("__group_".length);
11366
+ const found = [...groupLabels].some(
11367
+ (g) => g.toLowerCase() === label.toLowerCase()
11368
+ );
11369
+ if (!found) {
11370
+ result.diagnostics.push(
11371
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11372
+ );
11373
+ valid = false;
11374
+ }
11375
+ } else {
10642
11376
  ensureNode(edge.source, edge.lineNumber);
10643
11377
  }
10644
- if (!edge.target.startsWith("__group_")) {
11378
+ if (edge.target.startsWith("__group_")) {
11379
+ const label = edge.target.slice("__group_".length);
11380
+ const found = [...groupLabels].some(
11381
+ (g) => g.toLowerCase() === label.toLowerCase()
11382
+ );
11383
+ if (!found) {
11384
+ result.diagnostics.push(
11385
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11386
+ );
11387
+ valid = false;
11388
+ }
11389
+ } else {
10645
11390
  ensureNode(edge.target, edge.lineNumber);
10646
11391
  }
11392
+ if (valid) {
11393
+ validEdges.push(edge);
11394
+ }
10647
11395
  }
11396
+ result.edges = validEdges;
10648
11397
  if (result.tagGroups.length > 0) {
10649
11398
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10650
11399
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10673,10 +11422,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10673
11422
  description
10674
11423
  };
10675
11424
  }
11425
+ function resolveEndpoint(name) {
11426
+ const m = name.match(/^\[(.+)\]$/);
11427
+ return m ? groupId2(m[1].trim()) : name;
11428
+ }
10676
11429
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10677
11430
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10678
11431
  if (biLabeledMatch) {
10679
- const source2 = biLabeledMatch[1].trim();
11432
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10680
11433
  const label = biLabeledMatch[2].trim();
10681
11434
  let rest2 = biLabeledMatch[3].trim();
10682
11435
  let metadata2 = {};
@@ -10697,7 +11450,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10697
11450
  }
10698
11451
  return {
10699
11452
  source: source2,
10700
- target: rest2,
11453
+ target: resolveEndpoint(rest2),
10701
11454
  label: label || void 0,
10702
11455
  bidirectional: true,
10703
11456
  lineNumber: lineNum,
@@ -10706,7 +11459,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10706
11459
  }
10707
11460
  const biIdx = trimmed.indexOf("<->");
10708
11461
  if (biIdx >= 0) {
10709
- const source2 = trimmed.slice(0, biIdx).trim();
11462
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10710
11463
  let rest2 = trimmed.slice(biIdx + 3).trim();
10711
11464
  let metadata2 = {};
10712
11465
  const pipeIdx2 = rest2.indexOf("|");
@@ -10726,7 +11479,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10726
11479
  }
10727
11480
  return {
10728
11481
  source: source2,
10729
- target: rest2,
11482
+ target: resolveEndpoint(rest2),
10730
11483
  bidirectional: true,
10731
11484
  lineNumber: lineNum,
10732
11485
  metadata: metadata2
@@ -10734,7 +11487,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10734
11487
  }
10735
11488
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10736
11489
  if (labeledMatch) {
10737
- const source2 = labeledMatch[1].trim();
11490
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10738
11491
  const label = labeledMatch[2].trim();
10739
11492
  let rest2 = labeledMatch[3].trim();
10740
11493
  if (label) {
@@ -10756,7 +11509,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10756
11509
  }
10757
11510
  return {
10758
11511
  source: source2,
10759
- target: rest2,
11512
+ target: resolveEndpoint(rest2),
10760
11513
  label,
10761
11514
  bidirectional: false,
10762
11515
  lineNumber: lineNum,
@@ -10766,7 +11519,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10766
11519
  }
10767
11520
  const arrowIdx = trimmed.indexOf("->");
10768
11521
  if (arrowIdx < 0) return null;
10769
- const source = trimmed.slice(0, arrowIdx).trim();
11522
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10770
11523
  let rest = trimmed.slice(arrowIdx + 2).trim();
10771
11524
  if (!source || !rest) {
10772
11525
  diagnostics.push(
@@ -10787,7 +11540,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10787
11540
  }
10788
11541
  return {
10789
11542
  source,
10790
- target: rest,
11543
+ target: resolveEndpoint(rest),
10791
11544
  bidirectional: false,
10792
11545
  lineNumber: lineNum,
10793
11546
  metadata
@@ -11102,14 +11855,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11102
11855
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11103
11856
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11104
11857
  if (visibleEntries.length === 0) continue;
11105
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11106
- const minPillWidth = pillWidth2;
11107
- let entriesWidth2 = 0;
11858
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11859
+ const minPillWidth = pillWidth3;
11860
+ let entriesWidth3 = 0;
11108
11861
  for (const entry of visibleEntries) {
11109
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11862
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11110
11863
  }
11111
11864
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11112
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11865
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11113
11866
  groups.push({
11114
11867
  name: group.name,
11115
11868
  alias: group.alias,
@@ -11119,7 +11872,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11119
11872
  })),
11120
11873
  x: 0,
11121
11874
  y: 0,
11122
- width: capsuleWidth,
11875
+ width: capsuleWidth2,
11123
11876
  height: LEGEND_HEIGHT2,
11124
11877
  minifiedWidth: minPillWidth,
11125
11878
  minifiedHeight: LEGEND_HEIGHT2
@@ -12060,66 +12813,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12060
12813
  }
12061
12814
  }
12062
12815
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12063
- const visibleGroups = layout.legend.filter((group) => {
12064
- if (legendOnly) return true;
12065
- if (activeTagGroup == null) return true;
12066
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12067
- });
12068
- let fixedPositions;
12069
- if (fixedLegend && visibleGroups.length > 0) {
12070
- fixedPositions = /* @__PURE__ */ new Map();
12071
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12072
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12073
- let cx = (width - totalW) / 2;
12074
- for (const g of visibleGroups) {
12075
- fixedPositions.set(g.name, cx);
12076
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12077
- }
12078
- }
12079
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12080
- const legendParent = legendParentBase;
12081
- if (fixedLegend && activeTagGroup) {
12082
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12816
+ const groups = layout.legend.map((g) => ({
12817
+ name: g.name,
12818
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12819
+ }));
12820
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12821
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12822
+ let legendHandle;
12823
+ if (legendOnly) {
12824
+ for (const lg of layout.legend) {
12825
+ const singleConfig = {
12826
+ groups: [
12827
+ {
12828
+ name: lg.name,
12829
+ entries: lg.entries.map((e) => ({
12830
+ value: e.value,
12831
+ color: e.color
12832
+ }))
12833
+ }
12834
+ ],
12835
+ position: { placement: "top-center", titleRelation: "below-title" },
12836
+ mode: "fixed"
12837
+ };
12838
+ const singleState = { activeGroup: lg.name };
12839
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12840
+ renderLegendD3(
12841
+ groupG,
12842
+ singleConfig,
12843
+ singleState,
12844
+ palette,
12845
+ isDark,
12846
+ void 0,
12847
+ lg.width
12848
+ );
12849
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12850
+ }
12851
+ legendHandle = null;
12852
+ } else {
12853
+ const legendConfig = {
12854
+ groups,
12855
+ position: { placement: "top-center", titleRelation: "below-title" },
12856
+ mode: "fixed",
12857
+ capsulePillAddonWidth: eyeAddonWidth
12858
+ };
12859
+ const legendState = { activeGroup: activeTagGroup ?? null };
12860
+ legendHandle = renderLegendD3(
12861
+ legendParentBase,
12862
+ legendConfig,
12863
+ legendState,
12864
+ palette,
12865
+ isDark,
12866
+ void 0,
12867
+ fixedLegend ? width : layout.width
12868
+ );
12869
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12083
12870
  }
12084
- for (const group of visibleGroups) {
12085
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12086
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12087
- const pillLabel = group.name;
12088
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12089
- const gX = fixedPositions?.get(group.name) ?? group.x;
12090
- const gY = fixedPositions ? 0 : group.y;
12091
- const gEl = legendParent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "org-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", legendOnly ? "default" : "pointer");
12092
- if (isActive) {
12093
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12094
- }
12095
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12096
- const pillYOff = LEGEND_CAPSULE_PAD;
12097
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12098
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
12099
- if (isActive) {
12100
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
12101
- }
12102
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
12103
- if (isActive && fixedLegend) {
12104
- const groupKey = group.name.toLowerCase();
12871
+ if (fixedLegend && legendHandle) {
12872
+ const computedLayout = legendHandle.getLayout();
12873
+ if (computedLayout.activeCapsule?.addonX != null) {
12874
+ const capsule = computedLayout.activeCapsule;
12875
+ const groupKey = capsule.groupName.toLowerCase();
12105
12876
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12106
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12107
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12108
- const hitPad = 6;
12109
- const eyeG = gEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12110
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12111
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12112
- }
12113
- if (isActive) {
12114
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12115
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12116
- for (const entry of group.entries) {
12117
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12118
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12119
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12120
- const entryLabel = entry.value;
12121
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entryLabel);
12122
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12877
+ const activeGroupEl = legendParentBase.select(
12878
+ `[data-legend-group="${groupKey}"]`
12879
+ );
12880
+ if (!activeGroupEl.empty()) {
12881
+ const eyeX = capsule.addonX;
12882
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12883
+ const hitPad = 6;
12884
+ 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);
12885
+ 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");
12886
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12123
12887
  }
12124
12888
  }
12125
12889
  }
@@ -12153,6 +12917,7 @@ var init_renderer = __esm({
12153
12917
  init_parser4();
12154
12918
  init_layout();
12155
12919
  init_legend_constants();
12920
+ init_legend_d3();
12156
12921
  init_title_constants();
12157
12922
  DIAGRAM_PADDING = 20;
12158
12923
  MAX_SCALE = 3;
@@ -12182,6 +12947,17 @@ __export(layout_exports2, {
12182
12947
  layoutSitemap: () => layoutSitemap
12183
12948
  });
12184
12949
  import dagre from "@dagrejs/dagre";
12950
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12951
+ const dx = tx - cx;
12952
+ const dy = ty - cy;
12953
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12954
+ const hw = w / 2;
12955
+ const hh = h / 2;
12956
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12957
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12958
+ const s = Math.min(sx, sy);
12959
+ return { x: cx + dx * s, y: cy + dy * s };
12960
+ }
12185
12961
  function filterMetadata2(metadata, hiddenAttributes) {
12186
12962
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12187
12963
  const filtered = {};
@@ -12198,7 +12974,10 @@ function computeCardWidth2(label, meta) {
12198
12974
  const lineChars = key.length + 2 + value.length;
12199
12975
  if (lineChars > maxChars) maxChars = lineChars;
12200
12976
  }
12201
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12977
+ return Math.max(
12978
+ MIN_CARD_WIDTH2,
12979
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
12980
+ );
12202
12981
  }
12203
12982
  function computeCardHeight2(meta) {
12204
12983
  const metaCount = Object.keys(meta).length;
@@ -12207,7 +12986,12 @@ function computeCardHeight2(meta) {
12207
12986
  }
12208
12987
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12209
12988
  if (node.color) return node.color;
12210
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
12989
+ return resolveTagColor(
12990
+ node.metadata,
12991
+ tagGroups,
12992
+ activeGroupName,
12993
+ node.isContainer
12994
+ );
12211
12995
  }
12212
12996
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12213
12997
  const groups = [];
@@ -12216,21 +13000,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12216
13000
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12217
13001
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12218
13002
  if (visibleEntries.length === 0) continue;
12219
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12220
- const minPillWidth = pillWidth2;
12221
- let entriesWidth2 = 0;
13003
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
13004
+ const minPillWidth = pillWidth3;
13005
+ let entriesWidth3 = 0;
12222
13006
  for (const entry of visibleEntries) {
12223
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
13007
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12224
13008
  }
12225
13009
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12226
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
13010
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12227
13011
  groups.push({
12228
13012
  name: group.name,
12229
13013
  alias: group.alias,
12230
13014
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12231
13015
  x: 0,
12232
13016
  y: 0,
12233
- width: capsuleWidth,
13017
+ width: capsuleWidth2,
12234
13018
  height: LEGEND_HEIGHT3,
12235
13019
  minifiedWidth: minPillWidth,
12236
13020
  minifiedHeight: LEGEND_HEIGHT3
@@ -12250,10 +13034,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12250
13034
  parentPageId,
12251
13035
  meta,
12252
13036
  fullMeta: { ...node.metadata },
12253
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
13037
+ width: Math.max(
13038
+ MIN_CARD_WIDTH2,
13039
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
13040
+ ),
12254
13041
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12255
13042
  });
12256
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
13043
+ flattenNodes(
13044
+ node.children,
13045
+ node.id,
13046
+ parentPageId,
13047
+ hiddenCounts,
13048
+ hiddenAttributes,
13049
+ result
13050
+ );
12257
13051
  } else {
12258
13052
  result.push({
12259
13053
  sitemapNode: node,
@@ -12265,14 +13059,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12265
13059
  height: computeCardHeight2(meta)
12266
13060
  });
12267
13061
  if (node.children.length > 0) {
12268
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
13062
+ flattenNodes(
13063
+ node.children,
13064
+ parentContainerId,
13065
+ node.id,
13066
+ hiddenCounts,
13067
+ hiddenAttributes,
13068
+ result
13069
+ );
12269
13070
  }
12270
13071
  }
12271
13072
  }
12272
13073
  }
12273
13074
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12274
13075
  if (parsed.roots.length === 0) {
12275
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
13076
+ return {
13077
+ nodes: [],
13078
+ edges: [],
13079
+ containers: [],
13080
+ legend: [],
13081
+ width: 0,
13082
+ height: 0
13083
+ };
12276
13084
  }
12277
13085
  const allNodes = [];
12278
13086
  const collect = (node) => {
@@ -12280,9 +13088,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12280
13088
  for (const child of node.children) collect(child);
12281
13089
  };
12282
13090
  for (const root of parsed.roots) collect(root);
12283
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
13091
+ injectDefaultTagMetadata(
13092
+ allNodes,
13093
+ parsed.tagGroups,
13094
+ (e) => e.isContainer
13095
+ );
12284
13096
  const flatNodes = [];
12285
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
13097
+ flattenNodes(
13098
+ parsed.roots,
13099
+ null,
13100
+ null,
13101
+ hiddenCounts,
13102
+ hiddenAttributes,
13103
+ flatNodes
13104
+ );
12286
13105
  const nodeMap = /* @__PURE__ */ new Map();
12287
13106
  for (const flat of flatNodes) {
12288
13107
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12344,14 +13163,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12344
13163
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12345
13164
  }
12346
13165
  }
13166
+ const expandedContainerIds = /* @__PURE__ */ new Set();
13167
+ for (const cid of containerIds) {
13168
+ if (!collapsedContainerIds.has(cid)) {
13169
+ expandedContainerIds.add(cid);
13170
+ }
13171
+ }
13172
+ const deferredEdgeIndices = [];
12347
13173
  for (let i = 0; i < parsed.edges.length; i++) {
12348
13174
  const edge = parsed.edges[i];
12349
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12350
- g.setEdge(edge.sourceId, edge.targetId, {
13175
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
13176
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
13177
+ deferredEdgeIndices.push(i);
13178
+ continue;
13179
+ }
13180
+ g.setEdge(
13181
+ edge.sourceId,
13182
+ edge.targetId,
13183
+ {
12351
13184
  label: edge.label ?? "",
12352
13185
  minlen: 1
12353
- }, `e${i}`);
12354
- }
13186
+ },
13187
+ `e${i}`
13188
+ );
12355
13189
  }
12356
13190
  dagre.layout(g);
12357
13191
  const layoutNodes = [];
@@ -12419,19 +13253,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12419
13253
  });
12420
13254
  }
12421
13255
  }
13256
+ const deferredSet = new Set(deferredEdgeIndices);
12422
13257
  const layoutEdges = [];
12423
13258
  for (let i = 0; i < parsed.edges.length; i++) {
12424
13259
  const edge = parsed.edges[i];
12425
13260
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12426
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12427
- if (!edgeData) continue;
13261
+ let points;
13262
+ if (deferredSet.has(i)) {
13263
+ const srcNode = g.node(edge.sourceId);
13264
+ const tgtNode = g.node(edge.targetId);
13265
+ if (!srcNode || !tgtNode) continue;
13266
+ const srcPt = clipToRectBorder(
13267
+ srcNode.x,
13268
+ srcNode.y,
13269
+ srcNode.width,
13270
+ srcNode.height,
13271
+ tgtNode.x,
13272
+ tgtNode.y
13273
+ );
13274
+ const tgtPt = clipToRectBorder(
13275
+ tgtNode.x,
13276
+ tgtNode.y,
13277
+ tgtNode.width,
13278
+ tgtNode.height,
13279
+ srcNode.x,
13280
+ srcNode.y
13281
+ );
13282
+ const midX = (srcPt.x + tgtPt.x) / 2;
13283
+ const midY = (srcPt.y + tgtPt.y) / 2;
13284
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13285
+ } else {
13286
+ const edgeData = g.edge({
13287
+ v: edge.sourceId,
13288
+ w: edge.targetId,
13289
+ name: `e${i}`
13290
+ });
13291
+ if (!edgeData) continue;
13292
+ points = edgeData.points ?? [];
13293
+ }
12428
13294
  layoutEdges.push({
12429
13295
  sourceId: edge.sourceId,
12430
13296
  targetId: edge.targetId,
12431
- points: edgeData.points ?? [],
13297
+ points,
12432
13298
  label: edge.label,
12433
13299
  color: edge.color,
12434
- lineNumber: edge.lineNumber
13300
+ lineNumber: edge.lineNumber,
13301
+ deferred: deferredSet.has(i) || void 0
12435
13302
  });
12436
13303
  }
12437
13304
  {
@@ -12592,7 +13459,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12592
13459
  usedValuesByGroup.set(key, used);
12593
13460
  }
12594
13461
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12595
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13462
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13463
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13464
+ ) : legendGroups;
12596
13465
  const allExpanded = expandAllLegend && activeTagGroup == null;
12597
13466
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12598
13467
  if (visibleGroups.length > 0) {
@@ -12908,7 +13777,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12908
13777
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
12909
13778
  const edgeColor3 = edge.color ?? palette.textMuted;
12910
13779
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
12911
- const pathD = lineGenerator(edge.points);
13780
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13781
+ const pathD = gen(edge.points);
12912
13782
  if (pathD) {
12913
13783
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor3).attr("stroke-width", EDGE_STROKE_WIDTH2).attr("marker-end", `url(#${markerId})`).attr("class", "sitemap-edge");
12914
13784
  }
@@ -12992,64 +13862,51 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12992
13862
  palette,
12993
13863
  isDark,
12994
13864
  activeTagGroup,
12995
- width,
12996
- hiddenAttributes
12997
- );
12998
- }
12999
- }
13000
- function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13001
- if (legendGroups.length === 0) return;
13002
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13003
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13004
- ) : legendGroups;
13005
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13006
- let fixedPositions;
13007
- if (fixedWidth != null && visibleGroups.length > 0) {
13008
- fixedPositions = /* @__PURE__ */ new Map();
13009
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13010
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13011
- let cx = (fixedWidth - totalW) / 2;
13012
- for (const g of visibleGroups) {
13013
- fixedPositions.set(g.name, cx);
13014
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13015
- }
13865
+ width,
13866
+ hiddenAttributes
13867
+ );
13016
13868
  }
13017
- for (const group of visibleGroups) {
13018
- const isActive = activeTagGroup != null;
13019
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13020
- const gX = fixedPositions?.get(group.name) ?? group.x;
13021
- const gY = fixedPositions ? 0 : group.y;
13022
- const legendG = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "sitemap-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13023
- if (isActive) {
13024
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13025
- }
13026
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13027
- const pillYOff = LEGEND_CAPSULE_PAD;
13028
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13029
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
13030
- if (isActive) {
13031
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13032
- }
13033
- legendG.append("text").attr("x", pillXOff + pillW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13034
- if (isActive && fixedWidth != null) {
13035
- const groupKey = group.name.toLowerCase();
13869
+ }
13870
+ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13871
+ if (legendGroups.length === 0) return;
13872
+ const groups = legendGroups.map((g) => ({
13873
+ name: g.name,
13874
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13875
+ }));
13876
+ const isFixedMode = fixedWidth != null;
13877
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13878
+ const legendConfig = {
13879
+ groups,
13880
+ position: { placement: "top-center", titleRelation: "below-title" },
13881
+ mode: "fixed",
13882
+ capsulePillAddonWidth: eyeAddonWidth
13883
+ };
13884
+ const legendState = { activeGroup: activeTagGroup ?? null };
13885
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13886
+ const legendHandle = renderLegendD3(
13887
+ parent,
13888
+ legendConfig,
13889
+ legendState,
13890
+ palette,
13891
+ isDark,
13892
+ void 0,
13893
+ containerWidth
13894
+ );
13895
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13896
+ if (isFixedMode) {
13897
+ const computedLayout = legendHandle.getLayout();
13898
+ if (computedLayout.activeCapsule?.addonX != null) {
13899
+ const capsule = computedLayout.activeCapsule;
13900
+ const groupKey = capsule.groupName.toLowerCase();
13036
13901
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13037
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13038
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13039
- const hitPad = 6;
13040
- const eyeG = legendG.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13041
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13042
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13043
- }
13044
- if (isActive) {
13045
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13046
- let entryX = pillXOff + pillW + 4 + eyeShift;
13047
- for (const entry of group.entries) {
13048
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13049
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13050
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13051
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13052
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13902
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13903
+ if (!activeGroupEl.empty()) {
13904
+ const eyeX = capsule.addonX;
13905
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13906
+ const hitPad = 6;
13907
+ 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);
13908
+ 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");
13909
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13053
13910
  }
13054
13911
  }
13055
13912
  }
@@ -13103,13 +13960,14 @@ async function renderSitemapForExport(content, theme, palette) {
13103
13960
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13104
13961
  return injectBranding2(svgHtml, brandColor);
13105
13962
  }
13106
- var DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
13963
+ var DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator, lineGeneratorLinear;
13107
13964
  var init_renderer2 = __esm({
13108
13965
  "src/sitemap/renderer.ts"() {
13109
13966
  "use strict";
13110
13967
  init_fonts();
13111
13968
  init_color_utils();
13112
13969
  init_legend_constants();
13970
+ init_legend_d3();
13113
13971
  init_title_constants();
13114
13972
  DIAGRAM_PADDING2 = 20;
13115
13973
  MAX_SCALE2 = 3;
@@ -13133,6 +13991,7 @@ var init_renderer2 = __esm({
13133
13991
  COLLAPSE_BAR_HEIGHT2 = 6;
13134
13992
  LEGEND_FIXED_GAP2 = 8;
13135
13993
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
13994
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13136
13995
  }
13137
13996
  });
13138
13997
 
@@ -13395,8 +14254,7 @@ function computeLayout(parsed, _palette) {
13395
14254
  currentX += cl.width + COLUMN_GAP;
13396
14255
  }
13397
14256
  const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING3;
13398
- const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
13399
- const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3 + legendSpace;
14257
+ const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3;
13400
14258
  return { columns: columnLayouts, totalWidth, totalHeight };
13401
14259
  }
13402
14260
  function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exportDims, activeTagGroup) {
@@ -13409,54 +14267,25 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13409
14267
  svg.append("text").attr("class", "chart-title").attr("data-line-number", parsed.titleLineNumber ?? 0).attr("x", DIAGRAM_PADDING3).attr("y", DIAGRAM_PADDING3 + TITLE_FONT_SIZE).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).text(parsed.title);
13410
14268
  }
13411
14269
  if (parsed.tagGroups.length > 0) {
13412
- const legendY = height - LEGEND_HEIGHT;
13413
- let legendX = DIAGRAM_PADDING3;
13414
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13415
- const capsulePad = LEGEND_CAPSULE_PAD;
13416
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13417
- if (activeTagGroup) {
13418
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13419
- }
13420
- for (const group of parsed.tagGroups) {
13421
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13422
- if (activeTagGroup != null && !isActive) continue;
13423
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13424
- const pillWidth2 = pillTextWidth + 16;
13425
- let capsuleContentWidth = pillWidth2;
13426
- if (isActive) {
13427
- capsuleContentWidth += 4;
13428
- for (const entry of group.entries) {
13429
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13430
- }
13431
- }
13432
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13433
- if (isActive) {
13434
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13435
- }
13436
- const pillX = legendX + (isActive ? capsulePad : 0);
13437
- const pillBg = isActive ? palette.bg : groupBg;
13438
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", pillBg).attr("class", "kanban-legend-group").attr("data-legend-group", group.name.toLowerCase());
13439
- if (isActive) {
13440
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13441
- }
13442
- legendContainer.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13443
- if (isActive) {
13444
- let entryX = pillX + pillWidth2 + 4;
13445
- for (const entry of group.entries) {
13446
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13447
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13448
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13449
- entryG.append("text").attr("x", entryTextX).attr(
13450
- "y",
13451
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13452
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13453
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13454
- }
13455
- legendX += capsuleWidth + 12;
13456
- } else {
13457
- legendX += pillWidth2 + 12;
13458
- }
13459
- }
14270
+ const titleTextWidth = parsed.title ? measureLegendText(parsed.title, TITLE_FONT_SIZE) + 16 : 0;
14271
+ const legendX = DIAGRAM_PADDING3 + titleTextWidth;
14272
+ const legendY = DIAGRAM_PADDING3 + (TITLE_FONT_SIZE - LEGEND_HEIGHT) / 2;
14273
+ const legendConfig = {
14274
+ groups: parsed.tagGroups,
14275
+ position: { placement: "top-center", titleRelation: "below-title" },
14276
+ mode: exportDims ? "inline" : "fixed"
14277
+ };
14278
+ const legendState = { activeGroup: activeTagGroup ?? null };
14279
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${legendX},${legendY})`);
14280
+ renderLegendD3(
14281
+ legendG,
14282
+ legendConfig,
14283
+ legendState,
14284
+ palette,
14285
+ isDark,
14286
+ void 0,
14287
+ width - legendX - DIAGRAM_PADDING3
14288
+ );
13460
14289
  }
13461
14290
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13462
14291
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13551,6 +14380,7 @@ var init_renderer3 = __esm({
13551
14380
  init_parser5();
13552
14381
  init_mutations();
13553
14382
  init_legend_constants();
14383
+ init_legend_d3();
13554
14384
  init_title_constants();
13555
14385
  DIAGRAM_PADDING3 = 20;
13556
14386
  COLUMN_GAP = 16;
@@ -13746,14 +14576,9 @@ function collectClassTypes(parsed) {
13746
14576
  if (c.color) continue;
13747
14577
  present.add(c.modifier ?? "class");
13748
14578
  }
13749
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13750
- }
13751
- function legendEntriesWidth(entries) {
13752
- let w = 0;
13753
- for (const e of entries) {
13754
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13755
- }
13756
- return w;
14579
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14580
+ (k) => CLASS_TYPE_MAP[k]
14581
+ );
13757
14582
  }
13758
14583
  function classTypeKey(modifier) {
13759
14584
  return modifier ?? "class";
@@ -13822,7 +14647,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13822
14647
  defs.append("marker").attr("id", "cd-arrow-depend").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13823
14648
  defs.append("marker").attr("id", "cd-arrow-assoc").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13824
14649
  if (parsed.title) {
13825
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
14650
+ 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(
14651
+ "cursor",
14652
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14653
+ ).text(parsed.title);
13826
14654
  if (parsed.titleLineNumber) {
13827
14655
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13828
14656
  if (onClickItem) {
@@ -13836,32 +14664,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13836
14664
  }
13837
14665
  const isLegendExpanded = legendActive !== false;
13838
14666
  if (hasLegend) {
13839
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13840
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13841
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13842
- const entriesW = legendEntriesWidth(legendEntries);
13843
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13844
- const legendX = (width - totalW) / 2;
13845
- const legendY = titleHeight;
13846
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13847
- if (isLegendExpanded) {
13848
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13849
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
13850
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13851
- legendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13852
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13853
- for (const entry of legendEntries) {
13854
- const color = palette.colors[entry.colorKey];
13855
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13856
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13857
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13858
- entryG.append("text").attr("x", entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.label);
13859
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14667
+ const legendGroups = [
14668
+ {
14669
+ name: LEGEND_GROUP_NAME,
14670
+ entries: legendEntries.map((entry) => ({
14671
+ value: entry.label,
14672
+ color: palette.colors[entry.colorKey]
14673
+ }))
13860
14674
  }
13861
- } else {
13862
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13863
- legendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13864
- }
14675
+ ];
14676
+ const legendConfig = {
14677
+ groups: legendGroups,
14678
+ position: { placement: "top-center", titleRelation: "below-title" },
14679
+ mode: "fixed"
14680
+ };
14681
+ const legendState = {
14682
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14683
+ };
14684
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14685
+ renderLegendD3(
14686
+ legendG,
14687
+ legendConfig,
14688
+ legendState,
14689
+ palette,
14690
+ isDark,
14691
+ void 0,
14692
+ width
14693
+ );
13865
14694
  }
13866
14695
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13867
14696
  for (const edge of layout.edges) {
@@ -13905,7 +14734,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13905
14734
  const colorOff = !!parsed.options?.["no-auto-color"];
13906
14735
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
13907
14736
  const effectiveColor = neutralize ? palette.primary : node.color;
13908
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14737
+ const fill2 = nodeFill3(
14738
+ palette,
14739
+ isDark,
14740
+ node.modifier,
14741
+ effectiveColor,
14742
+ colorOff
14743
+ );
13909
14744
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
13910
14745
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", 3).attr("ry", 3).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", NODE_STROKE_WIDTH3);
13911
14746
  let yPos = -h / 2;
@@ -13974,15 +14809,10 @@ function renderClassDiagramForExport(content, theme, palette) {
13974
14809
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
13975
14810
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
13976
14811
  return runInExportContainer(exportWidth, exportHeight, (container) => {
13977
- renderClassDiagram(
13978
- container,
13979
- parsed,
13980
- layout,
13981
- palette,
13982
- isDark,
13983
- void 0,
13984
- { width: exportWidth, height: exportHeight }
13985
- );
14812
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14813
+ width: exportWidth,
14814
+ height: exportHeight
14815
+ });
13986
14816
  return extractExportSvg(container, theme);
13987
14817
  });
13988
14818
  }
@@ -13993,6 +14823,7 @@ var init_renderer4 = __esm({
13993
14823
  init_fonts();
13994
14824
  init_export_container();
13995
14825
  init_legend_constants();
14826
+ init_legend_d3();
13996
14827
  init_title_constants();
13997
14828
  init_color_utils();
13998
14829
  init_parser2();
@@ -14601,35 +15432,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14601
15432
  }
14602
15433
  }
14603
15434
  if (parsed.tagGroups.length > 0) {
14604
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14605
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14606
- const LEGEND_GAP = 8;
14607
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14608
- if (activeTagGroup) {
14609
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14610
- }
14611
- let legendX = DIAGRAM_PADDING5;
14612
15435
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14613
- for (const group of parsed.tagGroups) {
14614
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14615
- const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
14616
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14617
- legendX += labelWidth;
14618
- for (const entry of group.entries) {
14619
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14620
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14621
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14622
- tmpText.remove();
14623
- const pillW = textW + LEGEND_PILL_PAD * 2;
14624
- pillG.append("rect").attr("x", legendX).attr("y", legendY).attr("width", pillW).attr("height", LEGEND_PILL_H).attr("rx", LEGEND_PILL_RX).attr("ry", LEGEND_PILL_RX).attr(
14625
- "fill",
14626
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14627
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14628
- pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14629
- legendX += pillW + LEGEND_GAP;
14630
- }
14631
- legendX += LEGEND_GROUP_GAP;
14632
- }
15436
+ const legendConfig = {
15437
+ groups: parsed.tagGroups,
15438
+ position: { placement: "top-center", titleRelation: "below-title" },
15439
+ mode: "fixed"
15440
+ };
15441
+ const legendState = { activeGroup: activeTagGroup ?? null };
15442
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15443
+ renderLegendD3(
15444
+ legendG,
15445
+ legendConfig,
15446
+ legendState,
15447
+ palette,
15448
+ isDark,
15449
+ void 0,
15450
+ viewW
15451
+ );
15452
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14633
15453
  }
14634
15454
  if (semanticRoles) {
14635
15455
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14639,55 +15459,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14639
15459
  return false;
14640
15460
  });
14641
15461
  if (presentRoles.length > 0) {
14642
- const measureLabelW = (text, fontSize) => {
14643
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14644
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14645
- dummy.remove();
14646
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14647
- };
14648
- const labelWidths = /* @__PURE__ */ new Map();
14649
- for (const role of presentRoles) {
14650
- labelWidths.set(
14651
- role,
14652
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14653
- );
14654
- }
14655
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14656
- const groupName = "Role";
14657
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14658
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14659
- let totalWidth;
14660
- let entriesWidth2 = 0;
14661
- if (semanticActive) {
14662
- for (const role of presentRoles) {
14663
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14664
- }
14665
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14666
- } else {
14667
- totalWidth = pillWidth2;
14668
- }
14669
- const legendX = (viewW - totalWidth) / 2;
14670
15462
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14671
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14672
- if (semanticActive) {
14673
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14674
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
14675
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
14676
- semanticLegendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14677
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14678
- for (const role of presentRoles) {
14679
- const label = ROLE_LABELS[role];
14680
- const roleColor = palette.colors[ROLE_COLORS[role]];
14681
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14682
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14683
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14684
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(label);
14685
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15463
+ const semanticGroups = [
15464
+ {
15465
+ name: "Role",
15466
+ entries: presentRoles.map((role) => ({
15467
+ value: ROLE_LABELS[role],
15468
+ color: palette.colors[ROLE_COLORS[role]]
15469
+ }))
14686
15470
  }
14687
- } else {
14688
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14689
- semanticLegendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14690
- }
15471
+ ];
15472
+ const legendConfig = {
15473
+ groups: semanticGroups,
15474
+ position: { placement: "top-center", titleRelation: "below-title" },
15475
+ mode: "fixed"
15476
+ };
15477
+ const legendState = {
15478
+ activeGroup: semanticActive ? "Role" : null
15479
+ };
15480
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15481
+ renderLegendD3(
15482
+ legendG,
15483
+ legendConfig,
15484
+ legendState,
15485
+ palette,
15486
+ isDark,
15487
+ void 0,
15488
+ viewW
15489
+ );
15490
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14691
15491
  }
14692
15492
  }
14693
15493
  }
@@ -14730,6 +15530,7 @@ var init_renderer5 = __esm({
14730
15530
  init_palettes();
14731
15531
  init_tag_groups();
14732
15532
  init_legend_constants();
15533
+ init_legend_d3();
14733
15534
  init_title_constants();
14734
15535
  init_parser3();
14735
15536
  init_layout4();
@@ -14754,6 +15555,17 @@ __export(layout_exports5, {
14754
15555
  layoutBoxesAndLines: () => layoutBoxesAndLines
14755
15556
  });
14756
15557
  import dagre4 from "@dagrejs/dagre";
15558
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15559
+ const dx = tx - cx;
15560
+ const dy = ty - cy;
15561
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15562
+ const hw = w / 2;
15563
+ const hh = h / 2;
15564
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15565
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15566
+ const s = Math.min(sx, sy);
15567
+ return { x: cx + dx * s, y: cy + dy * s };
15568
+ }
14757
15569
  function computeNodeSize(_node) {
14758
15570
  const PHI = 1.618;
14759
15571
  const NODE_HEIGHT = 60;
@@ -14906,13 +15718,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14906
15718
  const srcNode = g.node(edge.source);
14907
15719
  const tgtNode = g.node(edge.target);
14908
15720
  if (!srcNode || !tgtNode) continue;
14909
- const midX = (srcNode.x + tgtNode.x) / 2;
14910
- const midY = (srcNode.y + tgtNode.y) / 2;
14911
- points = [
14912
- { x: srcNode.x, y: srcNode.y },
14913
- { x: midX, y: midY },
14914
- { x: tgtNode.x, y: tgtNode.y }
14915
- ];
15721
+ const srcPt = clipToRectBorder2(
15722
+ srcNode.x,
15723
+ srcNode.y,
15724
+ srcNode.width,
15725
+ srcNode.height,
15726
+ tgtNode.x,
15727
+ tgtNode.y
15728
+ );
15729
+ const tgtPt = clipToRectBorder2(
15730
+ tgtNode.x,
15731
+ tgtNode.y,
15732
+ tgtNode.width,
15733
+ tgtNode.height,
15734
+ srcNode.x,
15735
+ srcNode.y
15736
+ );
15737
+ const midX = (srcPt.x + tgtPt.x) / 2;
15738
+ const midY = (srcPt.y + tgtPt.y) / 2;
15739
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
14916
15740
  } else {
14917
15741
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
14918
15742
  points = dagreEdge?.points ?? [];
@@ -14935,7 +15759,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14935
15759
  labelY,
14936
15760
  yOffset: edgeYOffsets[i],
14937
15761
  parallelCount: edgeParallelCounts[i],
14938
- metadata: edge.metadata
15762
+ metadata: edge.metadata,
15763
+ deferred: deferredSet.has(i) || void 0
14939
15764
  });
14940
15765
  }
14941
15766
  let maxX = 0;
@@ -15001,7 +15826,7 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
15001
15826
  const maxTextWidth = nodeWidth - NODE_TEXT_PADDING * 2;
15002
15827
  const lineHeight = 1.3;
15003
15828
  for (let fontSize = NODE_FONT_SIZE; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
15004
- const charWidth2 = fontSize * CHAR_WIDTH_RATIO;
15829
+ const charWidth2 = fontSize * CHAR_WIDTH_RATIO2;
15005
15830
  const maxCharsPerLine = Math.floor(maxTextWidth / charWidth2);
15006
15831
  const maxLines = Math.floor((nodeHeight - 8) / (fontSize * lineHeight));
15007
15832
  if (maxCharsPerLine < 2 || maxLines < 1) continue;
@@ -15053,7 +15878,7 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
15053
15878
  }
15054
15879
  if (hardLines.length <= maxLines) return { lines: hardLines, fontSize };
15055
15880
  }
15056
- const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO;
15881
+ const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO2;
15057
15882
  const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING * 2) / charWidth);
15058
15883
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
15059
15884
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
@@ -15205,18 +16030,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15205
16030
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15206
16031
  edgeGroups.set(i, edgeG);
15207
16032
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15208
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15209
- "d",
15210
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15211
- points
15212
- ) ?? ""
15213
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
16033
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
16034
+ const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15214
16035
  if (le.bidirectional) {
15215
16036
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15216
16037
  path.attr("marker-start", `url(#${revId})`);
15217
16038
  }
15218
16039
  if (le.label && le.labelX != null && le.labelY != null) {
15219
- const lw = le.label.length * EDGE_LABEL_FONT_SIZE4 * CHAR_WIDTH_RATIO;
16040
+ const lw = le.label.length * EDGE_LABEL_FONT_SIZE4 * CHAR_WIDTH_RATIO2;
15220
16041
  labelPositions.push({
15221
16042
  x: le.labelX,
15222
16043
  y: le.labelY + le.yOffset,
@@ -15274,7 +16095,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15274
16095
  const descY = labelY + lineH / 2 + gap + META_FONT_SIZE3 / 2;
15275
16096
  nodeG.append("text").attr("x", 0).attr("y", labelY).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", NODE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).text(node.label);
15276
16097
  const maxChars = Math.floor(
15277
- (ln.width - NODE_TEXT_PADDING * 2) / (META_FONT_SIZE3 * CHAR_WIDTH_RATIO)
16098
+ (ln.width - NODE_TEXT_PADDING * 2) / (META_FONT_SIZE3 * CHAR_WIDTH_RATIO2)
15278
16099
  );
15279
16100
  const desc = node.description.length > maxChars ? node.description.slice(0, maxChars - 1) + "\u2026" : node.description;
15280
16101
  const descEl = nodeG.append("text").attr("x", 0).attr("y", descY).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", META_FONT_SIZE3).attr("fill", palette.textMuted).text(desc);
@@ -15291,50 +16112,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15291
16112
  }
15292
16113
  }
15293
16114
  if (parsed.tagGroups.length > 0) {
15294
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15295
- }
15296
- }
15297
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15298
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15299
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15300
- let totalW = 0;
15301
- for (const tg of parsed.tagGroups) {
15302
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15303
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15304
- if (isActive) {
15305
- totalW += 6;
15306
- for (const entry of tg.entries) {
15307
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15308
- }
15309
- }
15310
- totalW += LEGEND_GROUP_GAP;
15311
- }
15312
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15313
- const legendY = titleOffset + 4;
15314
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15315
- let x = 0;
15316
- for (const tg of parsed.tagGroups) {
15317
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15318
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15319
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15320
- const tagPill = groupG.append("rect").attr("x", x).attr("y", 0).attr("width", nameW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
15321
- if (isActiveGroup) {
15322
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15323
- }
15324
- groupG.append("text").attr("x", x + nameW / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", isActiveGroup ? palette.text : palette.textMuted).attr("pointer-events", "none").text(tg.name);
15325
- x += nameW;
15326
- if (isActiveGroup) {
15327
- x += 6;
15328
- for (const entry of tg.entries) {
15329
- const entryColor = entry.color || palette.textMuted;
15330
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15331
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15332
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15333
- entryG.append("text").attr("x", x + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
15334
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15335
- }
15336
- }
15337
- x += LEGEND_GROUP_GAP;
16115
+ const legendConfig = {
16116
+ groups: parsed.tagGroups,
16117
+ position: { placement: "top-center", titleRelation: "below-title" },
16118
+ mode: "fixed"
16119
+ };
16120
+ const legendState = { activeGroup };
16121
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
16122
+ renderLegendD3(
16123
+ legendG,
16124
+ legendConfig,
16125
+ legendState,
16126
+ palette,
16127
+ isDark,
16128
+ void 0,
16129
+ width
16130
+ );
16131
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15338
16132
  }
15339
16133
  }
15340
16134
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15342,12 +16136,13 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15342
16136
  exportDims: options?.exportDims
15343
16137
  });
15344
16138
  }
15345
- var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
16139
+ 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_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB, lineGeneratorLinear2;
15346
16140
  var init_renderer6 = __esm({
15347
16141
  "src/boxes-and-lines/renderer.ts"() {
15348
16142
  "use strict";
15349
16143
  init_fonts();
15350
16144
  init_legend_constants();
16145
+ init_legend_d3();
15351
16146
  init_title_constants();
15352
16147
  init_color_utils();
15353
16148
  init_tag_groups();
@@ -15362,12 +16157,13 @@ var init_renderer6 = __esm({
15362
16157
  COLLAPSE_BAR_HEIGHT3 = 4;
15363
16158
  ARROWHEAD_W2 = 5;
15364
16159
  ARROWHEAD_H2 = 4;
15365
- CHAR_WIDTH_RATIO = 0.6;
16160
+ CHAR_WIDTH_RATIO2 = 0.6;
15366
16161
  NODE_TEXT_PADDING = 12;
15367
16162
  GROUP_RX = 8;
15368
16163
  GROUP_LABEL_FONT_SIZE = 14;
15369
16164
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15370
16165
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
16166
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15371
16167
  }
15372
16168
  });
15373
16169
 
@@ -15433,7 +16229,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15433
16229
  }
15434
16230
  const nodeGeometry = /* @__PURE__ */ new Map();
15435
16231
  for (const name of g.nodes()) {
15436
- const pos = g.node(name);
16232
+ const pos = gNode(g, name);
15437
16233
  if (pos)
15438
16234
  nodeGeometry.set(name, {
15439
16235
  y: pos.y,
@@ -15443,14 +16239,14 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15443
16239
  }
15444
16240
  const rankMap = /* @__PURE__ */ new Map();
15445
16241
  for (const name of g.nodes()) {
15446
- const pos = g.node(name);
16242
+ const pos = gNode(g, name);
15447
16243
  if (!pos) continue;
15448
16244
  const rankY = Math.round(pos.y);
15449
16245
  if (!rankMap.has(rankY)) rankMap.set(rankY, []);
15450
16246
  rankMap.get(rankY).push(name);
15451
16247
  }
15452
16248
  for (const [, rankNodes] of rankMap) {
15453
- rankNodes.sort((a, b) => g.node(a).x - g.node(b).x);
16249
+ rankNodes.sort((a, b) => gNode(g, a).x - gNode(g, b).x);
15454
16250
  }
15455
16251
  let anyMoved = false;
15456
16252
  for (const [, rankNodes] of rankMap) {
@@ -15477,10 +16273,10 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15477
16273
  }
15478
16274
  for (const partition of partitions) {
15479
16275
  if (partition.length < 2) continue;
15480
- const xSlots = partition.map((name) => g.node(name).x).sort((a, b) => a - b);
16276
+ const xSlots = partition.map((name) => gNode(g, name).x).sort((a, b) => a - b);
15481
16277
  const basePositions = /* @__PURE__ */ new Map();
15482
16278
  for (const name of g.nodes()) {
15483
- const pos = g.node(name);
16279
+ const pos = gNode(g, name);
15484
16280
  if (pos) basePositions.set(name, pos.x);
15485
16281
  }
15486
16282
  const currentPenalty = computeEdgePenalty(
@@ -15558,7 +16354,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15558
16354
  }
15559
16355
  if (bestPerm.some((name, i) => name !== partition[i])) {
15560
16356
  for (let i = 0; i < bestPerm.length; i++) {
15561
- g.node(bestPerm[i]).x = xSlots[i];
16357
+ gNode(g, bestPerm[i]).x = xSlots[i];
15562
16358
  const rankIdx = rankNodes.indexOf(partition[i]);
15563
16359
  if (rankIdx >= 0) rankNodes[rankIdx] = bestPerm[i];
15564
16360
  }
@@ -15568,10 +16364,10 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15568
16364
  }
15569
16365
  if (anyMoved) {
15570
16366
  for (const edge of edgeList) {
15571
- const edgeData = g.edge(edge.source, edge.target);
16367
+ const edgeData = gEdge(g, edge.source, edge.target);
15572
16368
  if (!edgeData) continue;
15573
- const srcPos = g.node(edge.source);
15574
- const tgtPos = g.node(edge.target);
16369
+ const srcPos = gNode(g, edge.source);
16370
+ const tgtPos = gNode(g, edge.target);
15575
16371
  if (!srcPos || !tgtPos) continue;
15576
16372
  const srcBottom = { x: srcPos.x, y: srcPos.y + srcPos.height / 2 };
15577
16373
  const tgtTop = { x: tgtPos.x, y: tgtPos.y - tgtPos.height / 2 };
@@ -17015,11 +17811,13 @@ function layoutC4Deployment(parsed, activeTagGroup) {
17015
17811
  height: totalHeight
17016
17812
  };
17017
17813
  }
17018
- var CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN4, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT4, LEGEND_PILL_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_CAPSULE_PAD4, EDGE_NODE_COLLISION_WEIGHT, META_EXCLUDE_KEYS;
17814
+ var gNode, gEdge, CHAR_WIDTH5, MIN_NODE_WIDTH, MAX_NODE_WIDTH, TYPE_LABEL_HEIGHT, DIVIDER_GAP, NAME_HEIGHT, DESC_LINE_HEIGHT, DESC_CHAR_WIDTH, CARD_V_PAD3, CARD_H_PAD3, META_LINE_HEIGHT5, META_CHAR_WIDTH, MARGIN4, BOUNDARY_PAD, GROUP_BOUNDARY_PAD, LEGEND_HEIGHT4, LEGEND_PILL_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_CAPSULE_PAD4, EDGE_NODE_COLLISION_WEIGHT, META_EXCLUDE_KEYS;
17019
17815
  var init_layout6 = __esm({
17020
17816
  "src/c4/layout.ts"() {
17021
17817
  "use strict";
17022
17818
  init_legend_constants();
17819
+ gNode = (g, name) => g.node(name);
17820
+ gEdge = (g, v, w) => g.edge(v, w);
17023
17821
  CHAR_WIDTH5 = 8;
17024
17822
  MIN_NODE_WIDTH = 160;
17025
17823
  MAX_NODE_WIDTH = 260;
@@ -17276,7 +18074,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17276
18074
  if (activeTagGroup) {
17277
18075
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17278
18076
  }
17279
- renderLegend3(
18077
+ renderLegend2(
17280
18078
  legendParent,
17281
18079
  layout,
17282
18080
  palette,
@@ -17637,52 +18435,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17637
18435
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17638
18436
  }
17639
18437
  }
17640
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17641
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17642
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17643
- ) : layout.legend;
17644
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17645
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17646
- let fixedPositions = null;
17647
- if (fixedWidth != null && visibleGroups.length > 0) {
17648
- fixedPositions = /* @__PURE__ */ new Map();
17649
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17650
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17651
- for (const g of visibleGroups) {
17652
- fixedPositions.set(g.name, cx);
17653
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17654
- }
17655
- }
17656
- for (const group of visibleGroups) {
17657
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17658
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17659
- const pillLabel = group.name;
17660
- const pillWidth2 = pillWidthOf(group);
17661
- const gX = fixedPositions?.get(group.name) ?? group.x;
17662
- const gY = fixedPositions != null ? 0 : group.y;
17663
- const gEl = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
17664
- if (isActive) {
17665
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17666
- }
17667
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17668
- const pillY = LEGEND_CAPSULE_PAD;
17669
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17670
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
17671
- if (isActive) {
17672
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
17673
- }
17674
- gEl.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
17675
- if (isActive) {
17676
- let entryX = pillX + pillWidth2 + 4;
17677
- for (const entry of group.entries) {
17678
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17679
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17680
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17681
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
17682
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17683
- }
17684
- }
17685
- }
18438
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18439
+ const groups = layout.legend.map((g) => ({
18440
+ name: g.name,
18441
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18442
+ }));
18443
+ const legendConfig = {
18444
+ groups,
18445
+ position: { placement: "top-center", titleRelation: "below-title" },
18446
+ mode: "fixed"
18447
+ };
18448
+ const legendState = { activeGroup: activeTagGroup ?? null };
18449
+ const containerWidth = fixedWidth ?? layout.width;
18450
+ renderLegendD3(
18451
+ parent,
18452
+ legendConfig,
18453
+ legendState,
18454
+ palette,
18455
+ isDark,
18456
+ void 0,
18457
+ containerWidth
18458
+ );
18459
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17686
18460
  }
17687
18461
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17688
18462
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -17893,7 +18667,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
17893
18667
  if (activeTagGroup) {
17894
18668
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17895
18669
  }
17896
- renderLegend3(
18670
+ renderLegend2(
17897
18671
  legendParent,
17898
18672
  layout,
17899
18673
  palette,
@@ -18021,6 +18795,7 @@ var init_renderer7 = __esm({
18021
18795
  init_parser6();
18022
18796
  init_layout6();
18023
18797
  init_legend_constants();
18798
+ init_legend_d3();
18024
18799
  init_title_constants();
18025
18800
  DIAGRAM_PADDING7 = 20;
18026
18801
  MAX_SCALE5 = 3;
@@ -20910,17 +21685,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20910
21685
  color: r.color,
20911
21686
  key: r.name.toLowerCase().replace(/\s+/g, "-")
20912
21687
  }));
20913
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20914
- let entriesWidth2 = 0;
21688
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21689
+ let entriesWidth3 = 0;
20915
21690
  for (const e of entries) {
20916
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21691
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20917
21692
  }
20918
21693
  groups.push({
20919
21694
  name: "Capabilities",
20920
21695
  type: "role",
20921
21696
  entries,
20922
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20923
- minifiedWidth: pillWidth2
21697
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21698
+ minifiedWidth: pillWidth3
20924
21699
  });
20925
21700
  }
20926
21701
  for (const tg of tagGroups) {
@@ -20935,113 +21710,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20935
21710
  }
20936
21711
  }
20937
21712
  if (entries.length === 0) continue;
20938
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20939
- let entriesWidth2 = 0;
21713
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21714
+ let entriesWidth3 = 0;
20940
21715
  for (const e of entries) {
20941
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21716
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20942
21717
  }
20943
21718
  groups.push({
20944
21719
  name: tg.name,
20945
21720
  type: "tag",
20946
21721
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
20947
21722
  entries,
20948
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20949
- minifiedWidth: pillWidth2
21723
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21724
+ minifiedWidth: pillWidth3
20950
21725
  });
20951
21726
  }
20952
21727
  return groups;
20953
21728
  }
20954
- function computePlaybackWidth(playback) {
20955
- if (!playback) return 0;
20956
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20957
- if (!playback.expanded) return pillWidth2;
20958
- let entriesW = 8;
20959
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
20960
- for (const s of playback.speedOptions) {
20961
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
20962
- }
20963
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
20964
- }
20965
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21729
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
20966
21730
  if (legendGroups.length === 0 && !playback) return;
20967
21731
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
20968
21732
  if (activeGroup) {
20969
21733
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
20970
21734
  }
20971
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
20972
- const playbackW = computePlaybackWidth(playback);
20973
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
20974
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
20975
- let cursorX = (totalWidth - totalLegendW) / 2;
21735
+ const allGroups = legendGroups.map((g) => ({
21736
+ name: g.name,
21737
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21738
+ }));
21739
+ if (playback) {
21740
+ allGroups.push({ name: "Playback", entries: [] });
21741
+ }
21742
+ const legendConfig = {
21743
+ groups: allGroups,
21744
+ position: { placement: "top-center", titleRelation: "below-title" },
21745
+ mode: "fixed",
21746
+ showEmptyGroups: true
21747
+ };
21748
+ const legendState = { activeGroup };
21749
+ renderLegendD3(
21750
+ legendG,
21751
+ legendConfig,
21752
+ legendState,
21753
+ palette,
21754
+ isDark,
21755
+ void 0,
21756
+ totalWidth
21757
+ );
21758
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
20976
21759
  for (const group of legendGroups) {
20977
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
20978
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20979
- const pillLabel = group.name;
20980
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20981
- const gEl = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
20982
- if (isActive) {
20983
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
20984
- }
20985
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
20986
- const pillYOff = LEGEND_CAPSULE_PAD;
20987
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
20988
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
20989
- if (isActive) {
20990
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
20991
- }
20992
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
20993
- if (isActive) {
20994
- let entryX = pillXOff + pillWidth2 + 4;
20995
- for (const entry of group.entries) {
20996
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21760
+ const groupKey = group.name.toLowerCase();
21761
+ for (const entry of group.entries) {
21762
+ const entryEl = legendG.select(
21763
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21764
+ );
21765
+ if (!entryEl.empty()) {
21766
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
20997
21767
  "data-legend-tag-group",
20998
21768
  group.type === "tag" ? group.tagKey ?? "" : null
20999
- ).style("cursor", "pointer");
21000
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21001
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21002
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
21003
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21769
+ );
21004
21770
  }
21005
21771
  }
21006
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21007
21772
  }
21008
- if (playback) {
21009
- const isExpanded = playback.expanded;
21010
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21011
- const pillLabel = "Playback";
21012
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21013
- const fullW = computePlaybackWidth(playback);
21014
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21015
- if (isExpanded) {
21016
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21017
- }
21018
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21019
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21020
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21021
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isExpanded ? palette.bg : groupBg);
21022
- if (isExpanded) {
21023
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
21024
- }
21025
- pbG.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isExpanded ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
21026
- if (isExpanded) {
21027
- let entryX = pillXOff + pillWidth2 + 8;
21028
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21029
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21030
- pbG.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21031
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21032
- for (const s of playback.speedOptions) {
21033
- const label = `${s}x`;
21034
- const isActive = playback.speed === s;
21035
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21036
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21037
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21038
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21039
- speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isActive ? palette.primary : "transparent");
21040
- speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21041
- entryX += slotW + SPEED_BADGE_GAP;
21042
- }
21043
- }
21044
- cursorX += fullW + LEGEND_GROUP_GAP;
21773
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21774
+ if (!playbackEl.empty()) {
21775
+ playbackEl.classed("infra-playback-pill", true);
21776
+ }
21777
+ if (playback && playback.expanded && !playbackEl.empty()) {
21778
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21779
+ let entryX = pillWidth3 + 8;
21780
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21781
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21782
+ 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);
21783
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21784
+ for (const s of playback.speedOptions) {
21785
+ const label = `${s}x`;
21786
+ const isSpeedActive = playback.speed === s;
21787
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21788
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21789
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21790
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21791
+ 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");
21792
+ 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);
21793
+ entryX += slotW + SPEED_BADGE_GAP;
21794
+ }
21045
21795
  }
21046
21796
  }
21047
21797
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21172,7 +21922,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21172
21922
  "viewBox",
21173
21923
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21174
21924
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21175
- renderLegend4(
21925
+ renderLegend3(
21176
21926
  legendSvg,
21177
21927
  legendGroups,
21178
21928
  containerWidth,
@@ -21184,7 +21934,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21184
21934
  );
21185
21935
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21186
21936
  } else {
21187
- renderLegend4(
21937
+ renderLegend3(
21188
21938
  rootSvg,
21189
21939
  legendGroups,
21190
21940
  totalWidth,
@@ -21216,6 +21966,7 @@ var init_renderer8 = __esm({
21216
21966
  init_compute();
21217
21967
  init_layout8();
21218
21968
  init_legend_constants();
21969
+ init_legend_d3();
21219
21970
  init_title_constants();
21220
21971
  NODE_FONT_SIZE3 = 13;
21221
21972
  META_FONT_SIZE5 = 10;
@@ -22843,7 +23594,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22843
23594
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22844
23595
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22845
23596
  const showIcon = !legendViewMode && tagGroups.length > 0;
22846
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23597
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22847
23598
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22848
23599
  let groupW = pillW;
22849
23600
  if (isActive) {
@@ -22870,83 +23621,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22870
23621
  const legendX = (containerWidth - totalW) / 2;
22871
23622
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22872
23623
  let cursorX = 0;
22873
- for (let i = 0; i < visibleGroups.length; i++) {
22874
- const group = visibleGroups[i];
22875
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22876
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23624
+ if (visibleGroups.length > 0) {
22877
23625
  const showIcon = !legendViewMode && tagGroups.length > 0;
22878
23626
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
22879
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22880
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
22881
- const groupW = groupWidths[i];
22882
- const gEl = legendRow.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "gantt-tag-legend-group").attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber)).style("cursor", "pointer").on("click", () => {
22883
- if (onToggle) onToggle(group.name);
23627
+ const legendGroups = visibleGroups.map((g) => {
23628
+ const key = g.name.toLowerCase();
23629
+ const entries = filteredEntries.get(key) ?? g.entries;
23630
+ return {
23631
+ name: g.name,
23632
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23633
+ };
22884
23634
  });
22885
- if (isActive) {
22886
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
22887
- }
22888
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
22889
- const pillYOff = LEGEND_CAPSULE_PAD;
22890
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
22891
- if (isActive) {
22892
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
22893
- }
22894
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
22895
- gEl.append("text").attr("x", pillXOff + textW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("text-anchor", "middle").attr("font-size", `${LEGEND_PILL_FONT_SIZE}px`).attr("font-weight", "500").attr("fill", isActive || isSwimlane ? palette.text : palette.textMuted).text(group.name);
22896
- if (showIcon) {
22897
- const iconX = pillXOff + textW + 3;
22898
- const iconY = (LEGEND_HEIGHT - 10) / 2;
22899
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
22900
- iconEl.append("title").text(`Group by ${group.name}`);
22901
- iconEl.style("cursor", "pointer").on("click", (event) => {
22902
- event.stopPropagation();
22903
- if (onSwimlaneChange) {
22904
- onSwimlaneChange(
22905
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
22906
- );
22907
- }
22908
- });
22909
- }
22910
- if (isActive) {
22911
- const tagKey = group.name.toLowerCase();
22912
- const entries = filteredEntries.get(tagKey) ?? group.entries;
22913
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
22914
- for (const entry of entries) {
22915
- const entryValue = entry.value.toLowerCase();
22916
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
22917
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
22918
- entryG.append("text").attr("x", ex + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 2).attr("text-anchor", "start").attr("font-size", `${LEGEND_ENTRY_FONT_SIZE}px`).attr("fill", palette.textMuted).text(entry.value);
22919
- entryG.on("mouseenter", () => {
23635
+ const legendConfig = {
23636
+ groups: legendGroups,
23637
+ position: {
23638
+ placement: "top-center",
23639
+ titleRelation: "below-title"
23640
+ },
23641
+ mode: "fixed",
23642
+ capsulePillAddonWidth: iconReserve
23643
+ };
23644
+ const legendState = { activeGroup: activeGroupName };
23645
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23646
+ const tagGroupG = legendRow.append("g");
23647
+ const legendCallbacks = {
23648
+ onGroupToggle: onToggle,
23649
+ onEntryHover: (groupName, entryValue) => {
23650
+ const tagKey = groupName.toLowerCase();
23651
+ if (entryValue) {
23652
+ const ev = entryValue.toLowerCase();
22920
23653
  chartG.selectAll(".gantt-task").each(function() {
22921
23654
  const el = d3Selection10.select(this);
22922
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22923
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23655
+ el.attr(
23656
+ "opacity",
23657
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23658
+ );
22924
23659
  });
22925
23660
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
22926
23661
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
22927
23662
  svg.selectAll(".gantt-task-label").each(function() {
22928
23663
  const el = d3Selection10.select(this);
22929
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22930
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23664
+ el.attr(
23665
+ "opacity",
23666
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23667
+ );
22931
23668
  });
22932
23669
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
22933
23670
  svg.selectAll(".gantt-lane-header").each(function() {
22934
23671
  const el = d3Selection10.select(this);
22935
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22936
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23672
+ el.attr(
23673
+ "opacity",
23674
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23675
+ );
22937
23676
  });
22938
23677
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
22939
- }).on("mouseleave", () => {
23678
+ } else {
22940
23679
  if (criticalPathActive) {
22941
23680
  applyCriticalPathHighlight(svg, chartG);
22942
23681
  } else {
22943
23682
  resetHighlightAll(svg, chartG);
22944
23683
  }
22945
- });
22946
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23684
+ }
23685
+ },
23686
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23687
+ const group = visibleGroups.find((g) => g.name === groupName);
23688
+ if (group) {
23689
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23690
+ }
23691
+ if (showIcon && _isActive) {
23692
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23693
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23694
+ const pillXOff = LEGEND_CAPSULE_PAD;
23695
+ const iconX = pillXOff + textW + 3;
23696
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23697
+ const iconEl = drawSwimlaneIcon(
23698
+ groupEl,
23699
+ iconX,
23700
+ iconY,
23701
+ isSwimlane,
23702
+ palette
23703
+ );
23704
+ iconEl.append("title").text(`Group by ${groupName}`);
23705
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23706
+ event.stopPropagation();
23707
+ if (onSwimlaneChange) {
23708
+ onSwimlaneChange(
23709
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23710
+ );
23711
+ }
23712
+ });
23713
+ }
22947
23714
  }
23715
+ };
23716
+ renderLegendD3(
23717
+ tagGroupG,
23718
+ legendConfig,
23719
+ legendState,
23720
+ palette,
23721
+ isDark,
23722
+ legendCallbacks,
23723
+ tagGroupsW
23724
+ );
23725
+ for (let i = 0; i < visibleGroups.length; i++) {
23726
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
22948
23727
  }
22949
- cursorX += groupW + LEGEND_GROUP_GAP;
22950
23728
  }
22951
23729
  if (hasCriticalPath) {
22952
23730
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23573,6 +24351,7 @@ var init_renderer9 = __esm({
23573
24351
  init_tag_groups();
23574
24352
  init_d3();
23575
24353
  init_legend_constants();
24354
+ init_legend_d3();
23576
24355
  init_title_constants();
23577
24356
  BAR_H = 22;
23578
24357
  ROW_GAP = 6;
@@ -24743,57 +25522,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24743
25522
  }
24744
25523
  if (parsed.tagGroups.length > 0) {
24745
25524
  const legendY = TOP_MARGIN + titleOffset;
24746
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24747
- const legendItems = [];
24748
- for (const tg of parsed.tagGroups) {
24749
- if (tg.entries.length === 0) continue;
24750
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24751
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24752
- const entries = tg.entries.map((e) => ({
25525
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25526
+ name: tg.name,
25527
+ entries: tg.entries.map((e) => ({
24753
25528
  value: e.value,
24754
25529
  color: resolveColor(e.color) ?? e.color
24755
- }));
24756
- let totalWidth2 = pillWidth2;
24757
- if (isActive) {
24758
- let entriesWidth2 = 0;
24759
- for (const entry of entries) {
24760
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24761
- }
24762
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24763
- }
24764
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24765
- }
24766
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24767
- let legendX = (svgWidth - totalLegendWidth) / 2;
24768
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24769
- if (activeTagGroup) {
24770
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24771
- }
24772
- for (const item of legendItems) {
24773
- const gEl = legendContainer.append("g").attr("transform", `translate(${legendX}, ${legendY})`).attr("class", "sequence-legend-group").attr("data-legend-group", item.group.name.toLowerCase()).style("cursor", "pointer");
24774
- if (item.isActive) {
24775
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24776
- }
24777
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24778
- const pillYOff = LEGEND_CAPSULE_PAD;
24779
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24780
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", item.isActive ? palette.bg : groupBg);
24781
- if (item.isActive) {
24782
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
24783
- }
24784
- gEl.append("text").attr("x", pillXOff + item.pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", item.isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(item.group.name);
24785
- if (item.isActive) {
24786
- let entryX = pillXOff + item.pillWidth + 4;
24787
- for (const entry of item.entries) {
24788
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24789
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24790
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24791
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
24792
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24793
- }
24794
- }
24795
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24796
- }
25530
+ }))
25531
+ }));
25532
+ const legendConfig = {
25533
+ groups: resolvedGroups,
25534
+ position: { placement: "top-center", titleRelation: "below-title" },
25535
+ mode: "fixed"
25536
+ };
25537
+ const legendState = { activeGroup: activeTagGroup ?? null };
25538
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25539
+ renderLegendD3(
25540
+ legendG,
25541
+ legendConfig,
25542
+ legendState,
25543
+ palette,
25544
+ isDark,
25545
+ void 0,
25546
+ svgWidth
25547
+ );
24797
25548
  }
24798
25549
  for (const group of groups) {
24799
25550
  if (group.participantIds.length === 0) continue;
@@ -24870,7 +25621,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24870
25621
  for (const branch of el.elseIfBranches) {
24871
25622
  elseIfBranchData.push({
24872
25623
  label: branch.label,
24873
- indices: collectMsgIndices(branch.children)
25624
+ indices: collectMsgIndices(branch.children),
25625
+ lineNumber: branch.lineNumber
24874
25626
  });
24875
25627
  }
24876
25628
  }
@@ -24931,14 +25683,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24931
25683
  x1: frameX,
24932
25684
  y1: dividerY,
24933
25685
  x2: frameX + frameW,
24934
- y2: dividerY
25686
+ y2: dividerY,
25687
+ blockLine: branchData.lineNumber
24935
25688
  });
24936
25689
  deferredLabels.push({
24937
25690
  x: frameX + 6,
24938
25691
  y: dividerY + 14,
24939
25692
  text: `else if ${branchData.label}`,
24940
25693
  bold: false,
24941
- italic: true
25694
+ italic: true,
25695
+ blockLine: branchData.lineNumber
24942
25696
  });
24943
25697
  }
24944
25698
  }
@@ -24956,14 +25710,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24956
25710
  x1: frameX,
24957
25711
  y1: dividerY,
24958
25712
  x2: frameX + frameW,
24959
- y2: dividerY
25713
+ y2: dividerY,
25714
+ blockLine: el.elseLineNumber
24960
25715
  });
24961
25716
  deferredLabels.push({
24962
25717
  x: frameX + 6,
24963
25718
  y: dividerY + 14,
24964
25719
  text: "else",
24965
25720
  bold: false,
24966
- italic: true
25721
+ italic: true,
25722
+ blockLine: el.elseLineNumber
24967
25723
  });
24968
25724
  }
24969
25725
  }
@@ -25009,7 +25765,9 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
25009
25765
  }
25010
25766
  });
25011
25767
  for (const ln of deferredLines) {
25012
- svg.append("line").attr("x1", ln.x1).attr("y1", ln.y1).attr("x2", ln.x2).attr("y2", ln.y2).attr("stroke", palette.textMuted).attr("stroke-width", 1).attr("stroke-dasharray", "2 3");
25768
+ const line10 = svg.append("line").attr("x1", ln.x1).attr("y1", ln.y1).attr("x2", ln.x2).attr("y2", ln.y2).attr("stroke", palette.textMuted).attr("stroke-width", 1).attr("stroke-dasharray", "2 3").attr("class", "block-divider");
25769
+ if (ln.blockLine !== void 0)
25770
+ line10.attr("data-block-line", String(ln.blockLine));
25013
25771
  }
25014
25772
  for (const lbl of deferredLabels) {
25015
25773
  const t = svg.append("text").attr("x", lbl.x).attr("y", lbl.y).attr("fill", palette.text).attr("font-size", 11).attr("class", "block-label").text(lbl.text);
@@ -25352,6 +26110,7 @@ var init_renderer10 = __esm({
25352
26110
  init_parser();
25353
26111
  init_tag_resolution();
25354
26112
  init_legend_constants();
26113
+ init_legend_d3();
25355
26114
  init_title_constants();
25356
26115
  PARTICIPANT_GAP = 160;
25357
26116
  PARTICIPANT_BOX_WIDTH = 120;
@@ -27832,7 +28591,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
27832
28591
  }
27833
28592
  evG.append("rect").attr("x", x).attr("y", y - BAR_H2 / 2).attr("width", rectW).attr("height", BAR_H2).attr("rx", 4).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", 2);
27834
28593
  if (labelFitsInside) {
27835
- evG.append("text").attr("x", x + 8).attr("y", y).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", "14px").attr("font-weight", "700").text(ev.label);
28594
+ evG.append("text").attr("x", x + 8).attr("y", y).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", "13px").text(ev.label);
27836
28595
  } else {
27837
28596
  const wouldFlipLeft = x + rectW > innerWidth * 0.6;
27838
28597
  const labelFitsLeft = x - 6 - estLabelWidth > 0;
@@ -27986,7 +28745,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
27986
28745
  }
27987
28746
  evG.append("rect").attr("x", x).attr("y", y - BAR_H2 / 2).attr("width", rectW).attr("height", BAR_H2).attr("rx", 4).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", 2);
27988
28747
  if (labelFitsInside) {
27989
- evG.append("text").attr("x", x + 8).attr("y", y).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", "14px").attr("font-weight", "700").text(ev.label);
28748
+ evG.append("text").attr("x", x + 8).attr("y", y).attr("dy", "0.35em").attr("text-anchor", "start").attr("fill", textColor).attr("font-size", "13px").text(ev.label);
27990
28749
  } else {
27991
28750
  const wouldFlipLeft = x + rectW > innerWidth * 0.6;
27992
28751
  const labelFitsLeft = x - 6 - estLabelWidth > 0;
@@ -28012,7 +28771,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28012
28771
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28013
28772
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28014
28773
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28015
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28016
28774
  const LG_ICON_W = 20;
28017
28775
  const mainSvg = d3Selection13.select(container).select("svg");
28018
28776
  const mainG = mainSvg.select("g");
@@ -28051,11 +28809,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28051
28809
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28052
28810
  ) : legendGroups;
28053
28811
  if (visibleGroups.length === 0) return;
28054
- const totalW = visibleGroups.reduce((s, lg) => {
28055
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28056
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28057
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28058
- let cx = (width - totalW) / 2;
28059
28812
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28060
28813
  if (currentActiveGroup) {
28061
28814
  legendContainer.attr(
@@ -28063,82 +28816,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28063
28816
  currentActiveGroup.toLowerCase()
28064
28817
  );
28065
28818
  }
28066
- for (const lg of visibleGroups) {
28067
- const groupKey = lg.group.name.toLowerCase();
28068
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28069
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28070
- const pillLabel = lg.group.name;
28071
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28072
- const gEl = legendContainer.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", groupKey).attr("data-tag-group", groupKey).attr("data-legend-entry", "__group__");
28073
- if (!viewMode) {
28074
- gEl.style("cursor", "pointer").on("click", () => {
28075
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28076
- drawLegend2();
28077
- recolorEvents2();
28078
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28079
- });
28080
- }
28081
- if (isActive) {
28082
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28083
- }
28084
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28085
- const pillYOff = LG_CAPSULE_PAD;
28086
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28087
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
28088
- if (isActive) {
28089
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
28090
- }
28091
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2).attr("font-size", LG_PILL_FONT_SIZE).attr("font-weight", "500").attr("font-family", FONT_FAMILY).attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
28092
- if (isActive) {
28093
- let entryX;
28094
- if (!viewMode) {
28095
- const iconX = pillXOff + pillWidth2 + 5;
28819
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28820
+ const centralGroups = visibleGroups.map((lg) => ({
28821
+ name: lg.group.name,
28822
+ entries: lg.group.entries.map((e) => ({
28823
+ value: e.value,
28824
+ color: e.color
28825
+ }))
28826
+ }));
28827
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28828
+ const centralConfig = {
28829
+ groups: centralGroups,
28830
+ position: { placement: "top-center", titleRelation: "below-title" },
28831
+ mode: "fixed",
28832
+ capsulePillAddonWidth: iconAddon
28833
+ };
28834
+ const centralState = { activeGroup: centralActive };
28835
+ const centralCallbacks = viewMode ? {} : {
28836
+ onGroupToggle: (groupName) => {
28837
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28838
+ drawLegend2();
28839
+ recolorEvents2();
28840
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28841
+ },
28842
+ onEntryHover: (groupName, entryValue) => {
28843
+ const tagKey = groupName.toLowerCase();
28844
+ if (entryValue) {
28845
+ const tagVal = entryValue.toLowerCase();
28846
+ fadeToTagValue(mainG, tagKey, tagVal);
28847
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28848
+ const el = d3Selection13.select(this);
28849
+ const ev = el.attr("data-legend-entry");
28850
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28851
+ el.attr(
28852
+ "opacity",
28853
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28854
+ );
28855
+ });
28856
+ } else {
28857
+ fadeReset(mainG);
28858
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28859
+ }
28860
+ },
28861
+ onGroupRendered: (groupName, groupEl, isActive) => {
28862
+ const groupKey = groupName.toLowerCase();
28863
+ groupEl.attr("data-tag-group", groupKey);
28864
+ if (isActive && !viewMode) {
28865
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28866
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28867
+ const pillXOff = LG_CAPSULE_PAD;
28868
+ const iconX = pillXOff + pillWidth3 + 5;
28096
28869
  const iconY = (LG_HEIGHT - 10) / 2;
28097
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28870
+ const iconEl = drawSwimlaneIcon3(
28871
+ groupEl,
28872
+ iconX,
28873
+ iconY,
28874
+ isSwimActive
28875
+ );
28098
28876
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28099
28877
  event.stopPropagation();
28100
28878
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28101
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28879
+ onTagStateChange?.(
28880
+ currentActiveGroup,
28881
+ currentSwimlaneGroup
28882
+ );
28102
28883
  relayout2();
28103
28884
  });
28104
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28105
- } else {
28106
- entryX = pillXOff + pillWidth2 + 8;
28107
- }
28108
- for (const entry of lg.group.entries) {
28109
- const tagKey = lg.group.name.toLowerCase();
28110
- const tagVal = entry.value.toLowerCase();
28111
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28112
- if (!viewMode) {
28113
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28114
- event.stopPropagation();
28115
- fadeToTagValue(mainG, tagKey, tagVal);
28116
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28117
- const el = d3Selection13.select(this);
28118
- const ev = el.attr("data-legend-entry");
28119
- if (ev === "__group__") return;
28120
- const eg = el.attr("data-tag-group");
28121
- el.attr(
28122
- "opacity",
28123
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28124
- );
28125
- });
28126
- }).on("mouseleave", (event) => {
28127
- event.stopPropagation();
28128
- fadeReset(mainG);
28129
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28130
- }).on("click", (event) => {
28131
- event.stopPropagation();
28132
- });
28133
- }
28134
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28135
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28136
- entryG.append("text").attr("x", textX).attr("y", LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LG_ENTRY_FONT_SIZE).attr("font-family", FONT_FAMILY).attr("fill", palette.textMuted).text(entry.value);
28137
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28138
28885
  }
28139
28886
  }
28140
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28141
- }
28887
+ };
28888
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28889
+ renderLegendD3(
28890
+ legendInnerG,
28891
+ centralConfig,
28892
+ centralState,
28893
+ palette,
28894
+ isDark,
28895
+ centralCallbacks,
28896
+ width
28897
+ );
28142
28898
  }, recolorEvents2 = function() {
28143
28899
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28144
28900
  mainG.selectAll(".tl-event").each(function() {
@@ -28163,7 +28919,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28163
28919
  };
28164
28920
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28165
28921
  const legendY = title ? 50 : 10;
28166
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28167
28922
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28168
28923
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28169
28924
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -28358,7 +29113,7 @@ function regionCentroid(circles, inside) {
28358
29113
  }
28359
29114
  function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims) {
28360
29115
  const { vennSets, vennOverlaps, title } = parsed;
28361
- if (vennSets.length < 2) return;
29116
+ if (vennSets.length < 2 || vennSets.length > 3) return;
28362
29117
  const init2 = initD3Chart(container, palette, exportDims);
28363
29118
  if (!init2) return;
28364
29119
  const { svg, width, height, textColor, colors } = init2;
@@ -28414,7 +29169,9 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28414
29169
  marginBottom
28415
29170
  ).map((c) => ({ ...c, y: c.y + titleHeight }));
28416
29171
  const scaledR = circles[0].r;
28417
- svg.append("style").text("circle:focus, circle:focus-visible { outline: none !important; }");
29172
+ svg.append("style").text(
29173
+ "circle:focus, circle:focus-visible { outline-solid: none !important; }"
29174
+ );
28418
29175
  renderChartTitle(
28419
29176
  svg,
28420
29177
  title,
@@ -28596,7 +29353,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28596
29353
  }
28597
29354
  const hoverGroup = svg.append("g");
28598
29355
  circles.forEach((c, i) => {
28599
- hoverGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", String(vennSets[i].lineNumber)).style("cursor", onClickItem ? "pointer" : "default").style("outline", "none").on("mouseenter", () => {
29356
+ hoverGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", String(vennSets[i].lineNumber)).style("cursor", onClickItem ? "pointer" : "default").style("outline-solid", "none").on("mouseenter", () => {
28600
29357
  showRegionOverlay([i]);
28601
29358
  }).on("mouseleave", () => {
28602
29359
  hideAllOverlays();
@@ -28634,7 +29391,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28634
29391
  const declaredOv = vennOverlaps.find(
28635
29392
  (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
28636
29393
  );
28637
- hoverGroup.append("circle").attr("cx", centroid.x).attr("cy", centroid.y).attr("r", overlayR).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", declaredOv ? String(declaredOv.lineNumber) : "").style("cursor", onClickItem && declaredOv ? "pointer" : "default").style("outline", "none").on("mouseenter", () => {
29394
+ hoverGroup.append("circle").attr("cx", centroid.x).attr("cy", centroid.y).attr("r", overlayR).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", declaredOv ? String(declaredOv.lineNumber) : "").style("cursor", onClickItem && declaredOv ? "pointer" : "default").style("outline-solid", "none").on("mouseenter", () => {
28638
29395
  showRegionOverlay(idxs);
28639
29396
  }).on("mouseleave", () => {
28640
29397
  hideAllOverlays();
@@ -28768,8 +29525,8 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28768
29525
  const LABEL_MAX_FONT = 48;
28769
29526
  const LABEL_MIN_FONT = 14;
28770
29527
  const LABEL_PAD2 = 40;
28771
- const CHAR_WIDTH_RATIO2 = 0.6;
28772
- const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO2;
29528
+ const CHAR_WIDTH_RATIO3 = 0.6;
29529
+ const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO3;
28773
29530
  const quadrantLabelLayout = (text, qw2, qh2) => {
28774
29531
  const availW = qw2 - LABEL_PAD2;
28775
29532
  const availH = qh2 - LABEL_PAD2;
@@ -28913,16 +29670,45 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28913
29670
  if (x < 0.5 && y < 0.5) return "bottom-left";
28914
29671
  return "bottom-right";
28915
29672
  };
29673
+ const POINT_RADIUS = 6;
29674
+ const POINT_LABEL_FONT_SIZE = 12;
29675
+ const quadrantLabelObstacles = quadrantDefsWithLabel.map((d) => {
29676
+ const layout = labelLayouts.get(d.label.text);
29677
+ const totalW = Math.max(...layout.lines.map((l) => l.length)) * layout.fontSize * CHAR_WIDTH_RATIO3;
29678
+ const totalH = layout.lines.length * layout.fontSize * 1.2;
29679
+ return {
29680
+ x: d.labelX - totalW / 2,
29681
+ y: d.labelY - totalH / 2,
29682
+ w: totalW,
29683
+ h: totalH
29684
+ };
29685
+ });
29686
+ const pointPixels = quadrantPoints.map((point) => ({
29687
+ label: point.label,
29688
+ cx: xScale(point.x),
29689
+ cy: yScale(point.y)
29690
+ }));
29691
+ const placedPointLabels = computeQuadrantPointLabels(
29692
+ pointPixels,
29693
+ { left: 0, top: 0, right: chartWidth, bottom: chartHeight },
29694
+ quadrantLabelObstacles,
29695
+ POINT_RADIUS,
29696
+ POINT_LABEL_FONT_SIZE
29697
+ );
28916
29698
  const pointsG = chartG.append("g").attr("class", "points");
28917
- quadrantPoints.forEach((point) => {
29699
+ quadrantPoints.forEach((point, i) => {
28918
29700
  const cx = xScale(point.x);
28919
29701
  const cy = yScale(point.y);
28920
29702
  const quadrant = getPointQuadrant(point.x, point.y);
28921
29703
  const quadDef = quadrantDefs.find((d) => d.position === quadrant);
28922
29704
  const pointColor = quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
29705
+ const placed = placedPointLabels[i];
28923
29706
  const pointG = pointsG.append("g").attr("class", "point-group").attr("data-line-number", String(point.lineNumber));
28924
- pointG.append("circle").attr("cx", cx).attr("cy", cy).attr("r", 6).attr("fill", "#ffffff").attr("stroke", pointColor).attr("stroke-width", 2);
28925
- pointG.append("text").attr("x", cx).attr("y", cy - 10).attr("text-anchor", "middle").attr("fill", textColor).attr("font-size", "12px").attr("font-weight", "700").style("text-shadow", `0 1px 2px ${shadowColor}`).text(point.label);
29707
+ if (placed.connectorLine) {
29708
+ pointG.append("line").attr("x1", placed.connectorLine.x1).attr("y1", placed.connectorLine.y1).attr("x2", placed.connectorLine.x2).attr("y2", placed.connectorLine.y2).attr("stroke", pointColor).attr("stroke-width", 1).attr("opacity", 0.5);
29709
+ }
29710
+ pointG.append("circle").attr("cx", cx).attr("cy", cy).attr("r", POINT_RADIUS).attr("fill", "#ffffff").attr("stroke", pointColor).attr("stroke-width", 2);
29711
+ pointG.append("text").attr("x", placed.x).attr("y", placed.y).attr("text-anchor", placed.anchor).attr("dominant-baseline", "central").attr("fill", textColor).attr("font-size", `${POINT_LABEL_FONT_SIZE}px`).attr("font-weight", "700").style("text-shadow", `0 1px 2px ${shadowColor}`).text(point.label);
28926
29712
  const tipHtml = `<strong>${point.label}</strong><br>x: ${point.x.toFixed(2)}, y: ${point.y.toFixed(2)}`;
28927
29713
  pointG.style("cursor", onClickItem ? "pointer" : "default").on("mouseenter", (event) => {
28928
29714
  showTooltip(tooltip, tipHtml, event);
@@ -28931,7 +29717,7 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28931
29717
  showTooltip(tooltip, tipHtml, event);
28932
29718
  }).on("mouseleave", () => {
28933
29719
  hideTooltip(tooltip);
28934
- pointG.select("circle").attr("r", 6);
29720
+ pointG.select("circle").attr("r", POINT_RADIUS);
28935
29721
  }).on("click", () => {
28936
29722
  if (onClickItem && point.lineNumber) onClickItem(point.lineNumber);
28937
29723
  });
@@ -29398,6 +30184,7 @@ var init_d3 = __esm({
29398
30184
  "use strict";
29399
30185
  init_fonts();
29400
30186
  init_branding();
30187
+ init_label_layout();
29401
30188
  init_colors();
29402
30189
  init_palettes();
29403
30190
  init_color_utils();
@@ -29405,6 +30192,7 @@ var init_d3 = __esm({
29405
30192
  init_parsing();
29406
30193
  init_tag_groups();
29407
30194
  init_legend_constants();
30195
+ init_legend_d3();
29408
30196
  init_title_constants();
29409
30197
  DEFAULT_CLOUD_OPTIONS = {
29410
30198
  rotate: "none",
@@ -29559,11 +30347,26 @@ async function ensureDom() {
29559
30347
  const { JSDOM } = await import("jsdom");
29560
30348
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29561
30349
  const win = dom.window;
29562
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29563
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29564
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29565
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29566
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30350
+ Object.defineProperty(globalThis, "document", {
30351
+ value: win.document,
30352
+ configurable: true
30353
+ });
30354
+ Object.defineProperty(globalThis, "window", {
30355
+ value: win,
30356
+ configurable: true
30357
+ });
30358
+ Object.defineProperty(globalThis, "navigator", {
30359
+ value: win.navigator,
30360
+ configurable: true
30361
+ });
30362
+ Object.defineProperty(globalThis, "HTMLElement", {
30363
+ value: win.HTMLElement,
30364
+ configurable: true
30365
+ });
30366
+ Object.defineProperty(globalThis, "SVGElement", {
30367
+ value: win.SVGElement,
30368
+ configurable: true
30369
+ });
29567
30370
  }
29568
30371
  async function render(content, options) {
29569
30372
  const theme = options?.theme ?? "light";
@@ -29572,11 +30375,17 @@ async function render(content, options) {
29572
30375
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29573
30376
  const chartType = parseDgmoChartType(content);
29574
30377
  const category = chartType ? getRenderCategory(chartType) : null;
30378
+ const legendExportState = options?.legendState ? {
30379
+ activeTagGroup: options.legendState.activeGroup ?? null,
30380
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30381
+ } : void 0;
29575
30382
  if (category === "data-chart") {
29576
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30383
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30384
+ branding
30385
+ });
29577
30386
  }
29578
30387
  await ensureDom();
29579
- return renderForExport(content, theme, paletteColors, void 0, {
30388
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29580
30389
  branding,
29581
30390
  c4Level: options?.c4Level,
29582
30391
  c4System: options?.c4System,
@@ -30299,6 +31108,8 @@ init_flowchart_renderer();
30299
31108
  init_echarts();
30300
31109
  init_legend_svg();
30301
31110
  init_legend_constants();
31111
+ init_legend_d3();
31112
+ init_legend_layout();
30302
31113
  init_d3();
30303
31114
  init_renderer10();
30304
31115
  init_colors();
@@ -31145,10 +31956,12 @@ export {
31145
31956
  computeCardMove,
31146
31957
  computeInfra,
31147
31958
  computeInfraLegendGroups,
31959
+ computeLegendLayout,
31148
31960
  computeScatterLabelGraphics,
31149
31961
  computeTimeTicks,
31150
31962
  contrastText,
31151
31963
  decodeDiagramUrl,
31964
+ draculaPalette,
31152
31965
  encodeDiagramUrl,
31153
31966
  extractDiagramSymbols,
31154
31967
  extractTagDeclarations,
@@ -31156,6 +31969,7 @@ export {
31156
31969
  formatDgmoError,
31157
31970
  getAvailablePalettes,
31158
31971
  getExtendedChartLegendGroups,
31972
+ getLegendReservedHeight,
31159
31973
  getPalette,
31160
31974
  getRenderCategory,
31161
31975
  getSeriesColors,
@@ -31191,6 +32005,7 @@ export {
31191
32005
  looksLikeSitemap,
31192
32006
  looksLikeState,
31193
32007
  makeDgmoError,
32008
+ monokaiPalette,
31194
32009
  mute,
31195
32010
  nord,
31196
32011
  nordPalette,
@@ -31244,7 +32059,9 @@ export {
31244
32059
  renderInfra,
31245
32060
  renderKanban,
31246
32061
  renderKanbanForExport,
32062
+ renderLegendD3,
31247
32063
  renderLegendSvg,
32064
+ renderLegendSvgFromConfig,
31248
32065
  renderOrg,
31249
32066
  renderOrgForExport,
31250
32067
  renderQuadrant,