@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.cjs CHANGED
@@ -144,6 +144,183 @@ var init_branding = __esm({
144
144
  }
145
145
  });
146
146
 
147
+ // src/label-layout.ts
148
+ function rectsOverlap(a, b) {
149
+ 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;
150
+ }
151
+ function rectCircleOverlap(rect, circle) {
152
+ const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
153
+ const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
154
+ const dx = nearestX - circle.cx;
155
+ const dy = nearestY - circle.cy;
156
+ return dx * dx + dy * dy < circle.r * circle.r;
157
+ }
158
+ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius, fontSize) {
159
+ const labelHeight = fontSize + 4;
160
+ const stepSize = labelHeight + 2;
161
+ const minGap = pointRadius + 4;
162
+ const pointCircles = points.map((p) => ({
163
+ cx: p.cx,
164
+ cy: p.cy,
165
+ r: pointRadius
166
+ }));
167
+ const placedLabels = [];
168
+ const results = [];
169
+ for (let i = 0; i < points.length; i++) {
170
+ const pt = points[i];
171
+ const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
172
+ let best = null;
173
+ const directions = [
174
+ {
175
+ // Above
176
+ gen: (offset) => {
177
+ const lx = pt.cx - labelWidth / 2;
178
+ const ly = pt.cy - offset - labelHeight;
179
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
180
+ return null;
181
+ return {
182
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
183
+ textX: pt.cx,
184
+ textY: ly + labelHeight / 2,
185
+ anchor: "middle"
186
+ };
187
+ }
188
+ },
189
+ {
190
+ // Below
191
+ gen: (offset) => {
192
+ const lx = pt.cx - labelWidth / 2;
193
+ const ly = pt.cy + offset;
194
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
195
+ return null;
196
+ return {
197
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
198
+ textX: pt.cx,
199
+ textY: ly + labelHeight / 2,
200
+ anchor: "middle"
201
+ };
202
+ }
203
+ },
204
+ {
205
+ // Right
206
+ gen: (offset) => {
207
+ const lx = pt.cx + offset;
208
+ const ly = pt.cy - labelHeight / 2;
209
+ if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
210
+ return null;
211
+ return {
212
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
213
+ textX: lx,
214
+ textY: pt.cy,
215
+ anchor: "start"
216
+ };
217
+ }
218
+ },
219
+ {
220
+ // Left
221
+ gen: (offset) => {
222
+ const lx = pt.cx - offset - labelWidth;
223
+ const ly = pt.cy - labelHeight / 2;
224
+ if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
225
+ return null;
226
+ return {
227
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
228
+ textX: lx + labelWidth,
229
+ textY: pt.cy,
230
+ anchor: "end"
231
+ };
232
+ }
233
+ }
234
+ ];
235
+ for (const { gen } of directions) {
236
+ for (let offset = minGap; ; offset += stepSize) {
237
+ const cand = gen(offset);
238
+ if (!cand) break;
239
+ let collision = false;
240
+ for (const pl of placedLabels) {
241
+ if (rectsOverlap(cand.rect, pl)) {
242
+ collision = true;
243
+ break;
244
+ }
245
+ }
246
+ if (!collision) {
247
+ for (const circle of pointCircles) {
248
+ if (rectCircleOverlap(cand.rect, circle)) {
249
+ collision = true;
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ if (!collision) {
255
+ for (const obs of obstacles) {
256
+ if (rectsOverlap(cand.rect, obs)) {
257
+ collision = true;
258
+ break;
259
+ }
260
+ }
261
+ }
262
+ if (!collision) {
263
+ const dist = offset;
264
+ if (!best || dist < best.dist) {
265
+ best = {
266
+ rect: cand.rect,
267
+ textX: cand.textX,
268
+ textY: cand.textY,
269
+ anchor: cand.anchor,
270
+ dist
271
+ };
272
+ }
273
+ break;
274
+ }
275
+ }
276
+ }
277
+ if (!best) {
278
+ const lx = pt.cx - labelWidth / 2;
279
+ const ly = pt.cy - minGap - labelHeight;
280
+ best = {
281
+ rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
282
+ textX: pt.cx,
283
+ textY: ly + labelHeight / 2,
284
+ anchor: "middle",
285
+ dist: minGap
286
+ };
287
+ }
288
+ placedLabels.push(best.rect);
289
+ let connectorLine;
290
+ if (best.dist > minGap + stepSize) {
291
+ const dx = best.textX - pt.cx;
292
+ const dy = best.textY - pt.cy;
293
+ const angle = Math.atan2(dy, dx);
294
+ const x1 = pt.cx + Math.cos(angle) * pointRadius;
295
+ const y1 = pt.cy + Math.sin(angle) * pointRadius;
296
+ const x2 = Math.max(
297
+ best.rect.x,
298
+ Math.min(pt.cx, best.rect.x + best.rect.w)
299
+ );
300
+ const y2 = Math.max(
301
+ best.rect.y,
302
+ Math.min(pt.cy, best.rect.y + best.rect.h)
303
+ );
304
+ connectorLine = { x1, y1, x2, y2 };
305
+ }
306
+ results.push({
307
+ label: pt.label,
308
+ x: best.textX,
309
+ y: best.textY,
310
+ anchor: best.anchor,
311
+ connectorLine
312
+ });
313
+ }
314
+ return results;
315
+ }
316
+ var CHAR_WIDTH_RATIO;
317
+ var init_label_layout = __esm({
318
+ "src/label-layout.ts"() {
319
+ "use strict";
320
+ CHAR_WIDTH_RATIO = 0.6;
321
+ }
322
+ });
323
+
147
324
  // src/colors.ts
148
325
  function resolveColor(color, palette) {
149
326
  if (color.startsWith("#")) return null;
@@ -1814,7 +1991,7 @@ function measureLegendText(text, fontSize) {
1814
1991
  }
1815
1992
  return w;
1816
1993
  }
1817
- 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;
1994
+ 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;
1818
1995
  var init_legend_constants = __esm({
1819
1996
  "src/utils/legend-constants.ts"() {
1820
1997
  "use strict";
@@ -1830,6 +2007,7 @@ var init_legend_constants = __esm({
1830
2007
  LEGEND_EYE_SIZE = 14;
1831
2008
  LEGEND_EYE_GAP = 6;
1832
2009
  LEGEND_ICON_W = 20;
2010
+ LEGEND_MAX_ENTRY_ROWS = 3;
1833
2011
  CHAR_W = {
1834
2012
  " ": 0.28,
1835
2013
  "!": 0.28,
@@ -1923,6 +2101,492 @@ var init_legend_constants = __esm({
1923
2101
  }
1924
2102
  });
1925
2103
 
2104
+ // src/utils/legend-layout.ts
2105
+ function pillWidth(name) {
2106
+ return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
2107
+ }
2108
+ function entriesWidth(entries) {
2109
+ let w = 0;
2110
+ for (const e of entries) {
2111
+ w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2112
+ }
2113
+ return w;
2114
+ }
2115
+ function entryWidth(value) {
2116
+ return LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
2117
+ }
2118
+ function controlWidth(control) {
2119
+ let w = CONTROL_PILL_PAD;
2120
+ if (control.label) {
2121
+ w += measureLegendText(control.label, CONTROL_FONT_SIZE);
2122
+ if (control.icon) w += CONTROL_ICON_GAP;
2123
+ }
2124
+ if (control.icon) w += 14;
2125
+ if (control.children) {
2126
+ for (const child of control.children) {
2127
+ w += measureLegendText(child.label, CONTROL_FONT_SIZE) + 12;
2128
+ }
2129
+ }
2130
+ return w;
2131
+ }
2132
+ function capsuleWidth(name, entries, containerWidth, addonWidth = 0) {
2133
+ const pw = pillWidth(name);
2134
+ const maxCapsuleW = containerWidth;
2135
+ const baseW = LEGEND_CAPSULE_PAD * 2 + pw + 4 + addonWidth;
2136
+ const ew = entriesWidth(entries);
2137
+ const singleRowW = baseW + ew;
2138
+ if (singleRowW <= maxCapsuleW) {
2139
+ return {
2140
+ width: singleRowW,
2141
+ entryRows: 1,
2142
+ moreCount: 0,
2143
+ visibleEntries: entries.length
2144
+ };
2145
+ }
2146
+ const rowWidth = maxCapsuleW - LEGEND_CAPSULE_PAD * 2;
2147
+ let row = 1;
2148
+ let rowX = pw + 4;
2149
+ let visible = 0;
2150
+ for (let i = 0; i < entries.length; i++) {
2151
+ const ew2 = entryWidth(entries[i].value);
2152
+ if (rowX + ew2 > rowWidth && rowX > pw + 4) {
2153
+ row++;
2154
+ rowX = 0;
2155
+ if (row > LEGEND_MAX_ENTRY_ROWS) {
2156
+ return {
2157
+ width: maxCapsuleW,
2158
+ entryRows: LEGEND_MAX_ENTRY_ROWS,
2159
+ moreCount: entries.length - visible,
2160
+ visibleEntries: visible
2161
+ };
2162
+ }
2163
+ }
2164
+ rowX += ew2;
2165
+ visible++;
2166
+ }
2167
+ return {
2168
+ width: maxCapsuleW,
2169
+ entryRows: row,
2170
+ moreCount: 0,
2171
+ visibleEntries: entries.length
2172
+ };
2173
+ }
2174
+ function computeLegendLayout(config, state, containerWidth) {
2175
+ const { groups, controls: configControls, mode } = config;
2176
+ const isExport = mode === "inline";
2177
+ const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
2178
+ if (isExport && !activeGroupName) {
2179
+ return {
2180
+ height: 0,
2181
+ width: 0,
2182
+ rows: [],
2183
+ controls: [],
2184
+ pills: [],
2185
+ activeCapsule: void 0
2186
+ };
2187
+ }
2188
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
2189
+ if (visibleGroups.length === 0 && (!configControls || configControls.length === 0)) {
2190
+ return {
2191
+ height: 0,
2192
+ width: 0,
2193
+ rows: [],
2194
+ controls: [],
2195
+ pills: [],
2196
+ activeCapsule: void 0
2197
+ };
2198
+ }
2199
+ const controlLayouts = [];
2200
+ let totalControlsW = 0;
2201
+ if (configControls && !isExport) {
2202
+ for (const ctrl of configControls) {
2203
+ const w = controlWidth(ctrl);
2204
+ controlLayouts.push({
2205
+ id: ctrl.id,
2206
+ x: 0,
2207
+ // positioned later
2208
+ y: 0,
2209
+ width: w,
2210
+ height: LEGEND_HEIGHT,
2211
+ icon: ctrl.icon,
2212
+ label: ctrl.label,
2213
+ exportBehavior: ctrl.exportBehavior,
2214
+ children: ctrl.children?.map((c) => ({
2215
+ id: c.id,
2216
+ label: c.label,
2217
+ x: 0,
2218
+ y: 0,
2219
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2220
+ isActive: c.isActive
2221
+ }))
2222
+ });
2223
+ totalControlsW += w + CONTROL_GAP;
2224
+ }
2225
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2226
+ } else if (configControls && isExport) {
2227
+ for (const ctrl of configControls) {
2228
+ if (ctrl.exportBehavior === "strip") continue;
2229
+ const w = controlWidth(ctrl);
2230
+ controlLayouts.push({
2231
+ id: ctrl.id,
2232
+ x: 0,
2233
+ y: 0,
2234
+ width: w,
2235
+ height: LEGEND_HEIGHT,
2236
+ icon: ctrl.icon,
2237
+ label: ctrl.label,
2238
+ exportBehavior: ctrl.exportBehavior,
2239
+ children: ctrl.children?.map((c) => ({
2240
+ id: c.id,
2241
+ label: c.label,
2242
+ x: 0,
2243
+ y: 0,
2244
+ width: measureLegendText(c.label, CONTROL_FONT_SIZE) + 12,
2245
+ isActive: c.isActive
2246
+ }))
2247
+ });
2248
+ totalControlsW += w + CONTROL_GAP;
2249
+ }
2250
+ if (totalControlsW > 0) totalControlsW -= CONTROL_GAP;
2251
+ }
2252
+ const controlsSpace = totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0;
2253
+ const groupAvailW = containerWidth - controlsSpace;
2254
+ const pills = [];
2255
+ let activeCapsule;
2256
+ for (const g of visibleGroups) {
2257
+ const isActive = activeGroupName === g.name.toLowerCase();
2258
+ if (isExport && !isActive) continue;
2259
+ if (isActive) {
2260
+ activeCapsule = buildCapsuleLayout(
2261
+ g,
2262
+ containerWidth,
2263
+ config.capsulePillAddonWidth ?? 0
2264
+ );
2265
+ } else {
2266
+ const pw = pillWidth(g.name);
2267
+ pills.push({
2268
+ groupName: g.name,
2269
+ x: 0,
2270
+ y: 0,
2271
+ width: pw,
2272
+ height: LEGEND_HEIGHT,
2273
+ isActive: false
2274
+ });
2275
+ }
2276
+ }
2277
+ const rows = layoutRows(
2278
+ activeCapsule,
2279
+ pills,
2280
+ controlLayouts,
2281
+ groupAvailW,
2282
+ containerWidth,
2283
+ totalControlsW
2284
+ );
2285
+ const height = rows.length * LEGEND_HEIGHT;
2286
+ const width = containerWidth;
2287
+ return {
2288
+ height,
2289
+ width,
2290
+ rows,
2291
+ activeCapsule,
2292
+ controls: controlLayouts,
2293
+ pills
2294
+ };
2295
+ }
2296
+ function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
2297
+ const pw = pillWidth(group.name);
2298
+ const info = capsuleWidth(
2299
+ group.name,
2300
+ group.entries,
2301
+ containerWidth,
2302
+ addonWidth
2303
+ );
2304
+ const pill = {
2305
+ groupName: group.name,
2306
+ x: LEGEND_CAPSULE_PAD,
2307
+ y: LEGEND_CAPSULE_PAD,
2308
+ width: pw,
2309
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
2310
+ isActive: true
2311
+ };
2312
+ const entries = [];
2313
+ let ex = LEGEND_CAPSULE_PAD + pw + 4 + addonWidth;
2314
+ let ey = 0;
2315
+ let rowX = ex;
2316
+ const maxRowW = containerWidth - LEGEND_CAPSULE_PAD * 2;
2317
+ let currentRow = 0;
2318
+ for (let i = 0; i < info.visibleEntries; i++) {
2319
+ const entry = group.entries[i];
2320
+ const ew = entryWidth(entry.value);
2321
+ if (rowX + ew > maxRowW && rowX > ex && i > 0) {
2322
+ currentRow++;
2323
+ rowX = 0;
2324
+ ey = currentRow * LEGEND_HEIGHT;
2325
+ if (currentRow === 0) ex = LEGEND_CAPSULE_PAD + pw + 4;
2326
+ }
2327
+ const dotCx = rowX + LEGEND_DOT_R;
2328
+ const dotCy = ey + LEGEND_HEIGHT / 2;
2329
+ const textX = rowX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
2330
+ const textY = ey + LEGEND_HEIGHT / 2;
2331
+ entries.push({
2332
+ value: entry.value,
2333
+ color: entry.color,
2334
+ x: rowX,
2335
+ y: ey,
2336
+ dotCx,
2337
+ dotCy,
2338
+ textX,
2339
+ textY
2340
+ });
2341
+ rowX += ew;
2342
+ }
2343
+ const totalRows = info.entryRows;
2344
+ const capsuleH = totalRows * LEGEND_HEIGHT;
2345
+ return {
2346
+ groupName: group.name,
2347
+ x: 0,
2348
+ y: 0,
2349
+ width: info.width,
2350
+ height: capsuleH,
2351
+ pill,
2352
+ entries,
2353
+ moreCount: info.moreCount > 0 ? info.moreCount : void 0,
2354
+ addonX: addonWidth > 0 ? LEGEND_CAPSULE_PAD + pw + 4 : void 0
2355
+ };
2356
+ }
2357
+ function layoutRows(activeCapsule, pills, controls, groupAvailW, containerWidth, totalControlsW) {
2358
+ const rows = [];
2359
+ const groupItems = [];
2360
+ if (activeCapsule) groupItems.push(activeCapsule);
2361
+ groupItems.push(...pills);
2362
+ let currentRowItems = [];
2363
+ let currentRowW = 0;
2364
+ let rowY = 0;
2365
+ for (const item of groupItems) {
2366
+ const itemW = item.width + LEGEND_GROUP_GAP;
2367
+ if (currentRowW + item.width > groupAvailW && currentRowItems.length > 0) {
2368
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2369
+ rows.push({ y: rowY, items: currentRowItems });
2370
+ rowY += LEGEND_HEIGHT;
2371
+ currentRowItems = [];
2372
+ currentRowW = 0;
2373
+ }
2374
+ item.x = currentRowW;
2375
+ item.y = rowY;
2376
+ currentRowItems.push(item);
2377
+ currentRowW += itemW;
2378
+ }
2379
+ if (controls.length > 0) {
2380
+ let cx = containerWidth;
2381
+ for (let i = controls.length - 1; i >= 0; i--) {
2382
+ cx -= controls[i].width;
2383
+ controls[i].x = cx;
2384
+ controls[i].y = 0;
2385
+ cx -= CONTROL_GAP;
2386
+ }
2387
+ if (rows.length > 0) {
2388
+ rows[0].items.push(...controls);
2389
+ } else if (currentRowItems.length > 0) {
2390
+ currentRowItems.push(...controls);
2391
+ } else {
2392
+ currentRowItems.push(...controls);
2393
+ }
2394
+ }
2395
+ if (currentRowItems.length > 0) {
2396
+ centerRowItems(currentRowItems, containerWidth, totalControlsW);
2397
+ rows.push({ y: rowY, items: currentRowItems });
2398
+ }
2399
+ if (rows.length === 0) {
2400
+ rows.push({ y: 0, items: [] });
2401
+ }
2402
+ return rows;
2403
+ }
2404
+ function centerRowItems(items, containerWidth, totalControlsW) {
2405
+ const groupItems = items.filter((it) => "groupName" in it);
2406
+ if (groupItems.length === 0) return;
2407
+ const totalGroupW = groupItems.reduce((s, it) => s + it.width, 0) + (groupItems.length - 1) * LEGEND_GROUP_GAP;
2408
+ const availW = containerWidth - (totalControlsW > 0 ? totalControlsW + LEGEND_GROUP_GAP * 2 : 0);
2409
+ const offset = Math.max(0, (availW - totalGroupW) / 2);
2410
+ let x = offset;
2411
+ for (const item of groupItems) {
2412
+ item.x = x;
2413
+ x += item.width + LEGEND_GROUP_GAP;
2414
+ }
2415
+ }
2416
+ function getLegendReservedHeight(config, state, containerWidth) {
2417
+ const layout = computeLegendLayout(config, state, containerWidth);
2418
+ return layout.height;
2419
+ }
2420
+ var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
2421
+ var init_legend_layout = __esm({
2422
+ "src/utils/legend-layout.ts"() {
2423
+ "use strict";
2424
+ init_legend_constants();
2425
+ CONTROL_PILL_PAD = 16;
2426
+ CONTROL_FONT_SIZE = 11;
2427
+ CONTROL_ICON_GAP = 4;
2428
+ CONTROL_GAP = 8;
2429
+ }
2430
+ });
2431
+
2432
+ // src/utils/legend-d3.ts
2433
+ function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
2434
+ const width = containerWidth ?? parseFloat(container.attr("width") || "800");
2435
+ let currentState = { ...state };
2436
+ let currentLayout;
2437
+ const legendG = container.append("g").attr("class", "dgmo-legend");
2438
+ function render2() {
2439
+ currentLayout = computeLegendLayout(config, currentState, width);
2440
+ legendG.selectAll("*").remove();
2441
+ if (currentLayout.height === 0) return;
2442
+ if (currentState.activeGroup) {
2443
+ legendG.attr(
2444
+ "data-legend-active",
2445
+ currentState.activeGroup.toLowerCase()
2446
+ );
2447
+ } else {
2448
+ legendG.attr("data-legend-active", null);
2449
+ }
2450
+ const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
2451
+ const pillBorder = mix(palette.textMuted, palette.bg, 50);
2452
+ if (currentLayout.activeCapsule) {
2453
+ renderCapsule(
2454
+ legendG,
2455
+ currentLayout.activeCapsule,
2456
+ palette,
2457
+ groupBg,
2458
+ pillBorder,
2459
+ isDark,
2460
+ callbacks
2461
+ );
2462
+ }
2463
+ for (const pill of currentLayout.pills) {
2464
+ renderPill(legendG, pill, palette, groupBg, callbacks);
2465
+ }
2466
+ for (const ctrl of currentLayout.controls) {
2467
+ renderControl(
2468
+ legendG,
2469
+ ctrl,
2470
+ palette,
2471
+ groupBg,
2472
+ pillBorder,
2473
+ isDark,
2474
+ config.controls
2475
+ );
2476
+ }
2477
+ }
2478
+ render2();
2479
+ return {
2480
+ setState(newState) {
2481
+ currentState = { ...newState };
2482
+ render2();
2483
+ },
2484
+ destroy() {
2485
+ legendG.remove();
2486
+ },
2487
+ getHeight() {
2488
+ return currentLayout?.height ?? 0;
2489
+ },
2490
+ getLayout() {
2491
+ return currentLayout;
2492
+ }
2493
+ };
2494
+ }
2495
+ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, callbacks) {
2496
+ const g = parent.append("g").attr("transform", `translate(${capsule.x},${capsule.y})`).attr("data-legend-group", capsule.groupName.toLowerCase()).style("cursor", "pointer");
2497
+ g.append("rect").attr("width", capsule.width).attr("height", capsule.height).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
2498
+ const pill = capsule.pill;
2499
+ 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);
2500
+ 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);
2501
+ 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);
2502
+ for (const entry of capsule.entries) {
2503
+ const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
2504
+ entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
2505
+ 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);
2506
+ if (callbacks?.onEntryHover) {
2507
+ const groupName = capsule.groupName;
2508
+ const entryValue = entry.value;
2509
+ const onHover = callbacks.onEntryHover;
2510
+ entryG.on("mouseenter", () => onHover(groupName, entryValue)).on("mouseleave", () => onHover(groupName, null));
2511
+ }
2512
+ }
2513
+ if (capsule.moreCount) {
2514
+ const lastEntry = capsule.entries[capsule.entries.length - 1];
2515
+ const moreX = lastEntry ? lastEntry.textX + measureLegendText(lastEntry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_DOT_GAP * 2 : pill.x + pill.width + 8;
2516
+ const moreY = lastEntry?.textY ?? LEGEND_HEIGHT / 2;
2517
+ 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`);
2518
+ }
2519
+ if (callbacks?.onGroupToggle) {
2520
+ const cb = callbacks.onGroupToggle;
2521
+ const name = capsule.groupName;
2522
+ g.on("click", () => cb(name));
2523
+ }
2524
+ if (callbacks?.onGroupRendered) {
2525
+ callbacks.onGroupRendered(capsule.groupName, g, true);
2526
+ }
2527
+ }
2528
+ function renderPill(parent, pill, palette, groupBg, callbacks) {
2529
+ const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
2530
+ g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
2531
+ 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);
2532
+ if (callbacks?.onGroupToggle) {
2533
+ const cb = callbacks.onGroupToggle;
2534
+ const name = pill.groupName;
2535
+ g.on("click", () => cb(name));
2536
+ }
2537
+ if (callbacks?.onGroupRendered) {
2538
+ callbacks.onGroupRendered(pill.groupName, g, false);
2539
+ }
2540
+ }
2541
+ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, configControls) {
2542
+ const g = parent.append("g").attr("transform", `translate(${ctrl.x},${ctrl.y})`).attr("data-legend-control", ctrl.id).style("cursor", "pointer");
2543
+ if (ctrl.exportBehavior === "strip") {
2544
+ g.attr("data-export-ignore", "true");
2545
+ }
2546
+ 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);
2547
+ let textX = ctrl.width / 2;
2548
+ if (ctrl.icon && ctrl.label) {
2549
+ const iconG = g.append("g").attr("transform", `translate(8,${(ctrl.height - 14) / 2})`);
2550
+ iconG.html(ctrl.icon);
2551
+ textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
2552
+ }
2553
+ if (ctrl.label) {
2554
+ 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);
2555
+ }
2556
+ if (ctrl.children) {
2557
+ let cx = ctrl.width + 4;
2558
+ for (const child of ctrl.children) {
2559
+ const childG = g.append("g").attr("transform", `translate(${cx},0)`).style("cursor", "pointer");
2560
+ childG.append("rect").attr("width", child.width).attr("height", ctrl.height).attr("rx", ctrl.height / 2).attr(
2561
+ "fill",
2562
+ child.isActive ? palette.primary ?? palette.text : "none"
2563
+ ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
2564
+ 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);
2565
+ const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
2566
+ const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
2567
+ if (configChild?.onClick) {
2568
+ const onClick = configChild.onClick;
2569
+ childG.on("click", () => onClick());
2570
+ }
2571
+ cx += child.width + 4;
2572
+ }
2573
+ }
2574
+ const configCtrl = configControls?.find((c) => c.id === ctrl.id);
2575
+ if (configCtrl?.onClick) {
2576
+ const onClick = configCtrl.onClick;
2577
+ g.on("click", () => onClick());
2578
+ }
2579
+ }
2580
+ var init_legend_d3 = __esm({
2581
+ "src/utils/legend-d3.ts"() {
2582
+ "use strict";
2583
+ init_legend_constants();
2584
+ init_legend_layout();
2585
+ init_color_utils();
2586
+ init_fonts();
2587
+ }
2588
+ });
2589
+
1926
2590
  // src/utils/title-constants.ts
1927
2591
  var TITLE_FONT_SIZE, TITLE_FONT_WEIGHT, TITLE_Y, TITLE_OFFSET;
1928
2592
  var init_title_constants = __esm({
@@ -2968,7 +3632,8 @@ function parseSequenceDgmo(content) {
2968
3632
  if (top.block.type === "if") {
2969
3633
  const branch = {
2970
3634
  label: elseIfMatch[1].trim(),
2971
- children: []
3635
+ children: [],
3636
+ lineNumber
2972
3637
  };
2973
3638
  if (!top.block.elseIfBranches) top.block.elseIfBranches = [];
2974
3639
  top.block.elseIfBranches.push(branch);
@@ -2991,6 +3656,7 @@ function parseSequenceDgmo(content) {
2991
3656
  if (top.block.type === "if") {
2992
3657
  top.inElse = true;
2993
3658
  top.activeElseIfBranch = void 0;
3659
+ top.block.elseLineNumber = lineNumber;
2994
3660
  }
2995
3661
  }
2996
3662
  continue;
@@ -4852,10 +5518,10 @@ var init_chart = __esm({
4852
5518
  function esc(s) {
4853
5519
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
4854
5520
  }
4855
- function pillWidth(name) {
5521
+ function pillWidth2(name) {
4856
5522
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
4857
5523
  }
4858
- function entriesWidth(entries) {
5524
+ function entriesWidth2(entries) {
4859
5525
  let w = 0;
4860
5526
  for (const e of entries) {
4861
5527
  w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
@@ -4863,9 +5529,9 @@ function entriesWidth(entries) {
4863
5529
  return w;
4864
5530
  }
4865
5531
  function groupTotalWidth(name, entries, isActive) {
4866
- const pw = pillWidth(name);
5532
+ const pw = pillWidth2(name);
4867
5533
  if (!isActive) return pw;
4868
- return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth(entries);
5534
+ return LEGEND_CAPSULE_PAD * 2 + pw + 4 + entriesWidth2(entries);
4869
5535
  }
4870
5536
  function renderLegendSvg(groups, options) {
4871
5537
  if (groups.length === 0) return { svg: "", height: 0, width: 0 };
@@ -4873,7 +5539,7 @@ function renderLegendSvg(groups, options) {
4873
5539
  const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
4874
5540
  const items = groups.filter((g) => g.entries.length > 0).map((g) => {
4875
5541
  const isActive = !!activeGroup && g.name.toLowerCase() === activeGroup.toLowerCase();
4876
- const pw = pillWidth(g.name);
5542
+ const pw = pillWidth2(g.name);
4877
5543
  const tw = groupTotalWidth(g.name, g.entries, isActive);
4878
5544
  return { group: g, isActive, pillWidth: pw, totalWidth: tw };
4879
5545
  });
@@ -4927,6 +5593,19 @@ function renderLegendSvg(groups, options) {
4927
5593
  const svg = `<g${classAttr}${activeAttr}>${parts.join("")}</g>`;
4928
5594
  return { svg, height: LEGEND_HEIGHT, width: totalWidth };
4929
5595
  }
5596
+ function renderLegendSvgFromConfig(config, state, palette, containerWidth) {
5597
+ return renderLegendSvg(config.groups, {
5598
+ palette: {
5599
+ bg: palette.bg,
5600
+ surface: palette.surface,
5601
+ text: palette.text,
5602
+ textMuted: palette.textMuted
5603
+ },
5604
+ isDark: palette.isDark,
5605
+ containerWidth,
5606
+ activeGroup: state.activeGroup
5607
+ });
5608
+ }
4930
5609
  var init_legend_svg = __esm({
4931
5610
  "src/utils/legend-svg.ts"() {
4932
5611
  "use strict";
@@ -5370,7 +6049,8 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5370
6049
  }
5371
6050
  const { textColor, axisLineColor, gridOpacity, colors, titleConfig } = buildChartCommons(parsed, palette, isDark);
5372
6051
  if (parsed.type === "sankey") {
5373
- return buildSankeyOption(parsed, textColor, colors, titleConfig);
6052
+ const bg = isDark ? palette.surface : palette.bg;
6053
+ return buildSankeyOption(parsed, textColor, colors, bg, titleConfig);
5374
6054
  }
5375
6055
  if (parsed.type === "chord") {
5376
6056
  const bg = isDark ? palette.surface : palette.bg;
@@ -5413,7 +6093,7 @@ function buildExtendedChartOption(parsed, palette, isDark) {
5413
6093
  titleConfig
5414
6094
  );
5415
6095
  }
5416
- function buildSankeyOption(parsed, textColor, colors, titleConfig) {
6096
+ function buildSankeyOption(parsed, textColor, colors, bg, titleConfig) {
5417
6097
  const nodeSet = /* @__PURE__ */ new Set();
5418
6098
  if (parsed.links) {
5419
6099
  for (const link of parsed.links) {
@@ -5421,12 +6101,15 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5421
6101
  nodeSet.add(link.target);
5422
6102
  }
5423
6103
  }
5424
- const nodes = Array.from(nodeSet).map((name, index) => ({
5425
- name,
5426
- itemStyle: {
5427
- color: parsed.nodeColors?.[name] ?? colors[index % colors.length]
5428
- }
5429
- }));
6104
+ const tintNode = (c) => mix(c, bg, 75);
6105
+ const tintLink = (c) => mix(c, bg, 45);
6106
+ const nodeColorMap = /* @__PURE__ */ new Map();
6107
+ const nodes = Array.from(nodeSet).map((name, index) => {
6108
+ const raw = parsed.nodeColors?.[name] ?? colors[index % colors.length];
6109
+ const tinted = tintNode(raw);
6110
+ nodeColorMap.set(name, tintLink(raw));
6111
+ return { name, itemStyle: { color: tinted } };
6112
+ });
5430
6113
  return {
5431
6114
  ...CHART_BASE,
5432
6115
  title: titleConfig,
@@ -5449,11 +6132,13 @@ function buildSankeyOption(parsed, textColor, colors, titleConfig) {
5449
6132
  source: link.source,
5450
6133
  target: link.target,
5451
6134
  value: link.value,
5452
- ...link.color && { lineStyle: { color: link.color } }
6135
+ lineStyle: {
6136
+ color: link.color ? tintLink(link.color) : nodeColorMap.get(link.source)
6137
+ }
5453
6138
  })),
5454
6139
  lineStyle: {
5455
- color: "gradient",
5456
- curveness: 0.5
6140
+ curveness: 0.5,
6141
+ opacity: 0.6
5457
6142
  },
5458
6143
  label: {
5459
6144
  color: textColor,
@@ -5710,16 +6395,6 @@ function getExtendedChartLegendGroups(parsed, colors) {
5710
6395
  }
5711
6396
  return [];
5712
6397
  }
5713
- function rectsOverlap(a, b) {
5714
- 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;
5715
- }
5716
- function rectCircleOverlap(rect, circle) {
5717
- const nearestX = Math.max(rect.x, Math.min(circle.cx, rect.x + rect.w));
5718
- const nearestY = Math.max(rect.y, Math.min(circle.cy, rect.y + rect.h));
5719
- const dx = nearestX - circle.cx;
5720
- const dy = nearestY - circle.cy;
5721
- return dx * dx + dy * dy < circle.r * circle.r;
5722
- }
5723
6398
  function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize, bg) {
5724
6399
  const labelHeight = fontSize + 4;
5725
6400
  const stepSize = labelHeight + 2;
@@ -6096,12 +6771,17 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6096
6771
  maxValue = Math.max(maxValue, value);
6097
6772
  });
6098
6773
  });
6774
+ const CHAR_WIDTH7 = 7;
6775
+ const ESTIMATED_CHART_WIDTH = 900;
6776
+ const longestCol = Math.max(...columns.map((c) => c.length), 0);
6777
+ const slotWidth = columns.length > 0 ? ESTIMATED_CHART_WIDTH / columns.length : Infinity;
6778
+ const needsRotation = longestCol * CHAR_WIDTH7 > slotWidth * 0.85;
6099
6779
  return {
6100
6780
  ...CHART_BASE,
6101
6781
  title: titleConfig,
6102
6782
  grid: {
6103
6783
  left: "3%",
6104
- right: "10%",
6784
+ right: "3%",
6105
6785
  bottom: "3%",
6106
6786
  top: parsed.title ? "15%" : "5%",
6107
6787
  containLabel: true
@@ -6109,6 +6789,7 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6109
6789
  xAxis: {
6110
6790
  type: "category",
6111
6791
  data: columns,
6792
+ position: "top",
6112
6793
  splitArea: {
6113
6794
  show: true
6114
6795
  },
@@ -6117,12 +6798,19 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6117
6798
  },
6118
6799
  axisLabel: {
6119
6800
  color: textColor,
6120
- fontSize: 16
6801
+ fontSize: 12,
6802
+ interval: 0,
6803
+ ...needsRotation && {
6804
+ rotate: -45,
6805
+ width: 200,
6806
+ overflow: "none"
6807
+ }
6121
6808
  }
6122
6809
  },
6123
6810
  yAxis: {
6124
6811
  type: "category",
6125
6812
  data: rowLabels,
6813
+ inverse: true,
6126
6814
  splitArea: {
6127
6815
  show: true
6128
6816
  },
@@ -6131,16 +6819,14 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6131
6819
  },
6132
6820
  axisLabel: {
6133
6821
  color: textColor,
6134
- fontSize: 16
6822
+ fontSize: 12,
6823
+ interval: 0
6135
6824
  }
6136
6825
  },
6137
6826
  visualMap: {
6827
+ show: false,
6138
6828
  min: minValue,
6139
6829
  max: maxValue,
6140
- calculable: true,
6141
- orient: "vertical",
6142
- right: "2%",
6143
- top: "center",
6144
6830
  inRange: {
6145
6831
  color: [
6146
6832
  mix(palette.primary, bg, 30),
@@ -6148,9 +6834,6 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6148
6834
  mix(palette.colors.yellow, bg, 30),
6149
6835
  mix(palette.colors.orange, bg, 30)
6150
6836
  ]
6151
- },
6152
- textStyle: {
6153
- color: textColor
6154
6837
  }
6155
6838
  },
6156
6839
  series: [
@@ -6168,9 +6851,8 @@ function buildHeatmapOption(parsed, palette, isDark, textColor, axisLineColor, t
6168
6851
  fontWeight: "bold"
6169
6852
  },
6170
6853
  emphasis: {
6171
- ...EMPHASIS_SELF
6172
- },
6173
- blur: BLUR_DIM
6854
+ disabled: true
6855
+ }
6174
6856
  }
6175
6857
  ]
6176
6858
  };
@@ -7025,6 +7707,7 @@ var init_echarts = __esm({
7025
7707
  init_fonts();
7026
7708
  init_branding();
7027
7709
  init_legend_svg();
7710
+ init_label_layout();
7028
7711
  init_palettes();
7029
7712
  init_color_utils();
7030
7713
  init_chart();
@@ -8379,7 +9062,12 @@ __export(parser_exports7, {
8379
9062
  function parseArrowLine(trimmed, palette) {
8380
9063
  const bareMatch = trimmed.match(BARE_ARROW_RE);
8381
9064
  if (bareMatch) {
8382
- return { target: bareMatch[1].trim() };
9065
+ const rawTarget = bareMatch[1].trim();
9066
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
9067
+ return {
9068
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
9069
+ targetIsGroup: !!groupMatch
9070
+ };
8383
9071
  }
8384
9072
  const arrowMatch = trimmed.match(ARROW_RE);
8385
9073
  if (arrowMatch) {
@@ -8388,8 +9076,14 @@ function parseArrowLine(trimmed, palette) {
8388
9076
  if (label && !color) {
8389
9077
  color = inferArrowColor(label);
8390
9078
  }
8391
- const target = arrowMatch[3].trim();
8392
- return { label, color, target };
9079
+ const rawTarget = arrowMatch[3].trim();
9080
+ const groupMatch = rawTarget.match(/^\[(.+)\]$/);
9081
+ return {
9082
+ label,
9083
+ color,
9084
+ target: groupMatch ? groupMatch[1].trim() : rawTarget,
9085
+ targetIsGroup: !!groupMatch
9086
+ };
8393
9087
  }
8394
9088
  return null;
8395
9089
  }
@@ -8451,6 +9145,7 @@ function parseSitemap(content, palette) {
8451
9145
  const aliasMap = /* @__PURE__ */ new Map();
8452
9146
  const indentStack = [];
8453
9147
  const labelToNode = /* @__PURE__ */ new Map();
9148
+ const labelToContainer = /* @__PURE__ */ new Map();
8454
9149
  const deferredArrows = [];
8455
9150
  for (let i = 0; i < lines.length; i++) {
8456
9151
  const line10 = lines[i];
@@ -8552,6 +9247,7 @@ function parseSitemap(content, palette) {
8552
9247
  deferredArrows.push({
8553
9248
  sourceNode: source,
8554
9249
  targetLabel: arrowInfo.target,
9250
+ targetIsGroup: arrowInfo.targetIsGroup,
8555
9251
  label: arrowInfo.label,
8556
9252
  color: arrowInfo.color,
8557
9253
  lineNumber
@@ -8585,6 +9281,7 @@ function parseSitemap(content, palette) {
8585
9281
  color
8586
9282
  };
8587
9283
  attachNode2(node, indent, indentStack, result);
9284
+ labelToContainer.set(label.toLowerCase(), node);
8588
9285
  } else if (metadataMatch && indentStack.length > 0) {
8589
9286
  const rawKey = metadataMatch[1].trim().toLowerCase();
8590
9287
  const key = aliasMap.get(rawKey) ?? rawKey;
@@ -8625,22 +9322,41 @@ function parseSitemap(content, palette) {
8625
9322
  }
8626
9323
  for (const arrow of deferredArrows) {
8627
9324
  const targetKey = arrow.targetLabel.toLowerCase();
8628
- const targetNode = labelToNode.get(targetKey);
8629
- if (!targetNode) {
8630
- const allLabels = Array.from(labelToNode.keys());
8631
- let msg = `Arrow target "${arrow.targetLabel}" not found`;
8632
- const hint = suggest(targetKey, allLabels);
8633
- if (hint) msg += `. ${hint}`;
8634
- pushError(arrow.lineNumber, msg);
8635
- continue;
9325
+ if (arrow.targetIsGroup) {
9326
+ const targetContainer = labelToContainer.get(targetKey);
9327
+ if (!targetContainer) {
9328
+ const allLabels = Array.from(labelToContainer.keys());
9329
+ let msg = `Group '[${arrow.targetLabel}]' not found`;
9330
+ const hint = suggest(targetKey, allLabels);
9331
+ if (hint) msg += `. ${hint}`;
9332
+ pushError(arrow.lineNumber, msg);
9333
+ continue;
9334
+ }
9335
+ result.edges.push({
9336
+ sourceId: arrow.sourceNode.id,
9337
+ targetId: targetContainer.id,
9338
+ label: arrow.label,
9339
+ color: arrow.color,
9340
+ lineNumber: arrow.lineNumber
9341
+ });
9342
+ } else {
9343
+ const targetNode = labelToNode.get(targetKey);
9344
+ if (!targetNode) {
9345
+ const allLabels = Array.from(labelToNode.keys());
9346
+ let msg = `Arrow target "${arrow.targetLabel}" not found`;
9347
+ const hint = suggest(targetKey, allLabels);
9348
+ if (hint) msg += `. ${hint}`;
9349
+ pushError(arrow.lineNumber, msg);
9350
+ continue;
9351
+ }
9352
+ result.edges.push({
9353
+ sourceId: arrow.sourceNode.id,
9354
+ targetId: targetNode.id,
9355
+ label: arrow.label,
9356
+ color: arrow.color,
9357
+ lineNumber: arrow.lineNumber
9358
+ });
8636
9359
  }
8637
- result.edges.push({
8638
- sourceId: arrow.sourceNode.id,
8639
- targetId: targetNode.id,
8640
- label: arrow.label,
8641
- color: arrow.color,
8642
- lineNumber: arrow.lineNumber
8643
- });
8644
9360
  }
8645
9361
  if (result.tagGroups.length > 0) {
8646
9362
  const allNodes = [];
@@ -10354,6 +11070,7 @@ function parseBoxesAndLines(content) {
10354
11070
  const nodeLabels = /* @__PURE__ */ new Set();
10355
11071
  const groupLabels = /* @__PURE__ */ new Set();
10356
11072
  let lastNodeLabel = null;
11073
+ let lastSourceIsGroup = false;
10357
11074
  const groupStack = [];
10358
11075
  let contentStarted = false;
10359
11076
  let currentTagGroup = null;
@@ -10592,6 +11309,8 @@ function parseBoxesAndLines(content) {
10592
11309
  };
10593
11310
  groupLabels.add(label);
10594
11311
  groupStack.push({ group, indent, depth: currentDepth });
11312
+ lastNodeLabel = label;
11313
+ lastSourceIsGroup = true;
10595
11314
  continue;
10596
11315
  }
10597
11316
  if (trimmed.includes("->") || trimmed.includes("<->")) {
@@ -10609,7 +11328,8 @@ function parseBoxesAndLines(content) {
10609
11328
  );
10610
11329
  continue;
10611
11330
  }
10612
- edgeText = `${lastNodeLabel} ${trimmed}`;
11331
+ const sourcePrefix = lastSourceIsGroup ? `[${lastNodeLabel}]` : lastNodeLabel;
11332
+ edgeText = `${sourcePrefix} ${trimmed}`;
10613
11333
  }
10614
11334
  const edge = parseEdgeLine(
10615
11335
  edgeText,
@@ -10632,6 +11352,7 @@ function parseBoxesAndLines(content) {
10632
11352
  continue;
10633
11353
  }
10634
11354
  lastNodeLabel = node.label;
11355
+ lastSourceIsGroup = false;
10635
11356
  const gs = currentGroupState();
10636
11357
  const isGroupChild = gs && indent > gs.indent;
10637
11358
  if (nodeLabels.has(node.label)) {
@@ -10659,14 +11380,42 @@ function parseBoxesAndLines(content) {
10659
11380
  const gs = groupStack.pop();
10660
11381
  result.groups.push(gs.group);
10661
11382
  }
11383
+ const validEdges = [];
10662
11384
  for (const edge of result.edges) {
10663
- if (!edge.source.startsWith("__group_")) {
11385
+ let valid = true;
11386
+ if (edge.source.startsWith("__group_")) {
11387
+ const label = edge.source.slice("__group_".length);
11388
+ const found = [...groupLabels].some(
11389
+ (g) => g.toLowerCase() === label.toLowerCase()
11390
+ );
11391
+ if (!found) {
11392
+ result.diagnostics.push(
11393
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11394
+ );
11395
+ valid = false;
11396
+ }
11397
+ } else {
10664
11398
  ensureNode(edge.source, edge.lineNumber);
10665
11399
  }
10666
- if (!edge.target.startsWith("__group_")) {
11400
+ if (edge.target.startsWith("__group_")) {
11401
+ const label = edge.target.slice("__group_".length);
11402
+ const found = [...groupLabels].some(
11403
+ (g) => g.toLowerCase() === label.toLowerCase()
11404
+ );
11405
+ if (!found) {
11406
+ result.diagnostics.push(
11407
+ makeDgmoError(edge.lineNumber, `Group '[${label}]' not found`)
11408
+ );
11409
+ valid = false;
11410
+ }
11411
+ } else {
10667
11412
  ensureNode(edge.target, edge.lineNumber);
10668
11413
  }
11414
+ if (valid) {
11415
+ validEdges.push(edge);
11416
+ }
10669
11417
  }
11418
+ result.edges = validEdges;
10670
11419
  if (result.tagGroups.length > 0) {
10671
11420
  injectDefaultTagMetadata(result.nodes, result.tagGroups);
10672
11421
  validateTagValues(result.nodes, result.tagGroups, pushWarning, suggest);
@@ -10695,10 +11444,14 @@ function parseNodeLine(trimmed, lineNum, aliasMap, _diagnostics) {
10695
11444
  description
10696
11445
  };
10697
11446
  }
11447
+ function resolveEndpoint(name) {
11448
+ const m = name.match(/^\[(.+)\]$/);
11449
+ return m ? groupId2(m[1].trim()) : name;
11450
+ }
10698
11451
  function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10699
11452
  const biLabeledMatch = trimmed.match(/^(.+?)\s*<-(.+)->\s*(.+)$/);
10700
11453
  if (biLabeledMatch) {
10701
- const source2 = biLabeledMatch[1].trim();
11454
+ const source2 = resolveEndpoint(biLabeledMatch[1].trim());
10702
11455
  const label = biLabeledMatch[2].trim();
10703
11456
  let rest2 = biLabeledMatch[3].trim();
10704
11457
  let metadata2 = {};
@@ -10719,7 +11472,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10719
11472
  }
10720
11473
  return {
10721
11474
  source: source2,
10722
- target: rest2,
11475
+ target: resolveEndpoint(rest2),
10723
11476
  label: label || void 0,
10724
11477
  bidirectional: true,
10725
11478
  lineNumber: lineNum,
@@ -10728,7 +11481,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10728
11481
  }
10729
11482
  const biIdx = trimmed.indexOf("<->");
10730
11483
  if (biIdx >= 0) {
10731
- const source2 = trimmed.slice(0, biIdx).trim();
11484
+ const source2 = resolveEndpoint(trimmed.slice(0, biIdx).trim());
10732
11485
  let rest2 = trimmed.slice(biIdx + 3).trim();
10733
11486
  let metadata2 = {};
10734
11487
  const pipeIdx2 = rest2.indexOf("|");
@@ -10748,7 +11501,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10748
11501
  }
10749
11502
  return {
10750
11503
  source: source2,
10751
- target: rest2,
11504
+ target: resolveEndpoint(rest2),
10752
11505
  bidirectional: true,
10753
11506
  lineNumber: lineNum,
10754
11507
  metadata: metadata2
@@ -10756,7 +11509,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10756
11509
  }
10757
11510
  const labeledMatch = trimmed.match(/^(.+?)\s+-(.+)->\s*(.+)$/);
10758
11511
  if (labeledMatch) {
10759
- const source2 = labeledMatch[1].trim();
11512
+ const source2 = resolveEndpoint(labeledMatch[1].trim());
10760
11513
  const label = labeledMatch[2].trim();
10761
11514
  let rest2 = labeledMatch[3].trim();
10762
11515
  if (label) {
@@ -10778,7 +11531,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10778
11531
  }
10779
11532
  return {
10780
11533
  source: source2,
10781
- target: rest2,
11534
+ target: resolveEndpoint(rest2),
10782
11535
  label,
10783
11536
  bidirectional: false,
10784
11537
  lineNumber: lineNum,
@@ -10788,7 +11541,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10788
11541
  }
10789
11542
  const arrowIdx = trimmed.indexOf("->");
10790
11543
  if (arrowIdx < 0) return null;
10791
- const source = trimmed.slice(0, arrowIdx).trim();
11544
+ const source = resolveEndpoint(trimmed.slice(0, arrowIdx).trim());
10792
11545
  let rest = trimmed.slice(arrowIdx + 2).trim();
10793
11546
  if (!source || !rest) {
10794
11547
  diagnostics.push(
@@ -10809,7 +11562,7 @@ function parseEdgeLine(trimmed, lineNum, aliasMap, diagnostics) {
10809
11562
  }
10810
11563
  return {
10811
11564
  source,
10812
- target: rest,
11565
+ target: resolveEndpoint(rest),
10813
11566
  bidirectional: false,
10814
11567
  lineNumber: lineNum,
10815
11568
  metadata
@@ -11123,14 +11876,14 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11123
11876
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
11124
11877
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
11125
11878
  if (visibleEntries.length === 0) continue;
11126
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11127
- const minPillWidth = pillWidth2;
11128
- let entriesWidth2 = 0;
11879
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD2;
11880
+ const minPillWidth = pillWidth3;
11881
+ let entriesWidth3 = 0;
11129
11882
  for (const entry of visibleEntries) {
11130
- entriesWidth2 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11883
+ entriesWidth3 += LEGEND_DOT_R2 * 2 + LEGEND_ENTRY_DOT_GAP2 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL2;
11131
11884
  }
11132
11885
  const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
11133
- const capsuleWidth = LEGEND_CAPSULE_PAD2 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
11886
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD2 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
11134
11887
  groups.push({
11135
11888
  name: group.name,
11136
11889
  alias: group.alias,
@@ -11140,7 +11893,7 @@ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
11140
11893
  })),
11141
11894
  x: 0,
11142
11895
  y: 0,
11143
- width: capsuleWidth,
11896
+ width: capsuleWidth2,
11144
11897
  height: LEGEND_HEIGHT2,
11145
11898
  minifiedWidth: minPillWidth,
11146
11899
  minifiedHeight: LEGEND_HEIGHT2
@@ -12081,66 +12834,77 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
12081
12834
  }
12082
12835
  }
12083
12836
  if (fixedLegend || legendOnly || exportDims && hasLegend) {
12084
- const visibleGroups = layout.legend.filter((group) => {
12085
- if (legendOnly) return true;
12086
- if (activeTagGroup == null) return true;
12087
- return group.name.toLowerCase() === activeTagGroup.toLowerCase();
12088
- });
12089
- let fixedPositions;
12090
- if (fixedLegend && visibleGroups.length > 0) {
12091
- fixedPositions = /* @__PURE__ */ new Map();
12092
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
12093
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
12094
- let cx = (width - totalW) / 2;
12095
- for (const g of visibleGroups) {
12096
- fixedPositions.set(g.name, cx);
12097
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
12098
- }
12099
- }
12100
- const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG;
12101
- const legendParent = legendParentBase;
12102
- if (fixedLegend && activeTagGroup) {
12103
- legendParentBase.attr("data-legend-active", activeTagGroup.toLowerCase());
12837
+ const groups = layout.legend.map((g) => ({
12838
+ name: g.name,
12839
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
12840
+ }));
12841
+ const eyeAddonWidth = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12842
+ const legendParentBase = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr("transform", `translate(0, ${DIAGRAM_PADDING + titleReserve})`) : contentG.append("g");
12843
+ let legendHandle;
12844
+ if (legendOnly) {
12845
+ for (const lg of layout.legend) {
12846
+ const singleConfig = {
12847
+ groups: [
12848
+ {
12849
+ name: lg.name,
12850
+ entries: lg.entries.map((e) => ({
12851
+ value: e.value,
12852
+ color: e.color
12853
+ }))
12854
+ }
12855
+ ],
12856
+ position: { placement: "top-center", titleRelation: "below-title" },
12857
+ mode: "fixed"
12858
+ };
12859
+ const singleState = { activeGroup: lg.name };
12860
+ const groupG = legendParentBase.append("g").attr("transform", `translate(${lg.x}, ${lg.y})`);
12861
+ renderLegendD3(
12862
+ groupG,
12863
+ singleConfig,
12864
+ singleState,
12865
+ palette,
12866
+ isDark,
12867
+ void 0,
12868
+ lg.width
12869
+ );
12870
+ groupG.selectAll("[data-legend-group]").classed("org-legend-group", true);
12871
+ }
12872
+ legendHandle = null;
12873
+ } else {
12874
+ const legendConfig = {
12875
+ groups,
12876
+ position: { placement: "top-center", titleRelation: "below-title" },
12877
+ mode: "fixed",
12878
+ capsulePillAddonWidth: eyeAddonWidth
12879
+ };
12880
+ const legendState = { activeGroup: activeTagGroup ?? null };
12881
+ legendHandle = renderLegendD3(
12882
+ legendParentBase,
12883
+ legendConfig,
12884
+ legendState,
12885
+ palette,
12886
+ isDark,
12887
+ void 0,
12888
+ fixedLegend ? width : layout.width
12889
+ );
12890
+ legendParentBase.selectAll("[data-legend-group]").classed("org-legend-group", true);
12104
12891
  }
12105
- for (const group of visibleGroups) {
12106
- const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
12107
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
12108
- const pillLabel = group.name;
12109
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
12110
- const gX = fixedPositions?.get(group.name) ?? group.x;
12111
- const gY = fixedPositions ? 0 : group.y;
12112
- const gEl = legendParent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "org-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", legendOnly ? "default" : "pointer");
12113
- if (isActive) {
12114
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
12115
- }
12116
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
12117
- const pillYOff = LEGEND_CAPSULE_PAD;
12118
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
12119
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
12120
- if (isActive) {
12121
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
12122
- }
12123
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
12124
- if (isActive && fixedLegend) {
12125
- const groupKey = group.name.toLowerCase();
12892
+ if (fixedLegend && legendHandle) {
12893
+ const computedLayout = legendHandle.getLayout();
12894
+ if (computedLayout.activeCapsule?.addonX != null) {
12895
+ const capsule = computedLayout.activeCapsule;
12896
+ const groupKey = capsule.groupName.toLowerCase();
12126
12897
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
12127
- const eyeX = pillXOff + pillWidth2 + LEGEND_EYE_GAP;
12128
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12129
- const hitPad = 6;
12130
- const eyeG = gEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
12131
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
12132
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12133
- }
12134
- if (isActive) {
12135
- const eyeShift = fixedLegend ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
12136
- let entryX = pillXOff + pillWidth2 + 4 + eyeShift;
12137
- for (const entry of group.entries) {
12138
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
12139
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
12140
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
12141
- const entryLabel = entry.value;
12142
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entryLabel);
12143
- entryX = textX + measureLegendText(entryLabel, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
12898
+ const activeGroupEl = legendParentBase.select(
12899
+ `[data-legend-group="${groupKey}"]`
12900
+ );
12901
+ if (!activeGroupEl.empty()) {
12902
+ const eyeX = capsule.addonX;
12903
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
12904
+ const hitPad = 6;
12905
+ 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);
12906
+ 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");
12907
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
12144
12908
  }
12145
12909
  }
12146
12910
  }
@@ -12175,6 +12939,7 @@ var init_renderer = __esm({
12175
12939
  init_parser4();
12176
12940
  init_layout();
12177
12941
  init_legend_constants();
12942
+ init_legend_d3();
12178
12943
  init_title_constants();
12179
12944
  DIAGRAM_PADDING = 20;
12180
12945
  MAX_SCALE = 3;
@@ -12203,6 +12968,17 @@ var layout_exports2 = {};
12203
12968
  __export(layout_exports2, {
12204
12969
  layoutSitemap: () => layoutSitemap
12205
12970
  });
12971
+ function clipToRectBorder(cx, cy, w, h, tx, ty) {
12972
+ const dx = tx - cx;
12973
+ const dy = ty - cy;
12974
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
12975
+ const hw = w / 2;
12976
+ const hh = h / 2;
12977
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
12978
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
12979
+ const s = Math.min(sx, sy);
12980
+ return { x: cx + dx * s, y: cy + dy * s };
12981
+ }
12206
12982
  function filterMetadata2(metadata, hiddenAttributes) {
12207
12983
  if (!hiddenAttributes || hiddenAttributes.size === 0) return metadata;
12208
12984
  const filtered = {};
@@ -12219,7 +12995,10 @@ function computeCardWidth2(label, meta) {
12219
12995
  const lineChars = key.length + 2 + value.length;
12220
12996
  if (lineChars > maxChars) maxChars = lineChars;
12221
12997
  }
12222
- return Math.max(MIN_CARD_WIDTH2, Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2);
12998
+ return Math.max(
12999
+ MIN_CARD_WIDTH2,
13000
+ Math.ceil(maxChars * CHAR_WIDTH2) + CARD_H_PAD2 * 2
13001
+ );
12223
13002
  }
12224
13003
  function computeCardHeight2(meta) {
12225
13004
  const metaCount = Object.keys(meta).length;
@@ -12228,7 +13007,12 @@ function computeCardHeight2(meta) {
12228
13007
  }
12229
13008
  function resolveNodeColor2(node, tagGroups, activeGroupName) {
12230
13009
  if (node.color) return node.color;
12231
- return resolveTagColor(node.metadata, tagGroups, activeGroupName, node.isContainer);
13010
+ return resolveTagColor(
13011
+ node.metadata,
13012
+ tagGroups,
13013
+ activeGroupName,
13014
+ node.isContainer
13015
+ );
12232
13016
  }
12233
13017
  function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12234
13018
  const groups = [];
@@ -12237,21 +13021,21 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
12237
13021
  const usedValues = usedValuesByGroup?.get(group.name.toLowerCase());
12238
13022
  const visibleEntries = usedValues ? group.entries.filter((e) => usedValues.has(e.value.toLowerCase())) : group.entries;
12239
13023
  if (visibleEntries.length === 0) continue;
12240
- const pillWidth2 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
12241
- const minPillWidth = pillWidth2;
12242
- let entriesWidth2 = 0;
13024
+ const pillWidth3 = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD3;
13025
+ const minPillWidth = pillWidth3;
13026
+ let entriesWidth3 = 0;
12243
13027
  for (const entry of visibleEntries) {
12244
- entriesWidth2 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
13028
+ entriesWidth3 += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL3;
12245
13029
  }
12246
13030
  const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
12247
- const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth2 + 4 + eyeSpace + entriesWidth2;
13031
+ const capsuleWidth2 = LEGEND_CAPSULE_PAD3 * 2 + pillWidth3 + 4 + eyeSpace + entriesWidth3;
12248
13032
  groups.push({
12249
13033
  name: group.name,
12250
13034
  alias: group.alias,
12251
13035
  entries: visibleEntries.map((e) => ({ value: e.value, color: e.color })),
12252
13036
  x: 0,
12253
13037
  y: 0,
12254
- width: capsuleWidth,
13038
+ width: capsuleWidth2,
12255
13039
  height: LEGEND_HEIGHT3,
12256
13040
  minifiedWidth: minPillWidth,
12257
13041
  minifiedHeight: LEGEND_HEIGHT3
@@ -12271,10 +13055,20 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12271
13055
  parentPageId,
12272
13056
  meta,
12273
13057
  fullMeta: { ...node.metadata },
12274
- width: Math.max(MIN_CARD_WIDTH2, node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2),
13058
+ width: Math.max(
13059
+ MIN_CARD_WIDTH2,
13060
+ node.label.length * CHAR_WIDTH2 + CARD_H_PAD2 * 2
13061
+ ),
12275
13062
  height: labelHeight + CONTAINER_PAD_BOTTOM2
12276
13063
  });
12277
- flattenNodes(node.children, node.id, parentPageId, hiddenCounts, hiddenAttributes, result);
13064
+ flattenNodes(
13065
+ node.children,
13066
+ node.id,
13067
+ parentPageId,
13068
+ hiddenCounts,
13069
+ hiddenAttributes,
13070
+ result
13071
+ );
12278
13072
  } else {
12279
13073
  result.push({
12280
13074
  sitemapNode: node,
@@ -12286,14 +13080,28 @@ function flattenNodes(nodes, parentContainerId, parentPageId, hiddenCounts, hidd
12286
13080
  height: computeCardHeight2(meta)
12287
13081
  });
12288
13082
  if (node.children.length > 0) {
12289
- flattenNodes(node.children, parentContainerId, node.id, hiddenCounts, hiddenAttributes, result);
13083
+ flattenNodes(
13084
+ node.children,
13085
+ parentContainerId,
13086
+ node.id,
13087
+ hiddenCounts,
13088
+ hiddenAttributes,
13089
+ result
13090
+ );
12290
13091
  }
12291
13092
  }
12292
13093
  }
12293
13094
  }
12294
13095
  function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expandAllLegend) {
12295
13096
  if (parsed.roots.length === 0) {
12296
- return { nodes: [], edges: [], containers: [], legend: [], width: 0, height: 0 };
13097
+ return {
13098
+ nodes: [],
13099
+ edges: [],
13100
+ containers: [],
13101
+ legend: [],
13102
+ width: 0,
13103
+ height: 0
13104
+ };
12297
13105
  }
12298
13106
  const allNodes = [];
12299
13107
  const collect = (node) => {
@@ -12301,9 +13109,20 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12301
13109
  for (const child of node.children) collect(child);
12302
13110
  };
12303
13111
  for (const root of parsed.roots) collect(root);
12304
- injectDefaultTagMetadata(allNodes, parsed.tagGroups, (e) => e.isContainer);
13112
+ injectDefaultTagMetadata(
13113
+ allNodes,
13114
+ parsed.tagGroups,
13115
+ (e) => e.isContainer
13116
+ );
12305
13117
  const flatNodes = [];
12306
- flattenNodes(parsed.roots, null, null, hiddenCounts, hiddenAttributes, flatNodes);
13118
+ flattenNodes(
13119
+ parsed.roots,
13120
+ null,
13121
+ null,
13122
+ hiddenCounts,
13123
+ hiddenAttributes,
13124
+ flatNodes
13125
+ );
12307
13126
  const nodeMap = /* @__PURE__ */ new Map();
12308
13127
  for (const flat of flatNodes) {
12309
13128
  nodeMap.set(flat.sitemapNode.id, flat);
@@ -12365,14 +13184,29 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12365
13184
  g.setParent(flat.sitemapNode.id, flat.parentContainerId);
12366
13185
  }
12367
13186
  }
13187
+ const expandedContainerIds = /* @__PURE__ */ new Set();
13188
+ for (const cid of containerIds) {
13189
+ if (!collapsedContainerIds.has(cid)) {
13190
+ expandedContainerIds.add(cid);
13191
+ }
13192
+ }
13193
+ const deferredEdgeIndices = [];
12368
13194
  for (let i = 0; i < parsed.edges.length; i++) {
12369
13195
  const edge = parsed.edges[i];
12370
- if (g.hasNode(edge.sourceId) && g.hasNode(edge.targetId)) {
12371
- g.setEdge(edge.sourceId, edge.targetId, {
13196
+ if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
13197
+ if (expandedContainerIds.has(edge.sourceId) || expandedContainerIds.has(edge.targetId)) {
13198
+ deferredEdgeIndices.push(i);
13199
+ continue;
13200
+ }
13201
+ g.setEdge(
13202
+ edge.sourceId,
13203
+ edge.targetId,
13204
+ {
12372
13205
  label: edge.label ?? "",
12373
13206
  minlen: 1
12374
- }, `e${i}`);
12375
- }
13207
+ },
13208
+ `e${i}`
13209
+ );
12376
13210
  }
12377
13211
  import_dagre.default.layout(g);
12378
13212
  const layoutNodes = [];
@@ -12440,19 +13274,52 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12440
13274
  });
12441
13275
  }
12442
13276
  }
13277
+ const deferredSet = new Set(deferredEdgeIndices);
12443
13278
  const layoutEdges = [];
12444
13279
  for (let i = 0; i < parsed.edges.length; i++) {
12445
13280
  const edge = parsed.edges[i];
12446
13281
  if (!g.hasNode(edge.sourceId) || !g.hasNode(edge.targetId)) continue;
12447
- const edgeData = g.edge({ v: edge.sourceId, w: edge.targetId, name: `e${i}` });
12448
- if (!edgeData) continue;
13282
+ let points;
13283
+ if (deferredSet.has(i)) {
13284
+ const srcNode = g.node(edge.sourceId);
13285
+ const tgtNode = g.node(edge.targetId);
13286
+ if (!srcNode || !tgtNode) continue;
13287
+ const srcPt = clipToRectBorder(
13288
+ srcNode.x,
13289
+ srcNode.y,
13290
+ srcNode.width,
13291
+ srcNode.height,
13292
+ tgtNode.x,
13293
+ tgtNode.y
13294
+ );
13295
+ const tgtPt = clipToRectBorder(
13296
+ tgtNode.x,
13297
+ tgtNode.y,
13298
+ tgtNode.width,
13299
+ tgtNode.height,
13300
+ srcNode.x,
13301
+ srcNode.y
13302
+ );
13303
+ const midX = (srcPt.x + tgtPt.x) / 2;
13304
+ const midY = (srcPt.y + tgtPt.y) / 2;
13305
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
13306
+ } else {
13307
+ const edgeData = g.edge({
13308
+ v: edge.sourceId,
13309
+ w: edge.targetId,
13310
+ name: `e${i}`
13311
+ });
13312
+ if (!edgeData) continue;
13313
+ points = edgeData.points ?? [];
13314
+ }
12449
13315
  layoutEdges.push({
12450
13316
  sourceId: edge.sourceId,
12451
13317
  targetId: edge.targetId,
12452
- points: edgeData.points ?? [],
13318
+ points,
12453
13319
  label: edge.label,
12454
13320
  color: edge.color,
12455
- lineNumber: edge.lineNumber
13321
+ lineNumber: edge.lineNumber,
13322
+ deferred: deferredSet.has(i) || void 0
12456
13323
  });
12457
13324
  }
12458
13325
  {
@@ -12613,7 +13480,9 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
12613
13480
  usedValuesByGroup.set(key, used);
12614
13481
  }
12615
13482
  const legendGroups = computeLegendGroups2(parsed.tagGroups, usedValuesByGroup);
12616
- const visibleGroups = activeTagGroup != null ? legendGroups.filter((g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()) : legendGroups;
13483
+ const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13484
+ (g2) => g2.name.toLowerCase() === activeTagGroup.toLowerCase()
13485
+ ) : legendGroups;
12617
13486
  const allExpanded = expandAllLegend && activeTagGroup == null;
12618
13487
  const effectiveW = (g2) => activeTagGroup != null || allExpanded ? g2.width : g2.minifiedWidth;
12619
13488
  if (visibleGroups.length > 0) {
@@ -12928,7 +13797,8 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
12928
13797
  const edgeG = contentG.append("g").attr("class", "sitemap-edge-group").attr("data-line-number", String(edge.lineNumber));
12929
13798
  const edgeColor3 = edge.color ?? palette.textMuted;
12930
13799
  const markerId = edge.color ? `sm-arrow-${edge.color.replace("#", "")}` : "sm-arrow";
12931
- const pathD = lineGenerator(edge.points);
13800
+ const gen = edge.deferred ? lineGeneratorLinear : lineGenerator;
13801
+ const pathD = gen(edge.points);
12932
13802
  if (pathD) {
12933
13803
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor3).attr("stroke-width", EDGE_STROKE_WIDTH2).attr("marker-end", `url(#${markerId})`).attr("class", "sitemap-edge");
12934
13804
  }
@@ -13014,62 +13884,49 @@ function renderSitemap(container, parsed, layout, palette, isDark, onClickItem,
13014
13884
  activeTagGroup,
13015
13885
  width,
13016
13886
  hiddenAttributes
13017
- );
13018
- }
13019
- }
13020
- function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13021
- if (legendGroups.length === 0) return;
13022
- const visibleGroups = activeTagGroup != null ? legendGroups.filter(
13023
- (g) => g.name.toLowerCase() === activeTagGroup.toLowerCase()
13024
- ) : legendGroups;
13025
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13026
- let fixedPositions;
13027
- if (fixedWidth != null && visibleGroups.length > 0) {
13028
- fixedPositions = /* @__PURE__ */ new Map();
13029
- const effectiveW = (g) => activeTagGroup != null ? g.width : g.minifiedWidth;
13030
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
13031
- let cx = (fixedWidth - totalW) / 2;
13032
- for (const g of visibleGroups) {
13033
- fixedPositions.set(g.name, cx);
13034
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
13035
- }
13887
+ );
13036
13888
  }
13037
- for (const group of visibleGroups) {
13038
- const isActive = activeTagGroup != null;
13039
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13040
- const gX = fixedPositions?.get(group.name) ?? group.x;
13041
- const gY = fixedPositions ? 0 : group.y;
13042
- const legendG = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "sitemap-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
13043
- if (isActive) {
13044
- legendG.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13045
- }
13046
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
13047
- const pillYOff = LEGEND_CAPSULE_PAD;
13048
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13049
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
13050
- if (isActive) {
13051
- legendG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13052
- }
13053
- legendG.append("text").attr("x", pillXOff + pillW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13054
- if (isActive && fixedWidth != null) {
13055
- const groupKey = group.name.toLowerCase();
13889
+ }
13890
+ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fixedWidth, hiddenAttributes) {
13891
+ if (legendGroups.length === 0) return;
13892
+ const groups = legendGroups.map((g) => ({
13893
+ name: g.name,
13894
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
13895
+ }));
13896
+ const isFixedMode = fixedWidth != null;
13897
+ const eyeAddonWidth = isFixedMode ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13898
+ const legendConfig = {
13899
+ groups,
13900
+ position: { placement: "top-center", titleRelation: "below-title" },
13901
+ mode: "fixed",
13902
+ capsulePillAddonWidth: eyeAddonWidth
13903
+ };
13904
+ const legendState = { activeGroup: activeTagGroup ?? null };
13905
+ const containerWidth = fixedWidth ?? legendGroups[0]?.x + (legendGroups[0]?.width ?? 200);
13906
+ const legendHandle = renderLegendD3(
13907
+ parent,
13908
+ legendConfig,
13909
+ legendState,
13910
+ palette,
13911
+ isDark,
13912
+ void 0,
13913
+ containerWidth
13914
+ );
13915
+ parent.selectAll("[data-legend-group]").classed("sitemap-legend-group", true);
13916
+ if (isFixedMode) {
13917
+ const computedLayout = legendHandle.getLayout();
13918
+ if (computedLayout.activeCapsule?.addonX != null) {
13919
+ const capsule = computedLayout.activeCapsule;
13920
+ const groupKey = capsule.groupName.toLowerCase();
13056
13921
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
13057
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP;
13058
- const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13059
- const hitPad = 6;
13060
- const eyeG = legendG.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
13061
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE + hitPad * 2).attr("height", LEGEND_EYE_SIZE + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
13062
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13063
- }
13064
- if (isActive) {
13065
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
13066
- let entryX = pillXOff + pillW + 4 + eyeShift;
13067
- for (const entry of group.entries) {
13068
- const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13069
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13070
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
13071
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13072
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13922
+ const activeGroupEl = parent.select(`[data-legend-group="${groupKey}"]`);
13923
+ if (!activeGroupEl.empty()) {
13924
+ const eyeX = capsule.addonX;
13925
+ const eyeY = (LEGEND_HEIGHT - LEGEND_EYE_SIZE) / 2;
13926
+ const hitPad = 6;
13927
+ 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);
13928
+ 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");
13929
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
13073
13930
  }
13074
13931
  }
13075
13932
  }
@@ -13123,7 +13980,7 @@ async function renderSitemapForExport(content, theme, palette) {
13123
13980
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
13124
13981
  return injectBranding2(svgHtml, brandColor);
13125
13982
  }
13126
- var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator;
13983
+ var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_FIXED_GAP2, lineGenerator, lineGeneratorLinear;
13127
13984
  var init_renderer2 = __esm({
13128
13985
  "src/sitemap/renderer.ts"() {
13129
13986
  "use strict";
@@ -13132,6 +13989,7 @@ var init_renderer2 = __esm({
13132
13989
  init_fonts();
13133
13990
  init_color_utils();
13134
13991
  init_legend_constants();
13992
+ init_legend_d3();
13135
13993
  init_title_constants();
13136
13994
  DIAGRAM_PADDING2 = 20;
13137
13995
  MAX_SCALE2 = 3;
@@ -13155,6 +14013,7 @@ var init_renderer2 = __esm({
13155
14013
  COLLAPSE_BAR_HEIGHT2 = 6;
13156
14014
  LEGEND_FIXED_GAP2 = 8;
13157
14015
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
14016
+ lineGeneratorLinear = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveLinear);
13158
14017
  }
13159
14018
  });
13160
14019
 
@@ -13416,8 +14275,7 @@ function computeLayout(parsed, _palette) {
13416
14275
  currentX += cl.width + COLUMN_GAP;
13417
14276
  }
13418
14277
  const totalWidth = currentX - COLUMN_GAP + DIAGRAM_PADDING3;
13419
- const legendSpace = parsed.tagGroups.length > 0 ? LEGEND_HEIGHT : 0;
13420
- const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3 + legendSpace;
14278
+ const totalHeight = startY + maxColumnHeight + DIAGRAM_PADDING3;
13421
14279
  return { columns: columnLayouts, totalWidth, totalHeight };
13422
14280
  }
13423
14281
  function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exportDims, activeTagGroup) {
@@ -13430,54 +14288,25 @@ function renderKanban(container, parsed, palette, isDark, _onNavigateToLine, exp
13430
14288
  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);
13431
14289
  }
13432
14290
  if (parsed.tagGroups.length > 0) {
13433
- const legendY = height - LEGEND_HEIGHT;
13434
- let legendX = DIAGRAM_PADDING3;
13435
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13436
- const capsulePad = LEGEND_CAPSULE_PAD;
13437
- const legendContainer = svg.append("g").attr("class", "kanban-legend");
13438
- if (activeTagGroup) {
13439
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
13440
- }
13441
- for (const group of parsed.tagGroups) {
13442
- const isActive = activeTagGroup?.toLowerCase() === group.name.toLowerCase();
13443
- if (activeTagGroup != null && !isActive) continue;
13444
- const pillTextWidth = group.name.length * LEGEND_PILL_FONT_SIZE * 0.6;
13445
- const pillWidth2 = pillTextWidth + 16;
13446
- let capsuleContentWidth = pillWidth2;
13447
- if (isActive) {
13448
- capsuleContentWidth += 4;
13449
- for (const entry of group.entries) {
13450
- capsuleContentWidth += LEGEND_DOT_R * 2 + 4 + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13451
- }
13452
- }
13453
- const capsuleWidth = capsuleContentWidth + capsulePad * 2;
13454
- if (isActive) {
13455
- legendContainer.append("rect").attr("x", legendX).attr("y", legendY).attr("width", capsuleWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13456
- }
13457
- const pillX = legendX + (isActive ? capsulePad : 0);
13458
- const pillBg = isActive ? palette.bg : groupBg;
13459
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", pillBg).attr("class", "kanban-legend-group").attr("data-legend-group", group.name.toLowerCase());
13460
- if (isActive) {
13461
- legendContainer.append("rect").attr("x", pillX).attr("y", legendY + capsulePad).attr("width", pillWidth2).attr("height", LEGEND_HEIGHT - capsulePad * 2).attr("rx", (LEGEND_HEIGHT - capsulePad * 2) / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13462
- }
13463
- legendContainer.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", legendY + LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(group.name);
13464
- if (isActive) {
13465
- let entryX = pillX + pillWidth2 + 4;
13466
- for (const entry of group.entries) {
13467
- const entryG = legendContainer.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
13468
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", legendY + LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
13469
- const entryTextX = entryX + LEGEND_DOT_R * 2 + 4;
13470
- entryG.append("text").attr("x", entryTextX).attr(
13471
- "y",
13472
- legendY + LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1
13473
- ).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
13474
- entryX = entryTextX + entry.value.length * LEGEND_ENTRY_FONT_SIZE * 0.6 + 8;
13475
- }
13476
- legendX += capsuleWidth + 12;
13477
- } else {
13478
- legendX += pillWidth2 + 12;
13479
- }
13480
- }
14291
+ const titleTextWidth = parsed.title ? measureLegendText(parsed.title, TITLE_FONT_SIZE) + 16 : 0;
14292
+ const legendX = DIAGRAM_PADDING3 + titleTextWidth;
14293
+ const legendY = DIAGRAM_PADDING3 + (TITLE_FONT_SIZE - LEGEND_HEIGHT) / 2;
14294
+ const legendConfig = {
14295
+ groups: parsed.tagGroups,
14296
+ position: { placement: "top-center", titleRelation: "below-title" },
14297
+ mode: exportDims ? "inline" : "fixed"
14298
+ };
14299
+ const legendState = { activeGroup: activeTagGroup ?? null };
14300
+ const legendG = svg.append("g").attr("class", "kanban-legend").attr("transform", `translate(${legendX},${legendY})`);
14301
+ renderLegendD3(
14302
+ legendG,
14303
+ legendConfig,
14304
+ legendState,
14305
+ palette,
14306
+ isDark,
14307
+ void 0,
14308
+ width - legendX - DIAGRAM_PADDING3
14309
+ );
13481
14310
  }
13482
14311
  const defaultColBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13483
14312
  const defaultColHeaderBg = isDark ? mix(palette.surface, palette.bg, 70) : mix(palette.surface, palette.bg, 50);
@@ -13573,6 +14402,7 @@ var init_renderer3 = __esm({
13573
14402
  init_parser5();
13574
14403
  init_mutations();
13575
14404
  init_legend_constants();
14405
+ init_legend_d3();
13576
14406
  init_title_constants();
13577
14407
  DIAGRAM_PADDING3 = 20;
13578
14408
  COLUMN_GAP = 16;
@@ -13766,14 +14596,9 @@ function collectClassTypes(parsed) {
13766
14596
  if (c.color) continue;
13767
14597
  present.add(c.modifier ?? "class");
13768
14598
  }
13769
- return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map((k) => CLASS_TYPE_MAP[k]);
13770
- }
13771
- function legendEntriesWidth(entries) {
13772
- let w = 0;
13773
- for (const e of entries) {
13774
- w += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
13775
- }
13776
- return w;
14599
+ return CLASS_TYPE_ORDER.filter((k) => present.has(k)).map(
14600
+ (k) => CLASS_TYPE_MAP[k]
14601
+ );
13777
14602
  }
13778
14603
  function classTypeKey(modifier) {
13779
14604
  return modifier ?? "class";
@@ -13842,7 +14667,10 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13842
14667
  defs.append("marker").attr("id", "cd-arrow-depend").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13843
14668
  defs.append("marker").attr("id", "cd-arrow-assoc").attr("viewBox", `0 0 ${AW} ${AH}`).attr("refX", AW).attr("refY", AH / 2).attr("markerWidth", AW).attr("markerHeight", AH).attr("orient", "auto").append("polyline").attr("points", `0,0 ${AW},${AH / 2} 0,${AH}`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.5);
13844
14669
  if (parsed.title) {
13845
- const titleEl = svg.append("text").attr("class", "chart-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).style("cursor", onClickItem && parsed.titleLineNumber ? "pointer" : "default").text(parsed.title);
14670
+ 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(
14671
+ "cursor",
14672
+ onClickItem && parsed.titleLineNumber ? "pointer" : "default"
14673
+ ).text(parsed.title);
13846
14674
  if (parsed.titleLineNumber) {
13847
14675
  titleEl.attr("data-line-number", parsed.titleLineNumber);
13848
14676
  if (onClickItem) {
@@ -13856,32 +14684,33 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13856
14684
  }
13857
14685
  const isLegendExpanded = legendActive !== false;
13858
14686
  if (hasLegend) {
13859
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
13860
- const pillWidth2 = measureLegendText(LEGEND_GROUP_NAME, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
13861
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
13862
- const entriesW = legendEntriesWidth(legendEntries);
13863
- const totalW = isLegendExpanded ? LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesW : pillWidth2;
13864
- const legendX = (width - totalW) / 2;
13865
- const legendY = titleHeight;
13866
- const legendG = svg.append("g").attr("class", "cd-legend").attr("data-legend-group", "type").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
13867
- if (isLegendExpanded) {
13868
- legendG.append("rect").attr("width", totalW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13869
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
13870
- legendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
13871
- legendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13872
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
13873
- for (const entry of legendEntries) {
13874
- const color = palette.colors[entry.colorKey];
13875
- const typeKey = CLASS_TYPE_ORDER.find((k) => CLASS_TYPE_MAP[k] === entry);
13876
- const entryG = legendG.append("g").attr("data-legend-entry", typeKey);
13877
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", color);
13878
- entryG.append("text").attr("x", entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.label);
13879
- entryX += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.label, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
14687
+ const legendGroups = [
14688
+ {
14689
+ name: LEGEND_GROUP_NAME,
14690
+ entries: legendEntries.map((entry) => ({
14691
+ value: entry.label,
14692
+ color: palette.colors[entry.colorKey]
14693
+ }))
13880
14694
  }
13881
- } else {
13882
- legendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
13883
- legendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(LEGEND_GROUP_NAME);
13884
- }
14695
+ ];
14696
+ const legendConfig = {
14697
+ groups: legendGroups,
14698
+ position: { placement: "top-center", titleRelation: "below-title" },
14699
+ mode: "fixed"
14700
+ };
14701
+ const legendState = {
14702
+ activeGroup: isLegendExpanded ? LEGEND_GROUP_NAME : null
14703
+ };
14704
+ const legendG = svg.append("g").attr("class", "cd-legend").attr("transform", `translate(0,${titleHeight})`);
14705
+ renderLegendD3(
14706
+ legendG,
14707
+ legendConfig,
14708
+ legendState,
14709
+ palette,
14710
+ isDark,
14711
+ void 0,
14712
+ width
14713
+ );
13885
14714
  }
13886
14715
  const contentG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
13887
14716
  for (const edge of layout.edges) {
@@ -13925,7 +14754,13 @@ function renderClassDiagram(container, parsed, layout, palette, isDark, onClickI
13925
14754
  const colorOff = !!parsed.options?.["no-auto-color"];
13926
14755
  const neutralize = hasLegend && !isLegendExpanded && !node.color;
13927
14756
  const effectiveColor = neutralize ? palette.primary : node.color;
13928
- const fill2 = nodeFill3(palette, isDark, node.modifier, effectiveColor, colorOff);
14757
+ const fill2 = nodeFill3(
14758
+ palette,
14759
+ isDark,
14760
+ node.modifier,
14761
+ effectiveColor,
14762
+ colorOff
14763
+ );
13929
14764
  const stroke2 = nodeStroke3(palette, node.modifier, effectiveColor, colorOff);
13930
14765
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", 3).attr("ry", 3).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", NODE_STROKE_WIDTH3);
13931
14766
  let yPos = -h / 2;
@@ -13994,15 +14829,10 @@ function renderClassDiagramForExport(content, theme, palette) {
13994
14829
  const exportWidth = layout.width + DIAGRAM_PADDING4 * 2;
13995
14830
  const exportHeight = layout.height + DIAGRAM_PADDING4 * 2 + (parsed.title ? 40 : 0) + legendReserve;
13996
14831
  return runInExportContainer(exportWidth, exportHeight, (container) => {
13997
- renderClassDiagram(
13998
- container,
13999
- parsed,
14000
- layout,
14001
- palette,
14002
- isDark,
14003
- void 0,
14004
- { width: exportWidth, height: exportHeight }
14005
- );
14832
+ renderClassDiagram(container, parsed, layout, palette, isDark, void 0, {
14833
+ width: exportWidth,
14834
+ height: exportHeight
14835
+ });
14006
14836
  return extractExportSvg(container, theme);
14007
14837
  });
14008
14838
  }
@@ -14015,6 +14845,7 @@ var init_renderer4 = __esm({
14015
14845
  init_fonts();
14016
14846
  init_export_container();
14017
14847
  init_legend_constants();
14848
+ init_legend_d3();
14018
14849
  init_title_constants();
14019
14850
  init_color_utils();
14020
14851
  init_parser2();
@@ -14621,35 +15452,24 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14621
15452
  }
14622
15453
  }
14623
15454
  if (parsed.tagGroups.length > 0) {
14624
- const LEGEND_PILL_H = LEGEND_HEIGHT - 6;
14625
- const LEGEND_PILL_RX = Math.floor(LEGEND_PILL_H / 2);
14626
- const LEGEND_GAP = 8;
14627
- const legendG = svg.append("g").attr("class", "er-tag-legend");
14628
- if (activeTagGroup) {
14629
- legendG.attr("data-legend-active", activeTagGroup.toLowerCase());
14630
- }
14631
- let legendX = DIAGRAM_PADDING5;
14632
15455
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14633
- for (const group of parsed.tagGroups) {
14634
- const groupG = legendG.append("g").attr("data-legend-group", group.name.toLowerCase());
14635
- const labelText = groupG.append("text").attr("x", legendX).attr("y", legendY + LEGEND_PILL_H / 2).attr("dominant-baseline", "central").attr("fill", palette.textMuted).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(`${group.name}:`);
14636
- const labelWidth = (labelText.node()?.getComputedTextLength?.() ?? group.name.length * 7) + 6;
14637
- legendX += labelWidth;
14638
- for (const entry of group.entries) {
14639
- const pillG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
14640
- const tmpText = legendG.append("text").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14641
- const textW = tmpText.node()?.getComputedTextLength?.() ?? entry.value.length * 7;
14642
- tmpText.remove();
14643
- const pillW = textW + LEGEND_PILL_PAD * 2;
14644
- pillG.append("rect").attr("x", legendX).attr("y", legendY).attr("width", pillW).attr("height", LEGEND_PILL_H).attr("rx", LEGEND_PILL_RX).attr("ry", LEGEND_PILL_RX).attr(
14645
- "fill",
14646
- mix(entry.color, isDark ? palette.surface : palette.bg, 25)
14647
- ).attr("stroke", entry.color).attr("stroke-width", 1);
14648
- pillG.append("text").attr("x", legendX + pillW / 2).attr("y", legendY + LEGEND_PILL_H / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-family", FONT_FAMILY).text(entry.value);
14649
- legendX += pillW + LEGEND_GAP;
14650
- }
14651
- legendX += LEGEND_GROUP_GAP;
14652
- }
15456
+ const legendConfig = {
15457
+ groups: parsed.tagGroups,
15458
+ position: { placement: "top-center", titleRelation: "below-title" },
15459
+ mode: "fixed"
15460
+ };
15461
+ const legendState = { activeGroup: activeTagGroup ?? null };
15462
+ const legendG = svg.append("g").attr("class", "er-tag-legend").attr("transform", `translate(0,${legendY})`);
15463
+ renderLegendD3(
15464
+ legendG,
15465
+ legendConfig,
15466
+ legendState,
15467
+ palette,
15468
+ isDark,
15469
+ void 0,
15470
+ viewW
15471
+ );
15472
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14653
15473
  }
14654
15474
  if (semanticRoles) {
14655
15475
  const presentRoles = ROLE_ORDER.filter((role) => {
@@ -14659,55 +15479,35 @@ function renderERDiagram(container, parsed, layout, palette, isDark, onClickItem
14659
15479
  return false;
14660
15480
  });
14661
15481
  if (presentRoles.length > 0) {
14662
- const measureLabelW = (text, fontSize) => {
14663
- const dummy = svg.append("text").attr("font-size", fontSize).attr("font-family", FONT_FAMILY).attr("visibility", "hidden").text(text);
14664
- const measured = dummy.node()?.getComputedTextLength?.() ?? 0;
14665
- dummy.remove();
14666
- return measured > 0 ? measured : text.length * fontSize * 0.6;
14667
- };
14668
- const labelWidths = /* @__PURE__ */ new Map();
14669
- for (const role of presentRoles) {
14670
- labelWidths.set(
14671
- role,
14672
- measureLabelW(ROLE_LABELS[role], LEGEND_ENTRY_FONT_SIZE)
14673
- );
14674
- }
14675
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
14676
- const groupName = "Role";
14677
- const pillWidth2 = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
14678
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
14679
- let totalWidth;
14680
- let entriesWidth2 = 0;
14681
- if (semanticActive) {
14682
- for (const role of presentRoles) {
14683
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
14684
- }
14685
- totalWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + LEGEND_ENTRY_TRAIL + entriesWidth2;
14686
- } else {
14687
- totalWidth = pillWidth2;
14688
- }
14689
- const legendX = (viewW - totalWidth) / 2;
14690
15482
  const legendY = DIAGRAM_PADDING5 + titleHeight;
14691
- const semanticLegendG = svg.append("g").attr("class", "er-semantic-legend").attr("data-legend-group", "role").attr("transform", `translate(${legendX}, ${legendY})`).style("cursor", "pointer");
14692
- if (semanticActive) {
14693
- semanticLegendG.append("rect").attr("width", totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14694
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", palette.bg);
14695
- semanticLegendG.append("rect").attr("x", LEGEND_CAPSULE_PAD).attr("y", LEGEND_CAPSULE_PAD).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
14696
- semanticLegendG.append("text").attr("x", LEGEND_CAPSULE_PAD + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.text).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14697
- let entryX = LEGEND_CAPSULE_PAD + pillWidth2 + LEGEND_ENTRY_TRAIL;
14698
- for (const role of presentRoles) {
14699
- const label = ROLE_LABELS[role];
14700
- const roleColor = palette.colors[ROLE_COLORS[role]];
14701
- const entryG = semanticLegendG.append("g").attr("data-legend-entry", role);
14702
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", roleColor);
14703
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
14704
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(label);
14705
- entryX = textX + labelWidths.get(role) + LEGEND_ENTRY_TRAIL;
15483
+ const semanticGroups = [
15484
+ {
15485
+ name: "Role",
15486
+ entries: presentRoles.map((role) => ({
15487
+ value: ROLE_LABELS[role],
15488
+ color: palette.colors[ROLE_COLORS[role]]
15489
+ }))
14706
15490
  }
14707
- } else {
14708
- semanticLegendG.append("rect").attr("width", pillWidth2).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
14709
- semanticLegendG.append("text").attr("x", pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", palette.textMuted).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).text(groupName);
14710
- }
15491
+ ];
15492
+ const legendConfig = {
15493
+ groups: semanticGroups,
15494
+ position: { placement: "top-center", titleRelation: "below-title" },
15495
+ mode: "fixed"
15496
+ };
15497
+ const legendState = {
15498
+ activeGroup: semanticActive ? "Role" : null
15499
+ };
15500
+ const legendG = svg.append("g").attr("class", "er-semantic-legend").attr("transform", `translate(0,${legendY})`);
15501
+ renderLegendD3(
15502
+ legendG,
15503
+ legendConfig,
15504
+ legendState,
15505
+ palette,
15506
+ isDark,
15507
+ void 0,
15508
+ viewW
15509
+ );
15510
+ legendG.selectAll("[data-legend-group]").classed("er-legend-group", true);
14711
15511
  }
14712
15512
  }
14713
15513
  }
@@ -14752,6 +15552,7 @@ var init_renderer5 = __esm({
14752
15552
  init_palettes();
14753
15553
  init_tag_groups();
14754
15554
  init_legend_constants();
15555
+ init_legend_d3();
14755
15556
  init_title_constants();
14756
15557
  init_parser3();
14757
15558
  init_layout4();
@@ -14775,6 +15576,17 @@ var layout_exports5 = {};
14775
15576
  __export(layout_exports5, {
14776
15577
  layoutBoxesAndLines: () => layoutBoxesAndLines
14777
15578
  });
15579
+ function clipToRectBorder2(cx, cy, w, h, tx, ty) {
15580
+ const dx = tx - cx;
15581
+ const dy = ty - cy;
15582
+ if (dx === 0 && dy === 0) return { x: cx, y: cy };
15583
+ const hw = w / 2;
15584
+ const hh = h / 2;
15585
+ const sx = dx !== 0 ? hw / Math.abs(dx) : Infinity;
15586
+ const sy = dy !== 0 ? hh / Math.abs(dy) : Infinity;
15587
+ const s = Math.min(sx, sy);
15588
+ return { x: cx + dx * s, y: cy + dy * s };
15589
+ }
14778
15590
  function computeNodeSize(_node) {
14779
15591
  const PHI = 1.618;
14780
15592
  const NODE_HEIGHT = 60;
@@ -14927,13 +15739,25 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14927
15739
  const srcNode = g.node(edge.source);
14928
15740
  const tgtNode = g.node(edge.target);
14929
15741
  if (!srcNode || !tgtNode) continue;
14930
- const midX = (srcNode.x + tgtNode.x) / 2;
14931
- const midY = (srcNode.y + tgtNode.y) / 2;
14932
- points = [
14933
- { x: srcNode.x, y: srcNode.y },
14934
- { x: midX, y: midY },
14935
- { x: tgtNode.x, y: tgtNode.y }
14936
- ];
15742
+ const srcPt = clipToRectBorder2(
15743
+ srcNode.x,
15744
+ srcNode.y,
15745
+ srcNode.width,
15746
+ srcNode.height,
15747
+ tgtNode.x,
15748
+ tgtNode.y
15749
+ );
15750
+ const tgtPt = clipToRectBorder2(
15751
+ tgtNode.x,
15752
+ tgtNode.y,
15753
+ tgtNode.width,
15754
+ tgtNode.height,
15755
+ srcNode.x,
15756
+ srcNode.y
15757
+ );
15758
+ const midX = (srcPt.x + tgtPt.x) / 2;
15759
+ const midY = (srcPt.y + tgtPt.y) / 2;
15760
+ points = [srcPt, { x: midX, y: midY }, tgtPt];
14937
15761
  } else {
14938
15762
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
14939
15763
  points = dagreEdge?.points ?? [];
@@ -14956,7 +15780,8 @@ function layoutBoxesAndLines(parsed, collapseInfo) {
14956
15780
  labelY,
14957
15781
  yOffset: edgeYOffsets[i],
14958
15782
  parallelCount: edgeParallelCounts[i],
14959
- metadata: edge.metadata
15783
+ metadata: edge.metadata,
15784
+ deferred: deferredSet.has(i) || void 0
14960
15785
  });
14961
15786
  }
14962
15787
  let maxX = 0;
@@ -15021,7 +15846,7 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
15021
15846
  const maxTextWidth = nodeWidth - NODE_TEXT_PADDING * 2;
15022
15847
  const lineHeight = 1.3;
15023
15848
  for (let fontSize = NODE_FONT_SIZE; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
15024
- const charWidth2 = fontSize * CHAR_WIDTH_RATIO;
15849
+ const charWidth2 = fontSize * CHAR_WIDTH_RATIO2;
15025
15850
  const maxCharsPerLine = Math.floor(maxTextWidth / charWidth2);
15026
15851
  const maxLines = Math.floor((nodeHeight - 8) / (fontSize * lineHeight));
15027
15852
  if (maxCharsPerLine < 2 || maxLines < 1) continue;
@@ -15073,7 +15898,7 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
15073
15898
  }
15074
15899
  if (hardLines.length <= maxLines) return { lines: hardLines, fontSize };
15075
15900
  }
15076
- const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO;
15901
+ const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO2;
15077
15902
  const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING * 2) / charWidth);
15078
15903
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
15079
15904
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
@@ -15225,18 +16050,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15225
16050
  const edgeG = diagramG.append("g").attr("class", "bl-edge-group").attr("data-line-number", String(le.lineNumber));
15226
16051
  edgeGroups.set(i, edgeG);
15227
16052
  const markerId = `bl-arrow-${color.replace("#", "")}`;
15228
- const path = edgeG.append("path").attr("class", "bl-edge").attr(
15229
- "d",
15230
- (parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR)(
15231
- points
15232
- ) ?? ""
15233
- ).attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
16053
+ const gen = le.deferred ? lineGeneratorLinear2 : parsed.direction === "TB" ? lineGeneratorTB : lineGeneratorLR;
16054
+ const path = edgeG.append("path").attr("class", "bl-edge").attr("d", gen(points) ?? "").attr("fill", "none").attr("stroke", color).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`);
15234
16055
  if (le.bidirectional) {
15235
16056
  const revId = `bl-arrow-rev-${color.replace("#", "")}`;
15236
16057
  path.attr("marker-start", `url(#${revId})`);
15237
16058
  }
15238
16059
  if (le.label && le.labelX != null && le.labelY != null) {
15239
- const lw = le.label.length * EDGE_LABEL_FONT_SIZE4 * CHAR_WIDTH_RATIO;
16060
+ const lw = le.label.length * EDGE_LABEL_FONT_SIZE4 * CHAR_WIDTH_RATIO2;
15240
16061
  labelPositions.push({
15241
16062
  x: le.labelX,
15242
16063
  y: le.labelY + le.yOffset,
@@ -15294,7 +16115,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15294
16115
  const descY = labelY + lineH / 2 + gap + META_FONT_SIZE3 / 2;
15295
16116
  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);
15296
16117
  const maxChars = Math.floor(
15297
- (ln.width - NODE_TEXT_PADDING * 2) / (META_FONT_SIZE3 * CHAR_WIDTH_RATIO)
16118
+ (ln.width - NODE_TEXT_PADDING * 2) / (META_FONT_SIZE3 * CHAR_WIDTH_RATIO2)
15298
16119
  );
15299
16120
  const desc = node.description.length > maxChars ? node.description.slice(0, maxChars - 1) + "\u2026" : node.description;
15300
16121
  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);
@@ -15311,50 +16132,23 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
15311
16132
  }
15312
16133
  }
15313
16134
  if (parsed.tagGroups.length > 0) {
15314
- renderLegend2(svg, parsed, palette, isDark, activeGroup, width, titleOffset);
15315
- }
15316
- }
15317
- function renderLegend2(svg, parsed, palette, isDark, activeGroup, svgWidth, titleOffset) {
15318
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
15319
- const pillBorder = mix(palette.textMuted, palette.bg, 50);
15320
- let totalW = 0;
15321
- for (const tg of parsed.tagGroups) {
15322
- const isActive = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15323
- totalW += measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15324
- if (isActive) {
15325
- totalW += 6;
15326
- for (const entry of tg.entries) {
15327
- totalW += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
15328
- }
15329
- }
15330
- totalW += LEGEND_GROUP_GAP;
15331
- }
15332
- const legendX = Math.max(LEGEND_CAPSULE_PAD, (svgWidth - totalW) / 2);
15333
- const legendY = titleOffset + 4;
15334
- const legendG = svg.append("g").attr("transform", `translate(${legendX},${legendY})`);
15335
- let x = 0;
15336
- for (const tg of parsed.tagGroups) {
15337
- const isActiveGroup = activeGroup?.toLowerCase() === tg.name.toLowerCase();
15338
- const groupG = legendG.append("g").attr("class", "bl-legend-group").attr("data-legend-group", tg.name.toLowerCase()).style("cursor", "pointer");
15339
- const nameW = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
15340
- const tagPill = groupG.append("rect").attr("x", x).attr("y", 0).attr("width", nameW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
15341
- if (isActiveGroup) {
15342
- tagPill.attr("stroke", pillBorder).attr("stroke-width", 0.75);
15343
- }
15344
- groupG.append("text").attr("x", x + nameW / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", isActiveGroup ? palette.text : palette.textMuted).attr("pointer-events", "none").text(tg.name);
15345
- x += nameW;
15346
- if (isActiveGroup) {
15347
- x += 6;
15348
- for (const entry of tg.entries) {
15349
- const entryColor = entry.color || palette.textMuted;
15350
- const ew = measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE);
15351
- const entryG = groupG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
15352
- entryG.append("circle").attr("cx", x + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entryColor);
15353
- entryG.append("text").attr("x", x + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
15354
- x += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + ew + LEGEND_ENTRY_TRAIL;
15355
- }
15356
- }
15357
- x += LEGEND_GROUP_GAP;
16135
+ const legendConfig = {
16136
+ groups: parsed.tagGroups,
16137
+ position: { placement: "top-center", titleRelation: "below-title" },
16138
+ mode: "fixed"
16139
+ };
16140
+ const legendState = { activeGroup };
16141
+ const legendG = svg.append("g").attr("transform", `translate(0,${titleOffset + 4})`);
16142
+ renderLegendD3(
16143
+ legendG,
16144
+ legendConfig,
16145
+ legendState,
16146
+ palette,
16147
+ isDark,
16148
+ void 0,
16149
+ width
16150
+ );
16151
+ legendG.selectAll("[data-legend-group]").classed("bl-legend-group", true);
15358
16152
  }
15359
16153
  }
15360
16154
  function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark, options) {
@@ -15362,7 +16156,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
15362
16156
  exportDims: options?.exportDims
15363
16157
  });
15364
16158
  }
15365
- var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
16159
+ var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, META_FONT_SIZE3, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, lineGeneratorLR, lineGeneratorTB, lineGeneratorLinear2;
15366
16160
  var init_renderer6 = __esm({
15367
16161
  "src/boxes-and-lines/renderer.ts"() {
15368
16162
  "use strict";
@@ -15370,6 +16164,7 @@ var init_renderer6 = __esm({
15370
16164
  d3Shape4 = __toESM(require("d3-shape"), 1);
15371
16165
  init_fonts();
15372
16166
  init_legend_constants();
16167
+ init_legend_d3();
15373
16168
  init_title_constants();
15374
16169
  init_color_utils();
15375
16170
  init_tag_groups();
@@ -15384,12 +16179,13 @@ var init_renderer6 = __esm({
15384
16179
  COLLAPSE_BAR_HEIGHT3 = 4;
15385
16180
  ARROWHEAD_W2 = 5;
15386
16181
  ARROWHEAD_H2 = 4;
15387
- CHAR_WIDTH_RATIO = 0.6;
16182
+ CHAR_WIDTH_RATIO2 = 0.6;
15388
16183
  NODE_TEXT_PADDING = 12;
15389
16184
  GROUP_RX = 8;
15390
16185
  GROUP_LABEL_FONT_SIZE = 14;
15391
16186
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
15392
16187
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneY);
16188
+ lineGeneratorLinear2 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveLinear);
15393
16189
  }
15394
16190
  });
15395
16191
 
@@ -15454,7 +16250,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15454
16250
  }
15455
16251
  const nodeGeometry = /* @__PURE__ */ new Map();
15456
16252
  for (const name of g.nodes()) {
15457
- const pos = g.node(name);
16253
+ const pos = gNode(g, name);
15458
16254
  if (pos)
15459
16255
  nodeGeometry.set(name, {
15460
16256
  y: pos.y,
@@ -15464,14 +16260,14 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15464
16260
  }
15465
16261
  const rankMap = /* @__PURE__ */ new Map();
15466
16262
  for (const name of g.nodes()) {
15467
- const pos = g.node(name);
16263
+ const pos = gNode(g, name);
15468
16264
  if (!pos) continue;
15469
16265
  const rankY = Math.round(pos.y);
15470
16266
  if (!rankMap.has(rankY)) rankMap.set(rankY, []);
15471
16267
  rankMap.get(rankY).push(name);
15472
16268
  }
15473
16269
  for (const [, rankNodes] of rankMap) {
15474
- rankNodes.sort((a, b) => g.node(a).x - g.node(b).x);
16270
+ rankNodes.sort((a, b) => gNode(g, a).x - gNode(g, b).x);
15475
16271
  }
15476
16272
  let anyMoved = false;
15477
16273
  for (const [, rankNodes] of rankMap) {
@@ -15498,10 +16294,10 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15498
16294
  }
15499
16295
  for (const partition of partitions) {
15500
16296
  if (partition.length < 2) continue;
15501
- const xSlots = partition.map((name) => g.node(name).x).sort((a, b) => a - b);
16297
+ const xSlots = partition.map((name) => gNode(g, name).x).sort((a, b) => a - b);
15502
16298
  const basePositions = /* @__PURE__ */ new Map();
15503
16299
  for (const name of g.nodes()) {
15504
- const pos = g.node(name);
16300
+ const pos = gNode(g, name);
15505
16301
  if (pos) basePositions.set(name, pos.x);
15506
16302
  }
15507
16303
  const currentPenalty = computeEdgePenalty(
@@ -15579,7 +16375,7 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15579
16375
  }
15580
16376
  if (bestPerm.some((name, i) => name !== partition[i])) {
15581
16377
  for (let i = 0; i < bestPerm.length; i++) {
15582
- g.node(bestPerm[i]).x = xSlots[i];
16378
+ gNode(g, bestPerm[i]).x = xSlots[i];
15583
16379
  const rankIdx = rankNodes.indexOf(partition[i]);
15584
16380
  if (rankIdx >= 0) rankNodes[rankIdx] = bestPerm[i];
15585
16381
  }
@@ -15589,10 +16385,10 @@ function reduceCrossings(g, edgeList, nodeGroupMap) {
15589
16385
  }
15590
16386
  if (anyMoved) {
15591
16387
  for (const edge of edgeList) {
15592
- const edgeData = g.edge(edge.source, edge.target);
16388
+ const edgeData = gEdge(g, edge.source, edge.target);
15593
16389
  if (!edgeData) continue;
15594
- const srcPos = g.node(edge.source);
15595
- const tgtPos = g.node(edge.target);
16390
+ const srcPos = gNode(g, edge.source);
16391
+ const tgtPos = gNode(g, edge.target);
15596
16392
  if (!srcPos || !tgtPos) continue;
15597
16393
  const srcBottom = { x: srcPos.x, y: srcPos.y + srcPos.height / 2 };
15598
16394
  const tgtTop = { x: tgtPos.x, y: tgtPos.y - tgtPos.height / 2 };
@@ -17036,12 +17832,14 @@ function layoutC4Deployment(parsed, activeTagGroup) {
17036
17832
  height: totalHeight
17037
17833
  };
17038
17834
  }
17039
- var import_dagre5, 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;
17835
+ var import_dagre5, 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;
17040
17836
  var init_layout6 = __esm({
17041
17837
  "src/c4/layout.ts"() {
17042
17838
  "use strict";
17043
17839
  import_dagre5 = __toESM(require("@dagrejs/dagre"), 1);
17044
17840
  init_legend_constants();
17841
+ gNode = (g, name) => g.node(name);
17842
+ gEdge = (g, v, w) => g.edge(v, w);
17045
17843
  CHAR_WIDTH5 = 8;
17046
17844
  MIN_NODE_WIDTH = 160;
17047
17845
  MAX_NODE_WIDTH = 260;
@@ -17296,7 +18094,7 @@ function renderC4Context(container, parsed, layout, palette, isDark, onClickItem
17296
18094
  if (activeTagGroup) {
17297
18095
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17298
18096
  }
17299
- renderLegend3(
18097
+ renderLegend2(
17300
18098
  legendParent,
17301
18099
  layout,
17302
18100
  palette,
@@ -17657,52 +18455,28 @@ function placeEdgeLabels(labels, edges, obstacleRects) {
17657
18455
  placedRects.push({ x: lbl.x, y: lbl.y, w: lbl.bgW, h: lbl.bgH });
17658
18456
  }
17659
18457
  }
17660
- function renderLegend3(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
17661
- const visibleGroups = activeTagGroup != null ? layout.legend.filter(
17662
- (g) => g.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase()
17663
- ) : layout.legend;
17664
- const pillWidthOf = (g) => measureLegendText(g.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
17665
- const effectiveW = (g) => activeTagGroup != null ? g.width : pillWidthOf(g);
17666
- let fixedPositions = null;
17667
- if (fixedWidth != null && visibleGroups.length > 0) {
17668
- fixedPositions = /* @__PURE__ */ new Map();
17669
- const totalW = visibleGroups.reduce((s, g) => s + effectiveW(g), 0) + (visibleGroups.length - 1) * LEGEND_GROUP_GAP;
17670
- let cx = Math.max(DIAGRAM_PADDING7, (fixedWidth - totalW) / 2);
17671
- for (const g of visibleGroups) {
17672
- fixedPositions.set(g.name, cx);
17673
- cx += effectiveW(g) + LEGEND_GROUP_GAP;
17674
- }
17675
- }
17676
- for (const group of visibleGroups) {
17677
- const isActive = activeTagGroup != null && group.name.toLowerCase() === (activeTagGroup ?? "").toLowerCase();
17678
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
17679
- const pillLabel = group.name;
17680
- const pillWidth2 = pillWidthOf(group);
17681
- const gX = fixedPositions?.get(group.name) ?? group.x;
17682
- const gY = fixedPositions != null ? 0 : group.y;
17683
- const gEl = parent.append("g").attr("transform", `translate(${gX}, ${gY})`).attr("class", "c4-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
17684
- if (isActive) {
17685
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
17686
- }
17687
- const pillX = isActive ? LEGEND_CAPSULE_PAD : 0;
17688
- const pillY = LEGEND_CAPSULE_PAD;
17689
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
17690
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
17691
- if (isActive) {
17692
- gEl.append("rect").attr("x", pillX).attr("y", pillY).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
17693
- }
17694
- gEl.append("text").attr("x", pillX + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
17695
- if (isActive) {
17696
- let entryX = pillX + pillWidth2 + 4;
17697
- for (const entry of group.entries) {
17698
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
17699
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
17700
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
17701
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
17702
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
17703
- }
17704
- }
17705
- }
18458
+ function renderLegend2(parent, layout, palette, isDark, activeTagGroup, fixedWidth) {
18459
+ const groups = layout.legend.map((g) => ({
18460
+ name: g.name,
18461
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
18462
+ }));
18463
+ const legendConfig = {
18464
+ groups,
18465
+ position: { placement: "top-center", titleRelation: "below-title" },
18466
+ mode: "fixed"
18467
+ };
18468
+ const legendState = { activeGroup: activeTagGroup ?? null };
18469
+ const containerWidth = fixedWidth ?? layout.width;
18470
+ renderLegendD3(
18471
+ parent,
18472
+ legendConfig,
18473
+ legendState,
18474
+ palette,
18475
+ isDark,
18476
+ void 0,
18477
+ containerWidth
18478
+ );
18479
+ parent.selectAll("[data-legend-group]").classed("c4-legend-group", true);
17706
18480
  }
17707
18481
  function renderC4Containers(container, parsed, layout, palette, isDark, onClickItem, exportDims, activeTagGroup) {
17708
18482
  d3Selection7.select(container).selectAll(":not([data-d3-tooltip])").remove();
@@ -17913,7 +18687,7 @@ function renderC4Containers(container, parsed, layout, palette, isDark, onClickI
17913
18687
  if (activeTagGroup) {
17914
18688
  legendParent.attr("data-legend-active", activeTagGroup.toLowerCase());
17915
18689
  }
17916
- renderLegend3(
18690
+ renderLegend2(
17917
18691
  legendParent,
17918
18692
  layout,
17919
18693
  palette,
@@ -18043,6 +18817,7 @@ var init_renderer7 = __esm({
18043
18817
  init_parser6();
18044
18818
  init_layout6();
18045
18819
  init_legend_constants();
18820
+ init_legend_d3();
18046
18821
  init_title_constants();
18047
18822
  DIAGRAM_PADDING7 = 20;
18048
18823
  MAX_SCALE5 = 3;
@@ -20930,17 +21705,17 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20930
21705
  color: r.color,
20931
21706
  key: r.name.toLowerCase().replace(/\s+/g, "-")
20932
21707
  }));
20933
- const pillWidth2 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20934
- let entriesWidth2 = 0;
21708
+ const pillWidth3 = measureLegendText("Capabilities", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21709
+ let entriesWidth3 = 0;
20935
21710
  for (const e of entries) {
20936
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21711
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20937
21712
  }
20938
21713
  groups.push({
20939
21714
  name: "Capabilities",
20940
21715
  type: "role",
20941
21716
  entries,
20942
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20943
- minifiedWidth: pillWidth2
21717
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21718
+ minifiedWidth: pillWidth3
20944
21719
  });
20945
21720
  }
20946
21721
  for (const tg of tagGroups) {
@@ -20955,113 +21730,88 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
20955
21730
  }
20956
21731
  }
20957
21732
  if (entries.length === 0) continue;
20958
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20959
- let entriesWidth2 = 0;
21733
+ const pillWidth3 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21734
+ let entriesWidth3 = 0;
20960
21735
  for (const e of entries) {
20961
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21736
+ entriesWidth3 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(e.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
20962
21737
  }
20963
21738
  groups.push({
20964
21739
  name: tg.name,
20965
21740
  type: "tag",
20966
21741
  tagKey: (tg.alias ?? tg.name).toLowerCase(),
20967
21742
  entries,
20968
- width: LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2,
20969
- minifiedWidth: pillWidth2
21743
+ width: LEGEND_CAPSULE_PAD * 2 + pillWidth3 + 4 + entriesWidth3,
21744
+ minifiedWidth: pillWidth3
20970
21745
  });
20971
21746
  }
20972
21747
  return groups;
20973
21748
  }
20974
- function computePlaybackWidth(playback) {
20975
- if (!playback) return 0;
20976
- const pillWidth2 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
20977
- if (!playback.expanded) return pillWidth2;
20978
- let entriesW = 8;
20979
- entriesW += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
20980
- for (const s of playback.speedOptions) {
20981
- entriesW += measureLegendText(`${s}x`, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
20982
- }
20983
- return LEGEND_CAPSULE_PAD * 2 + pillWidth2 + entriesW;
20984
- }
20985
- function renderLegend4(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
21749
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback) {
20986
21750
  if (legendGroups.length === 0 && !playback) return;
20987
21751
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
20988
21752
  if (activeGroup) {
20989
21753
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
20990
21754
  }
20991
- const effectiveW = (g) => activeGroup != null && g.name.toLowerCase() === activeGroup.toLowerCase() ? g.width : g.minifiedWidth;
20992
- const playbackW = computePlaybackWidth(playback);
20993
- const trailingGaps = legendGroups.length > 0 && playbackW > 0 ? LEGEND_GROUP_GAP : 0;
20994
- const totalLegendW = legendGroups.reduce((s, g) => s + effectiveW(g), 0) + (legendGroups.length - 1) * LEGEND_GROUP_GAP + trailingGaps + playbackW;
20995
- let cursorX = (totalWidth - totalLegendW) / 2;
21755
+ const allGroups = legendGroups.map((g) => ({
21756
+ name: g.name,
21757
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
21758
+ }));
21759
+ if (playback) {
21760
+ allGroups.push({ name: "Playback", entries: [] });
21761
+ }
21762
+ const legendConfig = {
21763
+ groups: allGroups,
21764
+ position: { placement: "top-center", titleRelation: "below-title" },
21765
+ mode: "fixed",
21766
+ showEmptyGroups: true
21767
+ };
21768
+ const legendState = { activeGroup };
21769
+ renderLegendD3(
21770
+ legendG,
21771
+ legendConfig,
21772
+ legendState,
21773
+ palette,
21774
+ isDark,
21775
+ void 0,
21776
+ totalWidth
21777
+ );
21778
+ legendG.selectAll("[data-legend-group]").classed("infra-legend-group", true);
20996
21779
  for (const group of legendGroups) {
20997
- const isActive = activeGroup != null && group.name.toLowerCase() === activeGroup.toLowerCase();
20998
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
20999
- const pillLabel = group.name;
21000
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21001
- const gEl = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group").attr("data-legend-group", group.name.toLowerCase()).style("cursor", "pointer");
21002
- if (isActive) {
21003
- gEl.append("rect").attr("width", group.width).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21004
- }
21005
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
21006
- const pillYOff = LEGEND_CAPSULE_PAD;
21007
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
21008
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
21009
- if (isActive) {
21010
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
21011
- }
21012
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
21013
- if (isActive) {
21014
- let entryX = pillXOff + pillWidth2 + 4;
21015
- for (const entry of group.entries) {
21016
- const entryG = gEl.append("g").attr("class", "infra-legend-entry").attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21780
+ const groupKey = group.name.toLowerCase();
21781
+ for (const entry of group.entries) {
21782
+ const entryEl = legendG.select(
21783
+ `[data-legend-group="${groupKey}"] [data-legend-entry="${entry.value.toLowerCase()}"]`
21784
+ );
21785
+ if (!entryEl.empty()) {
21786
+ entryEl.attr("data-legend-entry", entry.key.toLowerCase()).attr("data-legend-color", entry.color).attr("data-legend-type", group.type).attr(
21017
21787
  "data-legend-tag-group",
21018
21788
  group.type === "tag" ? group.tagKey ?? "" : null
21019
- ).style("cursor", "pointer");
21020
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
21021
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
21022
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
21023
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
21789
+ );
21024
21790
  }
21025
21791
  }
21026
- cursorX += effectiveW(group) + LEGEND_GROUP_GAP;
21027
21792
  }
21028
- if (playback) {
21029
- const isExpanded = playback.expanded;
21030
- const groupBg = isDark ? mix(palette.bg, palette.text, 85) : mix(palette.bg, palette.text, 92);
21031
- const pillLabel = "Playback";
21032
- const pillWidth2 = measureLegendText(pillLabel, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21033
- const fullW = computePlaybackWidth(playback);
21034
- const pbG = legendG.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "infra-legend-group infra-playback-pill").style("cursor", "pointer");
21035
- if (isExpanded) {
21036
- pbG.append("rect").attr("width", fullW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
21037
- }
21038
- const pillXOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21039
- const pillYOff = isExpanded ? LEGEND_CAPSULE_PAD : 0;
21040
- const pillH = LEGEND_HEIGHT - (isExpanded ? LEGEND_CAPSULE_PAD * 2 : 0);
21041
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isExpanded ? palette.bg : groupBg);
21042
- if (isExpanded) {
21043
- pbG.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
21044
- }
21045
- pbG.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isExpanded ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
21046
- if (isExpanded) {
21047
- let entryX = pillXOff + pillWidth2 + 8;
21048
- const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21049
- const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21050
- pbG.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("fill", palette.textMuted).attr("data-playback-action", "toggle-pause").style("cursor", "pointer").text(ppLabel);
21051
- entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21052
- for (const s of playback.speedOptions) {
21053
- const label = `${s}x`;
21054
- const isActive = playback.speed === s;
21055
- const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21056
- const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21057
- const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21058
- const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21059
- speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isActive ? palette.primary : "transparent");
21060
- speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
21061
- entryX += slotW + SPEED_BADGE_GAP;
21062
- }
21063
- }
21064
- cursorX += fullW + LEGEND_GROUP_GAP;
21793
+ const playbackEl = legendG.select('[data-legend-group="playback"]');
21794
+ if (!playbackEl.empty()) {
21795
+ playbackEl.classed("infra-playback-pill", true);
21796
+ }
21797
+ if (playback && playback.expanded && !playbackEl.empty()) {
21798
+ const pillWidth3 = measureLegendText("Playback", LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
21799
+ let entryX = pillWidth3 + 8;
21800
+ const entryY = LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1;
21801
+ const ppLabel = playback.paused ? "\u25B6" : "\u23F8";
21802
+ 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);
21803
+ entryX += LEGEND_PILL_FONT_SIZE * 0.8 + 6;
21804
+ for (const s of playback.speedOptions) {
21805
+ const label = `${s}x`;
21806
+ const isSpeedActive = playback.speed === s;
21807
+ const slotW = measureLegendText(label, LEGEND_ENTRY_FONT_SIZE) + SPEED_BADGE_H_PAD * 2;
21808
+ const badgeH = LEGEND_ENTRY_FONT_SIZE + SPEED_BADGE_V_PAD * 2;
21809
+ const badgeY = (LEGEND_HEIGHT - badgeH) / 2;
21810
+ const speedG = playbackEl.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
21811
+ 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");
21812
+ 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);
21813
+ entryX += slotW + SPEED_BADGE_GAP;
21814
+ }
21065
21815
  }
21066
21816
  }
21067
21817
  function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
@@ -21192,7 +21942,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21192
21942
  "viewBox",
21193
21943
  `0 0 ${containerWidth} ${LEGEND_HEIGHT + LEGEND_FIXED_GAP3}`
21194
21944
  ).attr("preserveAspectRatio", "xMidYMid meet").style("display", "block").style("pointer-events", "none");
21195
- renderLegend4(
21945
+ renderLegend3(
21196
21946
  legendSvg,
21197
21947
  legendGroups,
21198
21948
  containerWidth,
@@ -21204,7 +21954,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
21204
21954
  );
21205
21955
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
21206
21956
  } else {
21207
- renderLegend4(
21957
+ renderLegend3(
21208
21958
  rootSvg,
21209
21959
  legendGroups,
21210
21960
  totalWidth,
@@ -21238,6 +21988,7 @@ var init_renderer8 = __esm({
21238
21988
  init_compute();
21239
21989
  init_layout8();
21240
21990
  init_legend_constants();
21991
+ init_legend_d3();
21241
21992
  init_title_constants();
21242
21993
  NODE_FONT_SIZE3 = 13;
21243
21994
  META_FONT_SIZE5 = 10;
@@ -22863,7 +23614,7 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22863
23614
  const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22864
23615
  const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
22865
23616
  const showIcon = !legendViewMode && tagGroups.length > 0;
22866
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
23617
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
22867
23618
  const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22868
23619
  let groupW = pillW;
22869
23620
  if (isActive) {
@@ -22890,83 +23641,110 @@ function renderTagLegend(svg, chartG, tagGroups, activeGroupName, chartLeftMargi
22890
23641
  const legendX = (containerWidth - totalW) / 2;
22891
23642
  const legendRow = svg.append("g").attr("class", "gantt-tag-legend-container").attr("transform", `translate(${legendX}, ${legendY})`);
22892
23643
  let cursorX = 0;
22893
- for (let i = 0; i < visibleGroups.length; i++) {
22894
- const group = visibleGroups[i];
22895
- const isActive = activeGroupName?.toLowerCase() === group.name.toLowerCase();
22896
- const isSwimlane = currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
23644
+ if (visibleGroups.length > 0) {
22897
23645
  const showIcon = !legendViewMode && tagGroups.length > 0;
22898
23646
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
22899
- const pillW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD + iconReserve;
22900
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
22901
- const groupW = groupWidths[i];
22902
- const gEl = legendRow.append("g").attr("transform", `translate(${cursorX}, 0)`).attr("class", "gantt-tag-legend-group").attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber)).style("cursor", "pointer").on("click", () => {
22903
- if (onToggle) onToggle(group.name);
23647
+ const legendGroups = visibleGroups.map((g) => {
23648
+ const key = g.name.toLowerCase();
23649
+ const entries = filteredEntries.get(key) ?? g.entries;
23650
+ return {
23651
+ name: g.name,
23652
+ entries: entries.map((e) => ({ value: e.value, color: e.color }))
23653
+ };
22904
23654
  });
22905
- if (isActive) {
22906
- gEl.append("rect").attr("width", groupW).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
22907
- }
22908
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
22909
- const pillYOff = LEGEND_CAPSULE_PAD;
22910
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
22911
- if (isActive) {
22912
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillW).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
22913
- }
22914
- const textW = measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
22915
- gEl.append("text").attr("x", pillXOff + textW / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("text-anchor", "middle").attr("font-size", `${LEGEND_PILL_FONT_SIZE}px`).attr("font-weight", "500").attr("fill", isActive || isSwimlane ? palette.text : palette.textMuted).text(group.name);
22916
- if (showIcon) {
22917
- const iconX = pillXOff + textW + 3;
22918
- const iconY = (LEGEND_HEIGHT - 10) / 2;
22919
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
22920
- iconEl.append("title").text(`Group by ${group.name}`);
22921
- iconEl.style("cursor", "pointer").on("click", (event) => {
22922
- event.stopPropagation();
22923
- if (onSwimlaneChange) {
22924
- onSwimlaneChange(
22925
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase() ? null : group.name
22926
- );
22927
- }
22928
- });
22929
- }
22930
- if (isActive) {
22931
- const tagKey = group.name.toLowerCase();
22932
- const entries = filteredEntries.get(tagKey) ?? group.entries;
22933
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
22934
- for (const entry of entries) {
22935
- const entryValue = entry.value.toLowerCase();
22936
- const entryG = gEl.append("g").attr("class", "gantt-legend-entry").attr("data-line-number", String(entry.lineNumber)).style("cursor", "pointer");
22937
- entryG.append("circle").attr("cx", ex + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
22938
- entryG.append("text").attr("x", ex + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 2).attr("text-anchor", "start").attr("font-size", `${LEGEND_ENTRY_FONT_SIZE}px`).attr("fill", palette.textMuted).text(entry.value);
22939
- entryG.on("mouseenter", () => {
23655
+ const legendConfig = {
23656
+ groups: legendGroups,
23657
+ position: {
23658
+ placement: "top-center",
23659
+ titleRelation: "below-title"
23660
+ },
23661
+ mode: "fixed",
23662
+ capsulePillAddonWidth: iconReserve
23663
+ };
23664
+ const legendState = { activeGroup: activeGroupName };
23665
+ const tagGroupsW = visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) + Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
23666
+ const tagGroupG = legendRow.append("g");
23667
+ const legendCallbacks = {
23668
+ onGroupToggle: onToggle,
23669
+ onEntryHover: (groupName, entryValue) => {
23670
+ const tagKey = groupName.toLowerCase();
23671
+ if (entryValue) {
23672
+ const ev = entryValue.toLowerCase();
22940
23673
  chartG.selectAll(".gantt-task").each(function() {
22941
23674
  const el = d3Selection10.select(this);
22942
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22943
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23675
+ el.attr(
23676
+ "opacity",
23677
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23678
+ );
22944
23679
  });
22945
23680
  chartG.selectAll(".gantt-milestone").attr("opacity", FADE_OPACITY);
22946
23681
  chartG.selectAll(".gantt-group-bar, .gantt-group-summary").attr("opacity", FADE_OPACITY);
22947
23682
  svg.selectAll(".gantt-task-label").each(function() {
22948
23683
  const el = d3Selection10.select(this);
22949
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22950
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23684
+ el.attr(
23685
+ "opacity",
23686
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23687
+ );
22951
23688
  });
22952
23689
  svg.selectAll(".gantt-group-label").attr("opacity", FADE_OPACITY);
22953
23690
  svg.selectAll(".gantt-lane-header").each(function() {
22954
23691
  const el = d3Selection10.select(this);
22955
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
22956
- el.attr("opacity", matches ? 1 : FADE_OPACITY);
23692
+ el.attr(
23693
+ "opacity",
23694
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
23695
+ );
22957
23696
  });
22958
23697
  chartG.selectAll(".gantt-lane-band, .gantt-lane-accent").attr("opacity", FADE_OPACITY);
22959
- }).on("mouseleave", () => {
23698
+ } else {
22960
23699
  if (criticalPathActive) {
22961
23700
  applyCriticalPathHighlight(svg, chartG);
22962
23701
  } else {
22963
23702
  resetHighlightAll(svg, chartG);
22964
23703
  }
22965
- });
22966
- ex += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
23704
+ }
23705
+ },
23706
+ onGroupRendered: (groupName, groupEl, _isActive) => {
23707
+ const group = visibleGroups.find((g) => g.name === groupName);
23708
+ if (group) {
23709
+ groupEl.attr("data-tag-group", group.name).attr("data-line-number", String(group.lineNumber));
23710
+ }
23711
+ if (showIcon && _isActive) {
23712
+ const isSwimlane = currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
23713
+ const textW = measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
23714
+ const pillXOff = LEGEND_CAPSULE_PAD;
23715
+ const iconX = pillXOff + textW + 3;
23716
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
23717
+ const iconEl = drawSwimlaneIcon(
23718
+ groupEl,
23719
+ iconX,
23720
+ iconY,
23721
+ isSwimlane,
23722
+ palette
23723
+ );
23724
+ iconEl.append("title").text(`Group by ${groupName}`);
23725
+ iconEl.style("cursor", "pointer").on("click", (event) => {
23726
+ event.stopPropagation();
23727
+ if (onSwimlaneChange) {
23728
+ onSwimlaneChange(
23729
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase() ? null : groupName
23730
+ );
23731
+ }
23732
+ });
23733
+ }
22967
23734
  }
23735
+ };
23736
+ renderLegendD3(
23737
+ tagGroupG,
23738
+ legendConfig,
23739
+ legendState,
23740
+ palette,
23741
+ isDark,
23742
+ legendCallbacks,
23743
+ tagGroupsW
23744
+ );
23745
+ for (let i = 0; i < visibleGroups.length; i++) {
23746
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
22968
23747
  }
22969
- cursorX += groupW + LEGEND_GROUP_GAP;
22970
23748
  }
22971
23749
  if (hasCriticalPath) {
22972
23750
  const cpLineNum = optionLineNumbers["critical-path"];
@@ -23595,6 +24373,7 @@ var init_renderer9 = __esm({
23595
24373
  init_tag_groups();
23596
24374
  init_d3();
23597
24375
  init_legend_constants();
24376
+ init_legend_d3();
23598
24377
  init_title_constants();
23599
24378
  BAR_H = 22;
23600
24379
  ROW_GAP = 6;
@@ -24764,57 +25543,29 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24764
25543
  }
24765
25544
  if (parsed.tagGroups.length > 0) {
24766
25545
  const legendY = TOP_MARGIN + titleOffset;
24767
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
24768
- const legendItems = [];
24769
- for (const tg of parsed.tagGroups) {
24770
- if (tg.entries.length === 0) continue;
24771
- const isActive = !!activeTagGroup && tg.name.toLowerCase() === activeTagGroup.toLowerCase();
24772
- const pillWidth2 = measureLegendText(tg.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
24773
- const entries = tg.entries.map((e) => ({
25546
+ const resolvedGroups = parsed.tagGroups.filter((tg) => tg.entries.length > 0).map((tg) => ({
25547
+ name: tg.name,
25548
+ entries: tg.entries.map((e) => ({
24774
25549
  value: e.value,
24775
25550
  color: resolveColor(e.color) ?? e.color
24776
- }));
24777
- let totalWidth2 = pillWidth2;
24778
- if (isActive) {
24779
- let entriesWidth2 = 0;
24780
- for (const entry of entries) {
24781
- entriesWidth2 += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24782
- }
24783
- totalWidth2 = LEGEND_CAPSULE_PAD * 2 + pillWidth2 + 4 + entriesWidth2;
24784
- }
24785
- legendItems.push({ group: tg, isActive, pillWidth: pillWidth2, totalWidth: totalWidth2, entries });
24786
- }
24787
- const totalLegendWidth = legendItems.reduce((s, item) => s + item.totalWidth, 0) + (legendItems.length - 1) * LEGEND_GROUP_GAP;
24788
- let legendX = (svgWidth - totalLegendWidth) / 2;
24789
- const legendContainer = svg.append("g").attr("class", "sequence-legend");
24790
- if (activeTagGroup) {
24791
- legendContainer.attr("data-legend-active", activeTagGroup.toLowerCase());
24792
- }
24793
- for (const item of legendItems) {
24794
- const gEl = legendContainer.append("g").attr("transform", `translate(${legendX}, ${legendY})`).attr("class", "sequence-legend-group").attr("data-legend-group", item.group.name.toLowerCase()).style("cursor", "pointer");
24795
- if (item.isActive) {
24796
- gEl.append("rect").attr("width", item.totalWidth).attr("height", LEGEND_HEIGHT).attr("rx", LEGEND_HEIGHT / 2).attr("fill", groupBg);
24797
- }
24798
- const pillXOff = item.isActive ? LEGEND_CAPSULE_PAD : 0;
24799
- const pillYOff = LEGEND_CAPSULE_PAD;
24800
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
24801
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", item.isActive ? palette.bg : groupBg);
24802
- if (item.isActive) {
24803
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", item.pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
24804
- }
24805
- gEl.append("text").attr("x", pillXOff + item.pillWidth / 2).attr("y", LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", item.isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(item.group.name);
24806
- if (item.isActive) {
24807
- let entryX = pillXOff + item.pillWidth + 4;
24808
- for (const entry of item.entries) {
24809
- const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
24810
- entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R).attr("cy", LEGEND_HEIGHT / 2).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
24811
- const textX = entryX + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP;
24812
- entryG.append("text").attr("x", textX).attr("y", LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).text(entry.value);
24813
- entryX = textX + measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) + LEGEND_ENTRY_TRAIL;
24814
- }
24815
- }
24816
- legendX += item.totalWidth + LEGEND_GROUP_GAP;
24817
- }
25551
+ }))
25552
+ }));
25553
+ const legendConfig = {
25554
+ groups: resolvedGroups,
25555
+ position: { placement: "top-center", titleRelation: "below-title" },
25556
+ mode: "fixed"
25557
+ };
25558
+ const legendState = { activeGroup: activeTagGroup ?? null };
25559
+ const legendG = svg.append("g").attr("class", "sequence-legend").attr("transform", `translate(0,${legendY})`);
25560
+ renderLegendD3(
25561
+ legendG,
25562
+ legendConfig,
25563
+ legendState,
25564
+ palette,
25565
+ isDark,
25566
+ void 0,
25567
+ svgWidth
25568
+ );
24818
25569
  }
24819
25570
  for (const group of groups) {
24820
25571
  if (group.participantIds.length === 0) continue;
@@ -24891,7 +25642,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24891
25642
  for (const branch of el.elseIfBranches) {
24892
25643
  elseIfBranchData.push({
24893
25644
  label: branch.label,
24894
- indices: collectMsgIndices(branch.children)
25645
+ indices: collectMsgIndices(branch.children),
25646
+ lineNumber: branch.lineNumber
24895
25647
  });
24896
25648
  }
24897
25649
  }
@@ -24952,14 +25704,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24952
25704
  x1: frameX,
24953
25705
  y1: dividerY,
24954
25706
  x2: frameX + frameW,
24955
- y2: dividerY
25707
+ y2: dividerY,
25708
+ blockLine: branchData.lineNumber
24956
25709
  });
24957
25710
  deferredLabels.push({
24958
25711
  x: frameX + 6,
24959
25712
  y: dividerY + 14,
24960
25713
  text: `else if ${branchData.label}`,
24961
25714
  bold: false,
24962
- italic: true
25715
+ italic: true,
25716
+ blockLine: branchData.lineNumber
24963
25717
  });
24964
25718
  }
24965
25719
  }
@@ -24977,14 +25731,16 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
24977
25731
  x1: frameX,
24978
25732
  y1: dividerY,
24979
25733
  x2: frameX + frameW,
24980
- y2: dividerY
25734
+ y2: dividerY,
25735
+ blockLine: el.elseLineNumber
24981
25736
  });
24982
25737
  deferredLabels.push({
24983
25738
  x: frameX + 6,
24984
25739
  y: dividerY + 14,
24985
25740
  text: "else",
24986
25741
  bold: false,
24987
- italic: true
25742
+ italic: true,
25743
+ blockLine: el.elseLineNumber
24988
25744
  });
24989
25745
  }
24990
25746
  }
@@ -25030,7 +25786,9 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
25030
25786
  }
25031
25787
  });
25032
25788
  for (const ln of deferredLines) {
25033
- 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");
25789
+ 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");
25790
+ if (ln.blockLine !== void 0)
25791
+ line10.attr("data-block-line", String(ln.blockLine));
25034
25792
  }
25035
25793
  for (const lbl of deferredLabels) {
25036
25794
  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);
@@ -25374,6 +26132,7 @@ var init_renderer10 = __esm({
25374
26132
  init_parser();
25375
26133
  init_tag_resolution();
25376
26134
  init_legend_constants();
26135
+ init_legend_d3();
25377
26136
  init_title_constants();
25378
26137
  PARTICIPANT_GAP = 160;
25379
26138
  PARTICIPANT_BOX_WIDTH = 120;
@@ -27849,7 +28608,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
27849
28608
  }
27850
28609
  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);
27851
28610
  if (labelFitsInside) {
27852
- 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);
28611
+ 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);
27853
28612
  } else {
27854
28613
  const wouldFlipLeft = x + rectW > innerWidth * 0.6;
27855
28614
  const labelFitsLeft = x - 6 - estLabelWidth > 0;
@@ -28003,7 +28762,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28003
28762
  }
28004
28763
  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);
28005
28764
  if (labelFitsInside) {
28006
- 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);
28765
+ 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);
28007
28766
  } else {
28008
28767
  const wouldFlipLeft = x + rectW > innerWidth * 0.6;
28009
28768
  const labelFitsLeft = x - 6 - estLabelWidth > 0;
@@ -28029,7 +28788,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28029
28788
  const LG_ENTRY_FONT_SIZE = LEGEND_ENTRY_FONT_SIZE;
28030
28789
  const LG_ENTRY_DOT_GAP = LEGEND_ENTRY_DOT_GAP;
28031
28790
  const LG_ENTRY_TRAIL = LEGEND_ENTRY_TRAIL;
28032
- const LG_GROUP_GAP = LEGEND_GROUP_GAP;
28033
28791
  const LG_ICON_W = 20;
28034
28792
  const mainSvg = d3Selection13.select(container).select("svg");
28035
28793
  const mainG = mainSvg.select("g");
@@ -28068,11 +28826,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28068
28826
  (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
28069
28827
  ) : legendGroups;
28070
28828
  if (visibleGroups.length === 0) return;
28071
- const totalW = visibleGroups.reduce((s, lg) => {
28072
- const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
28073
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
28074
- }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
28075
- let cx = (width - totalW) / 2;
28076
28829
  const legendContainer = mainSvg.append("g").attr("class", "tl-tag-legend-container");
28077
28830
  if (currentActiveGroup) {
28078
28831
  legendContainer.attr(
@@ -28080,82 +28833,85 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28080
28833
  currentActiveGroup.toLowerCase()
28081
28834
  );
28082
28835
  }
28083
- for (const lg of visibleGroups) {
28084
- const groupKey = lg.group.name.toLowerCase();
28085
- const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
28086
- const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28087
- const pillLabel = lg.group.name;
28088
- const pillWidth2 = measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28089
- const gEl = legendContainer.append("g").attr("transform", `translate(${cx}, ${legendY})`).attr("class", "tl-tag-legend-group tl-tag-legend-entry").attr("data-legend-group", groupKey).attr("data-tag-group", groupKey).attr("data-legend-entry", "__group__");
28090
- if (!viewMode) {
28091
- gEl.style("cursor", "pointer").on("click", () => {
28092
- currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
28093
- drawLegend2();
28094
- recolorEvents2();
28095
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28096
- });
28097
- }
28098
- if (isActive) {
28099
- gEl.append("rect").attr("width", lg.expandedWidth).attr("height", LG_HEIGHT).attr("rx", LG_HEIGHT / 2).attr("fill", groupBg);
28100
- }
28101
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
28102
- const pillYOff = LG_CAPSULE_PAD;
28103
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
28104
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", isActive ? palette.bg : groupBg);
28105
- if (isActive) {
28106
- gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth2).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
28107
- }
28108
- gEl.append("text").attr("x", pillXOff + pillWidth2 / 2).attr("y", LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2).attr("font-size", LG_PILL_FONT_SIZE).attr("font-weight", "500").attr("font-family", FONT_FAMILY).attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
28109
- if (isActive) {
28110
- let entryX;
28111
- if (!viewMode) {
28112
- const iconX = pillXOff + pillWidth2 + 5;
28836
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
28837
+ const centralGroups = visibleGroups.map((lg) => ({
28838
+ name: lg.group.name,
28839
+ entries: lg.group.entries.map((e) => ({
28840
+ value: e.value,
28841
+ color: e.color
28842
+ }))
28843
+ }));
28844
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
28845
+ const centralConfig = {
28846
+ groups: centralGroups,
28847
+ position: { placement: "top-center", titleRelation: "below-title" },
28848
+ mode: "fixed",
28849
+ capsulePillAddonWidth: iconAddon
28850
+ };
28851
+ const centralState = { activeGroup: centralActive };
28852
+ const centralCallbacks = viewMode ? {} : {
28853
+ onGroupToggle: (groupName) => {
28854
+ currentActiveGroup = currentActiveGroup === groupName.toLowerCase() ? null : groupName.toLowerCase();
28855
+ drawLegend2();
28856
+ recolorEvents2();
28857
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28858
+ },
28859
+ onEntryHover: (groupName, entryValue) => {
28860
+ const tagKey = groupName.toLowerCase();
28861
+ if (entryValue) {
28862
+ const tagVal = entryValue.toLowerCase();
28863
+ fadeToTagValue(mainG, tagKey, tagVal);
28864
+ mainSvg.selectAll("[data-legend-entry]").each(function() {
28865
+ const el = d3Selection13.select(this);
28866
+ const ev = el.attr("data-legend-entry");
28867
+ const eg = el.attr("data-tag-group") ?? el.node()?.closest?.("[data-tag-group]")?.getAttribute("data-tag-group");
28868
+ el.attr(
28869
+ "opacity",
28870
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28871
+ );
28872
+ });
28873
+ } else {
28874
+ fadeReset(mainG);
28875
+ mainSvg.selectAll("[data-legend-entry]").attr("opacity", 1);
28876
+ }
28877
+ },
28878
+ onGroupRendered: (groupName, groupEl, isActive) => {
28879
+ const groupKey = groupName.toLowerCase();
28880
+ groupEl.attr("data-tag-group", groupKey);
28881
+ if (isActive && !viewMode) {
28882
+ const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
28883
+ const pillWidth3 = measureLegendText(groupName, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28884
+ const pillXOff = LG_CAPSULE_PAD;
28885
+ const iconX = pillXOff + pillWidth3 + 5;
28113
28886
  const iconY = (LG_HEIGHT - 10) / 2;
28114
- const iconEl = drawSwimlaneIcon3(gEl, iconX, iconY, isSwimActive);
28887
+ const iconEl = drawSwimlaneIcon3(
28888
+ groupEl,
28889
+ iconX,
28890
+ iconY,
28891
+ isSwimActive
28892
+ );
28115
28893
  iconEl.attr("data-swimlane-toggle", groupKey).on("click", (event) => {
28116
28894
  event.stopPropagation();
28117
28895
  currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
28118
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
28896
+ onTagStateChange?.(
28897
+ currentActiveGroup,
28898
+ currentSwimlaneGroup
28899
+ );
28119
28900
  relayout2();
28120
28901
  });
28121
- entryX = pillXOff + pillWidth2 + LG_ICON_W + 4;
28122
- } else {
28123
- entryX = pillXOff + pillWidth2 + 8;
28124
- }
28125
- for (const entry of lg.group.entries) {
28126
- const tagKey = lg.group.name.toLowerCase();
28127
- const tagVal = entry.value.toLowerCase();
28128
- const entryG = gEl.append("g").attr("class", "tl-tag-legend-entry").attr("data-tag-group", tagKey).attr("data-legend-entry", tagVal);
28129
- if (!viewMode) {
28130
- entryG.style("cursor", "pointer").on("mouseenter", (event) => {
28131
- event.stopPropagation();
28132
- fadeToTagValue(mainG, tagKey, tagVal);
28133
- mainSvg.selectAll(".tl-tag-legend-entry").each(function() {
28134
- const el = d3Selection13.select(this);
28135
- const ev = el.attr("data-legend-entry");
28136
- if (ev === "__group__") return;
28137
- const eg = el.attr("data-tag-group");
28138
- el.attr(
28139
- "opacity",
28140
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY2
28141
- );
28142
- });
28143
- }).on("mouseleave", (event) => {
28144
- event.stopPropagation();
28145
- fadeReset(mainG);
28146
- mainSvg.selectAll(".tl-tag-legend-entry").attr("opacity", 1);
28147
- }).on("click", (event) => {
28148
- event.stopPropagation();
28149
- });
28150
- }
28151
- entryG.append("circle").attr("cx", entryX + LG_DOT_R).attr("cy", LG_HEIGHT / 2).attr("r", LG_DOT_R).attr("fill", entry.color);
28152
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
28153
- entryG.append("text").attr("x", textX).attr("y", LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1).attr("font-size", LG_ENTRY_FONT_SIZE).attr("font-family", FONT_FAMILY).attr("fill", palette.textMuted).text(entry.value);
28154
- entryX = textX + measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) + LG_ENTRY_TRAIL;
28155
28902
  }
28156
28903
  }
28157
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
28158
- }
28904
+ };
28905
+ const legendInnerG = legendContainer.append("g").attr("transform", `translate(0, ${legendY})`);
28906
+ renderLegendD3(
28907
+ legendInnerG,
28908
+ centralConfig,
28909
+ centralState,
28910
+ palette,
28911
+ isDark,
28912
+ centralCallbacks,
28913
+ width
28914
+ );
28159
28915
  }, recolorEvents2 = function() {
28160
28916
  const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
28161
28917
  mainG.selectAll(".tl-event").each(function() {
@@ -28180,7 +28936,6 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
28180
28936
  };
28181
28937
  var drawSwimlaneIcon2 = drawSwimlaneIcon3, relayout = relayout2, drawLegend = drawLegend2, recolorEvents = recolorEvents2;
28182
28938
  const legendY = title ? 50 : 10;
28183
- const groupBg = isDark ? mix(palette.surface, palette.bg, 50) : mix(palette.surface, palette.bg, 30);
28184
28939
  const legendGroups = parsed.timelineTagGroups.map((g) => {
28185
28940
  const pillW = measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
28186
28941
  const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
@@ -28375,7 +29130,7 @@ function regionCentroid(circles, inside) {
28375
29130
  }
28376
29131
  function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims) {
28377
29132
  const { vennSets, vennOverlaps, title } = parsed;
28378
- if (vennSets.length < 2) return;
29133
+ if (vennSets.length < 2 || vennSets.length > 3) return;
28379
29134
  const init2 = initD3Chart(container, palette, exportDims);
28380
29135
  if (!init2) return;
28381
29136
  const { svg, width, height, textColor, colors } = init2;
@@ -28431,7 +29186,9 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28431
29186
  marginBottom
28432
29187
  ).map((c) => ({ ...c, y: c.y + titleHeight }));
28433
29188
  const scaledR = circles[0].r;
28434
- svg.append("style").text("circle:focus, circle:focus-visible { outline: none !important; }");
29189
+ svg.append("style").text(
29190
+ "circle:focus, circle:focus-visible { outline-solid: none !important; }"
29191
+ );
28435
29192
  renderChartTitle(
28436
29193
  svg,
28437
29194
  title,
@@ -28613,7 +29370,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28613
29370
  }
28614
29371
  const hoverGroup = svg.append("g");
28615
29372
  circles.forEach((c, i) => {
28616
- 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", () => {
29373
+ 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", () => {
28617
29374
  showRegionOverlay([i]);
28618
29375
  }).on("mouseleave", () => {
28619
29376
  hideAllOverlays();
@@ -28651,7 +29408,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
28651
29408
  const declaredOv = vennOverlaps.find(
28652
29409
  (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
28653
29410
  );
28654
- 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", () => {
29411
+ 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", () => {
28655
29412
  showRegionOverlay(idxs);
28656
29413
  }).on("mouseleave", () => {
28657
29414
  hideAllOverlays();
@@ -28785,8 +29542,8 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28785
29542
  const LABEL_MAX_FONT = 48;
28786
29543
  const LABEL_MIN_FONT = 14;
28787
29544
  const LABEL_PAD2 = 40;
28788
- const CHAR_WIDTH_RATIO2 = 0.6;
28789
- const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO2;
29545
+ const CHAR_WIDTH_RATIO3 = 0.6;
29546
+ const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO3;
28790
29547
  const quadrantLabelLayout = (text, qw2, qh2) => {
28791
29548
  const availW = qw2 - LABEL_PAD2;
28792
29549
  const availH = qh2 - LABEL_PAD2;
@@ -28930,16 +29687,45 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28930
29687
  if (x < 0.5 && y < 0.5) return "bottom-left";
28931
29688
  return "bottom-right";
28932
29689
  };
29690
+ const POINT_RADIUS = 6;
29691
+ const POINT_LABEL_FONT_SIZE = 12;
29692
+ const quadrantLabelObstacles = quadrantDefsWithLabel.map((d) => {
29693
+ const layout = labelLayouts.get(d.label.text);
29694
+ const totalW = Math.max(...layout.lines.map((l) => l.length)) * layout.fontSize * CHAR_WIDTH_RATIO3;
29695
+ const totalH = layout.lines.length * layout.fontSize * 1.2;
29696
+ return {
29697
+ x: d.labelX - totalW / 2,
29698
+ y: d.labelY - totalH / 2,
29699
+ w: totalW,
29700
+ h: totalH
29701
+ };
29702
+ });
29703
+ const pointPixels = quadrantPoints.map((point) => ({
29704
+ label: point.label,
29705
+ cx: xScale(point.x),
29706
+ cy: yScale(point.y)
29707
+ }));
29708
+ const placedPointLabels = computeQuadrantPointLabels(
29709
+ pointPixels,
29710
+ { left: 0, top: 0, right: chartWidth, bottom: chartHeight },
29711
+ quadrantLabelObstacles,
29712
+ POINT_RADIUS,
29713
+ POINT_LABEL_FONT_SIZE
29714
+ );
28933
29715
  const pointsG = chartG.append("g").attr("class", "points");
28934
- quadrantPoints.forEach((point) => {
29716
+ quadrantPoints.forEach((point, i) => {
28935
29717
  const cx = xScale(point.x);
28936
29718
  const cy = yScale(point.y);
28937
29719
  const quadrant = getPointQuadrant(point.x, point.y);
28938
29720
  const quadDef = quadrantDefs.find((d) => d.position === quadrant);
28939
29721
  const pointColor = quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
29722
+ const placed = placedPointLabels[i];
28940
29723
  const pointG = pointsG.append("g").attr("class", "point-group").attr("data-line-number", String(point.lineNumber));
28941
- pointG.append("circle").attr("cx", cx).attr("cy", cy).attr("r", 6).attr("fill", "#ffffff").attr("stroke", pointColor).attr("stroke-width", 2);
28942
- 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);
29724
+ if (placed.connectorLine) {
29725
+ 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);
29726
+ }
29727
+ pointG.append("circle").attr("cx", cx).attr("cy", cy).attr("r", POINT_RADIUS).attr("fill", "#ffffff").attr("stroke", pointColor).attr("stroke-width", 2);
29728
+ 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);
28943
29729
  const tipHtml = `<strong>${point.label}</strong><br>x: ${point.x.toFixed(2)}, y: ${point.y.toFixed(2)}`;
28944
29730
  pointG.style("cursor", onClickItem ? "pointer" : "default").on("mouseenter", (event) => {
28945
29731
  showTooltip(tooltip, tipHtml, event);
@@ -28948,7 +29734,7 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
28948
29734
  showTooltip(tooltip, tipHtml, event);
28949
29735
  }).on("mouseleave", () => {
28950
29736
  hideTooltip(tooltip);
28951
- pointG.select("circle").attr("r", 6);
29737
+ pointG.select("circle").attr("r", POINT_RADIUS);
28952
29738
  }).on("click", () => {
28953
29739
  if (onClickItem && point.lineNumber) onClickItem(point.lineNumber);
28954
29740
  });
@@ -29420,6 +30206,7 @@ var init_d3 = __esm({
29420
30206
  import_d3_cloud = __toESM(require("d3-cloud"), 1);
29421
30207
  init_fonts();
29422
30208
  init_branding();
30209
+ init_label_layout();
29423
30210
  init_colors();
29424
30211
  init_palettes();
29425
30212
  init_color_utils();
@@ -29427,6 +30214,7 @@ var init_d3 = __esm({
29427
30214
  init_parsing();
29428
30215
  init_tag_groups();
29429
30216
  init_legend_constants();
30217
+ init_legend_d3();
29430
30218
  init_title_constants();
29431
30219
  DEFAULT_CLOUD_OPTIONS = {
29432
30220
  rotate: "none",
@@ -29605,10 +30393,12 @@ __export(index_exports, {
29605
30393
  computeCardMove: () => computeCardMove,
29606
30394
  computeInfra: () => computeInfra,
29607
30395
  computeInfraLegendGroups: () => computeInfraLegendGroups,
30396
+ computeLegendLayout: () => computeLegendLayout,
29608
30397
  computeScatterLabelGraphics: () => computeScatterLabelGraphics,
29609
30398
  computeTimeTicks: () => computeTimeTicks,
29610
30399
  contrastText: () => contrastText,
29611
30400
  decodeDiagramUrl: () => decodeDiagramUrl,
30401
+ draculaPalette: () => draculaPalette,
29612
30402
  encodeDiagramUrl: () => encodeDiagramUrl,
29613
30403
  extractDiagramSymbols: () => extractDiagramSymbols,
29614
30404
  extractTagDeclarations: () => extractTagDeclarations,
@@ -29616,6 +30406,7 @@ __export(index_exports, {
29616
30406
  formatDgmoError: () => formatDgmoError,
29617
30407
  getAvailablePalettes: () => getAvailablePalettes,
29618
30408
  getExtendedChartLegendGroups: () => getExtendedChartLegendGroups,
30409
+ getLegendReservedHeight: () => getLegendReservedHeight,
29619
30410
  getPalette: () => getPalette,
29620
30411
  getRenderCategory: () => getRenderCategory,
29621
30412
  getSeriesColors: () => getSeriesColors,
@@ -29651,6 +30442,7 @@ __export(index_exports, {
29651
30442
  looksLikeSitemap: () => looksLikeSitemap,
29652
30443
  looksLikeState: () => looksLikeState,
29653
30444
  makeDgmoError: () => makeDgmoError,
30445
+ monokaiPalette: () => monokaiPalette,
29654
30446
  mute: () => mute,
29655
30447
  nord: () => nord,
29656
30448
  nordPalette: () => nordPalette,
@@ -29704,7 +30496,9 @@ __export(index_exports, {
29704
30496
  renderInfra: () => renderInfra,
29705
30497
  renderKanban: () => renderKanban,
29706
30498
  renderKanbanForExport: () => renderKanbanForExport,
30499
+ renderLegendD3: () => renderLegendD3,
29707
30500
  renderLegendSvg: () => renderLegendSvg,
30501
+ renderLegendSvgFromConfig: () => renderLegendSvgFromConfig,
29708
30502
  renderOrg: () => renderOrg,
29709
30503
  renderOrgForExport: () => renderOrgForExport,
29710
30504
  renderQuadrant: () => renderQuadrant,
@@ -29744,11 +30538,26 @@ async function ensureDom() {
29744
30538
  const { JSDOM } = await import("jsdom");
29745
30539
  const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
29746
30540
  const win = dom.window;
29747
- Object.defineProperty(globalThis, "document", { value: win.document, configurable: true });
29748
- Object.defineProperty(globalThis, "window", { value: win, configurable: true });
29749
- Object.defineProperty(globalThis, "navigator", { value: win.navigator, configurable: true });
29750
- Object.defineProperty(globalThis, "HTMLElement", { value: win.HTMLElement, configurable: true });
29751
- Object.defineProperty(globalThis, "SVGElement", { value: win.SVGElement, configurable: true });
30541
+ Object.defineProperty(globalThis, "document", {
30542
+ value: win.document,
30543
+ configurable: true
30544
+ });
30545
+ Object.defineProperty(globalThis, "window", {
30546
+ value: win,
30547
+ configurable: true
30548
+ });
30549
+ Object.defineProperty(globalThis, "navigator", {
30550
+ value: win.navigator,
30551
+ configurable: true
30552
+ });
30553
+ Object.defineProperty(globalThis, "HTMLElement", {
30554
+ value: win.HTMLElement,
30555
+ configurable: true
30556
+ });
30557
+ Object.defineProperty(globalThis, "SVGElement", {
30558
+ value: win.SVGElement,
30559
+ configurable: true
30560
+ });
29752
30561
  }
29753
30562
  async function render(content, options) {
29754
30563
  const theme = options?.theme ?? "light";
@@ -29757,11 +30566,17 @@ async function render(content, options) {
29757
30566
  const paletteColors = getPalette(paletteName)[theme === "dark" ? "dark" : "light"];
29758
30567
  const chartType = parseDgmoChartType(content);
29759
30568
  const category = chartType ? getRenderCategory(chartType) : null;
30569
+ const legendExportState = options?.legendState ? {
30570
+ activeTagGroup: options.legendState.activeGroup ?? null,
30571
+ hiddenAttributes: options.legendState.hiddenAttributes ? new Set(options.legendState.hiddenAttributes) : void 0
30572
+ } : void 0;
29760
30573
  if (category === "data-chart") {
29761
- return renderExtendedChartForExport(content, theme, paletteColors, { branding });
30574
+ return renderExtendedChartForExport(content, theme, paletteColors, {
30575
+ branding
30576
+ });
29762
30577
  }
29763
30578
  await ensureDom();
29764
- return renderForExport(content, theme, paletteColors, void 0, {
30579
+ return renderForExport(content, theme, paletteColors, legendExportState, {
29765
30580
  branding,
29766
30581
  c4Level: options?.c4Level,
29767
30582
  c4System: options?.c4System,
@@ -30484,6 +31299,8 @@ init_flowchart_renderer();
30484
31299
  init_echarts();
30485
31300
  init_legend_svg();
30486
31301
  init_legend_constants();
31302
+ init_legend_d3();
31303
+ init_legend_layout();
30487
31304
  init_d3();
30488
31305
  init_renderer10();
30489
31306
  init_colors();
@@ -31328,10 +32145,12 @@ init_branding();
31328
32145
  computeCardMove,
31329
32146
  computeInfra,
31330
32147
  computeInfraLegendGroups,
32148
+ computeLegendLayout,
31331
32149
  computeScatterLabelGraphics,
31332
32150
  computeTimeTicks,
31333
32151
  contrastText,
31334
32152
  decodeDiagramUrl,
32153
+ draculaPalette,
31335
32154
  encodeDiagramUrl,
31336
32155
  extractDiagramSymbols,
31337
32156
  extractTagDeclarations,
@@ -31339,6 +32158,7 @@ init_branding();
31339
32158
  formatDgmoError,
31340
32159
  getAvailablePalettes,
31341
32160
  getExtendedChartLegendGroups,
32161
+ getLegendReservedHeight,
31342
32162
  getPalette,
31343
32163
  getRenderCategory,
31344
32164
  getSeriesColors,
@@ -31374,6 +32194,7 @@ init_branding();
31374
32194
  looksLikeSitemap,
31375
32195
  looksLikeState,
31376
32196
  makeDgmoError,
32197
+ monokaiPalette,
31377
32198
  mute,
31378
32199
  nord,
31379
32200
  nordPalette,
@@ -31427,7 +32248,9 @@ init_branding();
31427
32248
  renderInfra,
31428
32249
  renderKanban,
31429
32250
  renderKanbanForExport,
32251
+ renderLegendD3,
31430
32252
  renderLegendSvg,
32253
+ renderLegendSvgFromConfig,
31431
32254
  renderOrg,
31432
32255
  renderOrgForExport,
31433
32256
  renderQuadrant,