@diagrammo/dgmo 0.8.3 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +153 -153
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3336 -1055
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3336 -1055
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +30 -29
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +100 -55
- package/src/chart.ts +91 -28
- package/src/class/parser.ts +41 -12
- package/src/cli.ts +168 -61
- package/src/completion.ts +378 -183
- package/src/d3.ts +887 -288
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +69 -23
- package/src/echarts.ts +646 -153
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +48 -14
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +197 -71
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +46 -25
- package/src/graph/state-parser.ts +47 -17
- package/src/infra/parser.ts +157 -53
- package/src/infra/renderer.ts +723 -271
- package/src/initiative-status/parser.ts +138 -44
- package/src/kanban/parser.ts +25 -14
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +69 -22
- package/src/palettes/index.ts +3 -2
- package/src/sequence/parser.ts +193 -61
- package/src/sitemap/parser.ts +65 -29
- package/src/utils/arrows.ts +2 -22
- package/src/utils/duration.ts +39 -21
- package/src/utils/legend-constants.ts +0 -2
- package/src/utils/parsing.ts +75 -31
package/src/d3.ts
CHANGED
|
@@ -182,8 +182,18 @@ import { getSeriesColors } from './palettes';
|
|
|
182
182
|
import { mix } from './palettes/color-utils';
|
|
183
183
|
import type { DgmoError } from './diagnostics';
|
|
184
184
|
import { makeDgmoError, formatDgmoError, suggest } from './diagnostics';
|
|
185
|
-
import {
|
|
186
|
-
|
|
185
|
+
import {
|
|
186
|
+
collectIndentedValues,
|
|
187
|
+
extractColor,
|
|
188
|
+
parseFirstLine,
|
|
189
|
+
parsePipeMetadata,
|
|
190
|
+
MULTIPLE_PIPE_ERROR,
|
|
191
|
+
} from './utils/parsing';
|
|
192
|
+
import {
|
|
193
|
+
matchTagBlockHeading,
|
|
194
|
+
validateTagValues,
|
|
195
|
+
resolveTagColor,
|
|
196
|
+
} from './utils/tag-groups';
|
|
187
197
|
import type { TagGroup } from './utils/tag-groups';
|
|
188
198
|
import {
|
|
189
199
|
LEGEND_HEIGHT as TL_LEGEND_HEIGHT,
|
|
@@ -197,7 +207,11 @@ import {
|
|
|
197
207
|
LEGEND_GROUP_GAP as TL_LEGEND_GROUP_GAP,
|
|
198
208
|
measureLegendText,
|
|
199
209
|
} from './utils/legend-constants';
|
|
200
|
-
import {
|
|
210
|
+
import {
|
|
211
|
+
TITLE_FONT_SIZE,
|
|
212
|
+
TITLE_FONT_WEIGHT,
|
|
213
|
+
TITLE_Y,
|
|
214
|
+
} from './utils/title-constants';
|
|
201
215
|
|
|
202
216
|
// ============================================================
|
|
203
217
|
// Shared Rendering Helpers
|
|
@@ -215,7 +229,8 @@ function renderChartTitle(
|
|
|
215
229
|
onClickItem?: (lineNumber: number) => void
|
|
216
230
|
): void {
|
|
217
231
|
if (!title) return;
|
|
218
|
-
const titleEl = svg
|
|
232
|
+
const titleEl = svg
|
|
233
|
+
.append('text')
|
|
219
234
|
.attr('class', 'chart-title')
|
|
220
235
|
.attr('x', width / 2)
|
|
221
236
|
.attr('y', TITLE_Y)
|
|
@@ -230,8 +245,12 @@ function renderChartTitle(
|
|
|
230
245
|
if (onClickItem) {
|
|
231
246
|
titleEl
|
|
232
247
|
.on('click', () => onClickItem(titleLineNumber))
|
|
233
|
-
.on('mouseenter', function () {
|
|
234
|
-
|
|
248
|
+
.on('mouseenter', function () {
|
|
249
|
+
d3Selection.select(this).attr('opacity', 0.7);
|
|
250
|
+
})
|
|
251
|
+
.on('mouseleave', function () {
|
|
252
|
+
d3Selection.select(this).attr('opacity', 1);
|
|
253
|
+
});
|
|
235
254
|
}
|
|
236
255
|
}
|
|
237
256
|
}
|
|
@@ -244,7 +263,15 @@ function initD3Chart(
|
|
|
244
263
|
container: HTMLDivElement,
|
|
245
264
|
palette: PaletteColors,
|
|
246
265
|
exportDims?: D3ExportDimensions
|
|
247
|
-
): {
|
|
266
|
+
): {
|
|
267
|
+
svg: d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
|
|
268
|
+
width: number;
|
|
269
|
+
height: number;
|
|
270
|
+
textColor: string;
|
|
271
|
+
mutedColor: string;
|
|
272
|
+
bgColor: string;
|
|
273
|
+
colors: string[];
|
|
274
|
+
} | null {
|
|
248
275
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
249
276
|
const width = exportDims?.width ?? container.clientWidth;
|
|
250
277
|
const height = exportDims?.height ?? container.clientHeight;
|
|
@@ -253,7 +280,12 @@ function initD3Chart(
|
|
|
253
280
|
const mutedColor = palette.border;
|
|
254
281
|
const bgColor = palette.bg;
|
|
255
282
|
const colors = getSeriesColors(palette);
|
|
256
|
-
const svg = d3Selection
|
|
283
|
+
const svg = d3Selection
|
|
284
|
+
.select(container)
|
|
285
|
+
.append('svg')
|
|
286
|
+
.attr('width', width)
|
|
287
|
+
.attr('height', height)
|
|
288
|
+
.style('background', bgColor);
|
|
257
289
|
return { svg, width, height, textColor, mutedColor, bgColor, colors };
|
|
258
290
|
}
|
|
259
291
|
|
|
@@ -285,7 +317,9 @@ export function parseTimelineDate(s: string): number {
|
|
|
285
317
|
const year = parts[0];
|
|
286
318
|
const month = parts.length >= 2 ? parts[1] : 1;
|
|
287
319
|
const day = parts.length >= 3 ? parts[2] : 1;
|
|
288
|
-
return
|
|
320
|
+
return (
|
|
321
|
+
year + (month - 1) / 12 + (day - 1) / 365 + hour / 8760 + minute / 525600
|
|
322
|
+
);
|
|
289
323
|
}
|
|
290
324
|
|
|
291
325
|
/** Convert a fractional year number back to a Date (inverse of parseTimelineDate). */
|
|
@@ -307,8 +341,13 @@ function fractionalYearToDate(frac: number): Date {
|
|
|
307
341
|
|
|
308
342
|
/** Convert a Date to a fractional year number. */
|
|
309
343
|
function dateToFractionalYear(d: Date): number {
|
|
310
|
-
return
|
|
311
|
-
|
|
344
|
+
return (
|
|
345
|
+
d.getFullYear() +
|
|
346
|
+
d.getMonth() / 12 +
|
|
347
|
+
(d.getDate() - 1) / 365 +
|
|
348
|
+
d.getHours() / 8760 +
|
|
349
|
+
d.getMinutes() / 525600
|
|
350
|
+
);
|
|
312
351
|
}
|
|
313
352
|
|
|
314
353
|
/**
|
|
@@ -404,7 +443,10 @@ export function addDurationToDate(
|
|
|
404
443
|
/**
|
|
405
444
|
* Parses D3 chart text format into structured data.
|
|
406
445
|
*/
|
|
407
|
-
export function parseVisualization(
|
|
446
|
+
export function parseVisualization(
|
|
447
|
+
content: string,
|
|
448
|
+
palette?: PaletteColors
|
|
449
|
+
): ParsedVisualization {
|
|
408
450
|
const result: ParsedVisualization = {
|
|
409
451
|
type: null,
|
|
410
452
|
title: null,
|
|
@@ -468,7 +510,15 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
468
510
|
let inTimelineMarkerBlock = false;
|
|
469
511
|
let timelineMarkerBlockIndent = 0;
|
|
470
512
|
const timelineAliasMap = new Map<string, string>();
|
|
471
|
-
const VALID_D3_TYPES = new Set([
|
|
513
|
+
const VALID_D3_TYPES = new Set([
|
|
514
|
+
'slope',
|
|
515
|
+
'wordcloud',
|
|
516
|
+
'arc',
|
|
517
|
+
'timeline',
|
|
518
|
+
'venn',
|
|
519
|
+
'quadrant',
|
|
520
|
+
'sequence',
|
|
521
|
+
]);
|
|
472
522
|
let firstLineParsed = false;
|
|
473
523
|
|
|
474
524
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -509,7 +559,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
509
559
|
lineNumber,
|
|
510
560
|
};
|
|
511
561
|
if (tagBlockMatch.alias) {
|
|
512
|
-
timelineAliasMap.set(
|
|
562
|
+
timelineAliasMap.set(
|
|
563
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
564
|
+
tagBlockMatch.name.toLowerCase()
|
|
565
|
+
);
|
|
513
566
|
}
|
|
514
567
|
result.timelineTagGroups.push(currentTimelineTagGroup);
|
|
515
568
|
continue;
|
|
@@ -526,7 +579,11 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
526
579
|
const { label, color } = extractColor(entryText, palette);
|
|
527
580
|
if (color) {
|
|
528
581
|
if (isDefault) currentTimelineTagGroup.defaultValue = label;
|
|
529
|
-
currentTimelineTagGroup.entries.push({
|
|
582
|
+
currentTimelineTagGroup.entries.push({
|
|
583
|
+
value: label,
|
|
584
|
+
color,
|
|
585
|
+
lineNumber,
|
|
586
|
+
});
|
|
530
587
|
continue;
|
|
531
588
|
}
|
|
532
589
|
}
|
|
@@ -558,9 +615,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
558
615
|
}
|
|
559
616
|
|
|
560
617
|
// Reject legacy ## group syntax
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
result.
|
|
618
|
+
if (
|
|
619
|
+
/^#{2,}\s+/.test(line) &&
|
|
620
|
+
(result.type === 'arc' || result.type === 'timeline')
|
|
621
|
+
) {
|
|
622
|
+
const name = line
|
|
623
|
+
.replace(/^#{2,}\s+/, '')
|
|
624
|
+
.replace(/\s*\([^)]*\)\s*$/, '')
|
|
625
|
+
.trim();
|
|
626
|
+
result.diagnostics.push(
|
|
627
|
+
makeDgmoError(
|
|
628
|
+
lineNumber,
|
|
629
|
+
`'## ${name}' is no longer supported. Use '[${name}]' instead`,
|
|
630
|
+
'warning'
|
|
631
|
+
)
|
|
632
|
+
);
|
|
564
633
|
continue;
|
|
565
634
|
}
|
|
566
635
|
|
|
@@ -570,10 +639,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
570
639
|
currentTimelineGroup = null;
|
|
571
640
|
}
|
|
572
641
|
|
|
573
|
-
// Arc link line: source -> target(color)
|
|
642
|
+
// Arc link line: source -> target(color) weight
|
|
574
643
|
if (result.type === 'arc') {
|
|
575
644
|
const linkMatch = line.match(
|
|
576
|
-
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(
|
|
645
|
+
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
|
|
577
646
|
);
|
|
578
647
|
if (linkMatch) {
|
|
579
648
|
const source = linkMatch[1].trim();
|
|
@@ -613,7 +682,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
613
682
|
} else {
|
|
614
683
|
if (line.startsWith('//')) continue;
|
|
615
684
|
const eraEntryMatch = line.match(
|
|
616
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
685
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
617
686
|
);
|
|
618
687
|
if (eraEntryMatch) {
|
|
619
688
|
const colorAnnotation = eraEntryMatch[4]?.trim() || null;
|
|
@@ -678,7 +747,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
678
747
|
|
|
679
748
|
// Timeline era lines (inline): era YYYY->YYYY Label (color)
|
|
680
749
|
const eraMatch = line.match(
|
|
681
|
-
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
750
|
+
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
682
751
|
);
|
|
683
752
|
if (eraMatch) {
|
|
684
753
|
const colorAnnotation = eraMatch[4]?.trim() || null;
|
|
@@ -696,7 +765,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
696
765
|
|
|
697
766
|
// Timeline marker lines (inline): marker YYYY Label (color)
|
|
698
767
|
const markerMatch = line.match(
|
|
699
|
-
/^marker
|
|
768
|
+
/^marker\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
700
769
|
);
|
|
701
770
|
if (markerMatch) {
|
|
702
771
|
const colorAnnotation = markerMatch[3]?.trim() || null;
|
|
@@ -719,7 +788,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
719
788
|
// Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
|
|
720
789
|
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
721
790
|
const durationMatch = line.match(
|
|
722
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)
|
|
791
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?\s+(.+)$/
|
|
723
792
|
);
|
|
724
793
|
if (durationMatch) {
|
|
725
794
|
const startDate = durationMatch[1];
|
|
@@ -728,9 +797,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
728
797
|
const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
|
|
729
798
|
const endDate = addDurationToDate(startDate, amount, unit);
|
|
730
799
|
const segments = durationMatch[5].split('|');
|
|
731
|
-
const metadata =
|
|
732
|
-
|
|
733
|
-
|
|
800
|
+
const metadata =
|
|
801
|
+
segments.length > 1
|
|
802
|
+
? parsePipeMetadata(
|
|
803
|
+
['', ...segments.slice(1)],
|
|
804
|
+
timelineAliasMap,
|
|
805
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
806
|
+
)
|
|
807
|
+
: {};
|
|
734
808
|
result.timelineEvents.push({
|
|
735
809
|
date: startDate,
|
|
736
810
|
endDate,
|
|
@@ -747,13 +821,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
747
821
|
// Also supports YYYY-MM-DD HH:MM in both start and end dates
|
|
748
822
|
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
749
823
|
const rangeMatch = line.match(
|
|
750
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)(\?)
|
|
824
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)(\?)?\s+(.+)$/
|
|
751
825
|
);
|
|
752
826
|
if (rangeMatch) {
|
|
753
827
|
const segments = rangeMatch[4].split('|');
|
|
754
|
-
const metadata =
|
|
755
|
-
|
|
756
|
-
|
|
828
|
+
const metadata =
|
|
829
|
+
segments.length > 1
|
|
830
|
+
? parsePipeMetadata(
|
|
831
|
+
['', ...segments.slice(1)],
|
|
832
|
+
timelineAliasMap,
|
|
833
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
834
|
+
)
|
|
835
|
+
: {};
|
|
757
836
|
result.timelineEvents.push({
|
|
758
837
|
date: rangeMatch[1],
|
|
759
838
|
endDate: rangeMatch[2],
|
|
@@ -766,15 +845,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
766
845
|
continue;
|
|
767
846
|
}
|
|
768
847
|
|
|
769
|
-
// Point event: 1718 description
|
|
770
|
-
const pointMatch = line.match(
|
|
771
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2})?)(?:\s*:\s*|\s+)(.+)$/
|
|
772
|
-
);
|
|
848
|
+
// Point event: 1718 description
|
|
849
|
+
const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
|
|
773
850
|
if (pointMatch) {
|
|
774
851
|
const segments = pointMatch[2].split('|');
|
|
775
|
-
const metadata =
|
|
776
|
-
|
|
777
|
-
|
|
852
|
+
const metadata =
|
|
853
|
+
segments.length > 1
|
|
854
|
+
? parsePipeMetadata(
|
|
855
|
+
['', ...segments.slice(1)],
|
|
856
|
+
timelineAliasMap,
|
|
857
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
858
|
+
)
|
|
859
|
+
: {};
|
|
778
860
|
result.timelineEvents.push({
|
|
779
861
|
date: pointMatch[1],
|
|
780
862
|
endDate: null,
|
|
@@ -799,40 +881,37 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
799
881
|
if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
|
|
800
882
|
}
|
|
801
883
|
|
|
802
|
-
const segments = line
|
|
884
|
+
const segments = line
|
|
885
|
+
.split('+')
|
|
886
|
+
.map((s) => s.trim())
|
|
887
|
+
.filter(Boolean);
|
|
803
888
|
if (segments.length >= 2) {
|
|
804
889
|
// All segments except the last are pure set references
|
|
805
890
|
const rawSets = segments.slice(0, -1);
|
|
806
891
|
const lastSeg = segments[segments.length - 1];
|
|
807
892
|
|
|
808
893
|
// For the last segment, extract set reference and optional label.
|
|
809
|
-
//
|
|
810
|
-
|
|
894
|
+
// Find where the set reference ends and label begins.
|
|
895
|
+
// Try progressively shorter prefixes against known set names/aliases.
|
|
896
|
+
const words = lastSeg.split(/\s+/);
|
|
897
|
+
let matchLen = 0;
|
|
898
|
+
for (let w = words.length; w >= 1; w--) {
|
|
899
|
+
const candidate = words.slice(0, w).join(' ');
|
|
900
|
+
if (knownSetRefs.has(candidate.toLowerCase())) {
|
|
901
|
+
matchLen = w;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
811
905
|
let lastSetRef: string;
|
|
812
906
|
let label: string | null;
|
|
813
|
-
if (
|
|
814
|
-
lastSetRef =
|
|
815
|
-
label =
|
|
907
|
+
if (matchLen > 0) {
|
|
908
|
+
lastSetRef = words.slice(0, matchLen).join(' ');
|
|
909
|
+
label =
|
|
910
|
+
words.length > matchLen ? words.slice(matchLen).join(' ') : null;
|
|
816
911
|
} else {
|
|
817
|
-
// No
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
let matchLen = 0;
|
|
821
|
-
for (let w = words.length; w >= 1; w--) {
|
|
822
|
-
const candidate = words.slice(0, w).join(' ');
|
|
823
|
-
if (knownSetRefs.has(candidate.toLowerCase())) {
|
|
824
|
-
matchLen = w;
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
if (matchLen > 0) {
|
|
829
|
-
lastSetRef = words.slice(0, matchLen).join(' ');
|
|
830
|
-
label = words.length > matchLen ? words.slice(matchLen).join(' ') : null;
|
|
831
|
-
} else {
|
|
832
|
-
// No known set matched — assume first word is the set ref, rest is label
|
|
833
|
-
lastSetRef = words[0];
|
|
834
|
-
label = words.length > 1 ? words.slice(1).join(' ') : null;
|
|
835
|
-
}
|
|
912
|
+
// No known set matched — assume first word is the set ref, rest is label
|
|
913
|
+
lastSetRef = words[0];
|
|
914
|
+
label = words.length > 1 ? words.slice(1).join(' ') : null;
|
|
836
915
|
}
|
|
837
916
|
rawSets.push(lastSetRef);
|
|
838
917
|
result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
|
|
@@ -841,7 +920,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
841
920
|
}
|
|
842
921
|
|
|
843
922
|
// Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
|
|
844
|
-
const setDeclMatch = line.match(
|
|
923
|
+
const setDeclMatch = line.match(
|
|
924
|
+
/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
|
|
925
|
+
);
|
|
845
926
|
if (setDeclMatch) {
|
|
846
927
|
const name = setDeclMatch[1].trim();
|
|
847
928
|
const colorName = setDeclMatch[2]?.trim() ?? null;
|
|
@@ -849,11 +930,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
849
930
|
if (colorName) {
|
|
850
931
|
const resolved = resolveColor(colorName, palette);
|
|
851
932
|
if (resolved === null) {
|
|
852
|
-
warn(
|
|
933
|
+
warn(
|
|
934
|
+
lineNumber,
|
|
935
|
+
`Hex colors are not supported — use named colors (blue, red, green, etc.)`
|
|
936
|
+
);
|
|
853
937
|
} else if (resolved.startsWith('#')) {
|
|
854
938
|
color = resolved;
|
|
855
939
|
} else {
|
|
856
|
-
warn(
|
|
940
|
+
warn(
|
|
941
|
+
lineNumber,
|
|
942
|
+
`Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`
|
|
943
|
+
);
|
|
857
944
|
}
|
|
858
945
|
}
|
|
859
946
|
const alias = setDeclMatch[3]?.trim() ?? null;
|
|
@@ -864,8 +951,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
864
951
|
|
|
865
952
|
// Quadrant-specific parsing
|
|
866
953
|
if (result.type === 'quadrant') {
|
|
867
|
-
// x-
|
|
868
|
-
const xAxisMatch = line.match(/^x-
|
|
954
|
+
// x-label Low, High — or indented multi-line
|
|
955
|
+
const xAxisMatch = line.match(/^x-label\s+(.*)/i);
|
|
869
956
|
if (xAxisMatch) {
|
|
870
957
|
const val = xAxisMatch[1].trim();
|
|
871
958
|
let parts: string[];
|
|
@@ -883,8 +970,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
883
970
|
continue;
|
|
884
971
|
}
|
|
885
972
|
|
|
886
|
-
// y-
|
|
887
|
-
const yAxisMatch = line.match(/^y-
|
|
973
|
+
// y-label Low, High — or indented multi-line
|
|
974
|
+
const yAxisMatch = line.match(/^y-label\s+(.*)/i);
|
|
888
975
|
if (yAxisMatch) {
|
|
889
976
|
const val = yAxisMatch[1].trim();
|
|
890
977
|
let parts: string[];
|
|
@@ -902,9 +989,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
902
989
|
continue;
|
|
903
990
|
}
|
|
904
991
|
|
|
905
|
-
// Quadrant position labels: top-right
|
|
992
|
+
// Quadrant position labels: top-right Label (color)
|
|
906
993
|
const quadrantLabelRe =
|
|
907
|
-
/^(top-right|top-left|bottom-left|bottom-right)\s
|
|
994
|
+
/^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
|
|
908
995
|
const quadrantMatch = line.match(quadrantLabelRe);
|
|
909
996
|
if (quadrantMatch) {
|
|
910
997
|
const position = quadrantMatch[1].toLowerCase();
|
|
@@ -926,9 +1013,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
926
1013
|
continue;
|
|
927
1014
|
}
|
|
928
1015
|
|
|
929
|
-
// Data points: Label
|
|
1016
|
+
// Data points: Label x, y
|
|
930
1017
|
const pointMatch = line.match(
|
|
931
|
-
/^(.+?)
|
|
1018
|
+
/^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
|
|
932
1019
|
);
|
|
933
1020
|
if (pointMatch) {
|
|
934
1021
|
const label = pointMatch[1].trim();
|
|
@@ -957,7 +1044,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
957
1044
|
const firstToken = line.substring(0, spaceIdx).toLowerCase();
|
|
958
1045
|
const restValue = line.substring(spaceIdx + 1).trim();
|
|
959
1046
|
|
|
960
|
-
if (
|
|
1047
|
+
if (
|
|
1048
|
+
firstToken === 'chart' &&
|
|
1049
|
+
VALID_D3_TYPES.has(restValue.toLowerCase())
|
|
1050
|
+
) {
|
|
961
1051
|
result.type = restValue.toLowerCase() as ParsedVisualization['type'];
|
|
962
1052
|
continue;
|
|
963
1053
|
}
|
|
@@ -1114,9 +1204,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1114
1204
|
} else if (colonIndex === -1) {
|
|
1115
1205
|
// Try "word weight" or "multi-word-label weight" space-separated format
|
|
1116
1206
|
const lastSpace = line.lastIndexOf(' ');
|
|
1117
|
-
const maybeWeight =
|
|
1207
|
+
const maybeWeight =
|
|
1208
|
+
lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
|
|
1118
1209
|
if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
|
|
1119
|
-
result.words.push({
|
|
1210
|
+
result.words.push({
|
|
1211
|
+
text: line.substring(0, lastSpace).trim(),
|
|
1212
|
+
weight: maybeWeight,
|
|
1213
|
+
lineNumber,
|
|
1214
|
+
});
|
|
1120
1215
|
} else {
|
|
1121
1216
|
freeformLines.push(line);
|
|
1122
1217
|
}
|
|
@@ -1143,13 +1238,23 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1143
1238
|
continue;
|
|
1144
1239
|
}
|
|
1145
1240
|
}
|
|
1241
|
+
|
|
1242
|
+
// Catch-all: nothing matched this line
|
|
1243
|
+
// Skip on first line — chart type suggestion is handled post-loop
|
|
1244
|
+
if (firstLineParsed) {
|
|
1245
|
+
warn(lineNumber, `Unexpected line: '${line}'.`);
|
|
1246
|
+
}
|
|
1146
1247
|
}
|
|
1147
1248
|
|
|
1148
1249
|
// Validation
|
|
1149
1250
|
if (!result.type) {
|
|
1150
1251
|
const validD3Types = [...VALID_D3_TYPES];
|
|
1151
|
-
const firstNonEmpty =
|
|
1152
|
-
|
|
1252
|
+
const firstNonEmpty =
|
|
1253
|
+
lines.find((l) => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
|
|
1254
|
+
const hint = suggest(
|
|
1255
|
+
firstNonEmpty.split(/\s/)[0].toLowerCase(),
|
|
1256
|
+
validD3Types
|
|
1257
|
+
);
|
|
1153
1258
|
let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
|
|
1154
1259
|
if (hint) msg += `. ${hint}`;
|
|
1155
1260
|
return fail(1, msg);
|
|
@@ -1166,7 +1271,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1166
1271
|
result.words = tokenizeFreeformText(freeformLines.join(' '));
|
|
1167
1272
|
}
|
|
1168
1273
|
if (result.words.length === 0) {
|
|
1169
|
-
warn(
|
|
1274
|
+
warn(
|
|
1275
|
+
1,
|
|
1276
|
+
'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text'
|
|
1277
|
+
);
|
|
1170
1278
|
}
|
|
1171
1279
|
// Apply max word limit (words are already sorted by weight desc for freeform)
|
|
1172
1280
|
if (
|
|
@@ -1183,12 +1291,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1183
1291
|
|
|
1184
1292
|
if (result.type === 'arc') {
|
|
1185
1293
|
if (result.links.length === 0) {
|
|
1186
|
-
warn(
|
|
1294
|
+
warn(
|
|
1295
|
+
1,
|
|
1296
|
+
'No links found. Add links as "Source -> Target weight" (e.g., "Alice -> Bob 5")'
|
|
1297
|
+
);
|
|
1187
1298
|
}
|
|
1188
1299
|
// Validate arc ordering vs groups
|
|
1189
1300
|
if (result.arcNodeGroups.length > 0) {
|
|
1190
1301
|
if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
|
|
1191
|
-
warn(
|
|
1302
|
+
warn(
|
|
1303
|
+
1,
|
|
1304
|
+
`Cannot use "order ${result.arcOrder}" with [Group] headers. Use "order group" or remove group headers.`
|
|
1305
|
+
);
|
|
1192
1306
|
result.arcOrder = 'group';
|
|
1193
1307
|
}
|
|
1194
1308
|
if (result.arcOrder === 'appearance') {
|
|
@@ -1200,15 +1314,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1200
1314
|
|
|
1201
1315
|
if (result.type === 'timeline') {
|
|
1202
1316
|
if (result.timelineEvents.length === 0) {
|
|
1203
|
-
warn(
|
|
1317
|
+
warn(
|
|
1318
|
+
1,
|
|
1319
|
+
'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"'
|
|
1320
|
+
);
|
|
1204
1321
|
}
|
|
1205
1322
|
// Validate tag values and inject defaults
|
|
1206
1323
|
if (result.timelineTagGroups.length > 0) {
|
|
1207
1324
|
validateTagValues(
|
|
1208
1325
|
result.timelineEvents,
|
|
1209
1326
|
result.timelineTagGroups,
|
|
1210
|
-
(line, msg) =>
|
|
1211
|
-
|
|
1327
|
+
(line, msg) =>
|
|
1328
|
+
result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
|
|
1329
|
+
suggest
|
|
1212
1330
|
);
|
|
1213
1331
|
for (const group of result.timelineTagGroups) {
|
|
1214
1332
|
if (!group.defaultValue) continue;
|
|
@@ -1226,7 +1344,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1226
1344
|
|
|
1227
1345
|
if (result.type === 'venn') {
|
|
1228
1346
|
if (result.vennSets.length < 2) {
|
|
1229
|
-
return fail(
|
|
1347
|
+
return fail(
|
|
1348
|
+
1,
|
|
1349
|
+
'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")'
|
|
1350
|
+
);
|
|
1230
1351
|
}
|
|
1231
1352
|
if (result.vennSets.length > 3) {
|
|
1232
1353
|
return fail(1, 'Venn diagrams support 2–3 sets');
|
|
@@ -1240,7 +1361,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1240
1361
|
if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
|
|
1241
1362
|
}
|
|
1242
1363
|
const resolveSetRef = (ref: string): string | null =>
|
|
1243
|
-
setNameLower.get(ref.toLowerCase()) ??
|
|
1364
|
+
setNameLower.get(ref.toLowerCase()) ??
|
|
1365
|
+
aliasLower.get(ref.toLowerCase()) ??
|
|
1366
|
+
null;
|
|
1244
1367
|
|
|
1245
1368
|
// Resolve intersection set references; drop invalid ones with a diagnostic
|
|
1246
1369
|
const validOverlaps: VennOverlap[] = [];
|
|
@@ -1250,8 +1373,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1250
1373
|
for (const ref of ov.sets) {
|
|
1251
1374
|
const resolved = resolveSetRef(ref);
|
|
1252
1375
|
if (!resolved) {
|
|
1253
|
-
result.diagnostics.push(
|
|
1254
|
-
|
|
1376
|
+
result.diagnostics.push(
|
|
1377
|
+
makeDgmoError(
|
|
1378
|
+
ov.lineNumber,
|
|
1379
|
+
`Intersection references unknown set or alias "${ref}"`
|
|
1380
|
+
)
|
|
1381
|
+
);
|
|
1382
|
+
if (!result.error)
|
|
1383
|
+
result.error = formatDgmoError(
|
|
1384
|
+
result.diagnostics[result.diagnostics.length - 1]
|
|
1385
|
+
);
|
|
1255
1386
|
valid = false;
|
|
1256
1387
|
break;
|
|
1257
1388
|
}
|
|
@@ -1265,24 +1396,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1265
1396
|
|
|
1266
1397
|
if (result.type === 'quadrant') {
|
|
1267
1398
|
if (result.quadrantPoints.length === 0) {
|
|
1268
|
-
warn(
|
|
1399
|
+
warn(
|
|
1400
|
+
1,
|
|
1401
|
+
'No data points found. Add points as "Label x, y" (e.g., "Item A 0.5, 0.7")'
|
|
1402
|
+
);
|
|
1269
1403
|
}
|
|
1270
1404
|
return result;
|
|
1271
1405
|
}
|
|
1272
1406
|
|
|
1273
1407
|
// Slope chart validation
|
|
1274
1408
|
if (result.periods.length < 2) {
|
|
1275
|
-
return fail(
|
|
1409
|
+
return fail(
|
|
1410
|
+
1,
|
|
1411
|
+
'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")'
|
|
1412
|
+
);
|
|
1276
1413
|
}
|
|
1277
1414
|
|
|
1278
1415
|
if (result.data.length === 0) {
|
|
1279
|
-
warn(
|
|
1416
|
+
warn(
|
|
1417
|
+
1,
|
|
1418
|
+
'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")'
|
|
1419
|
+
);
|
|
1280
1420
|
}
|
|
1281
1421
|
|
|
1282
1422
|
// Validate value counts match period count — warn and skip mismatched items
|
|
1283
1423
|
for (const item of result.data) {
|
|
1284
1424
|
if (item.values.length !== result.periods.length) {
|
|
1285
|
-
warn(
|
|
1425
|
+
warn(
|
|
1426
|
+
item.lineNumber,
|
|
1427
|
+
`Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`
|
|
1428
|
+
);
|
|
1286
1429
|
}
|
|
1287
1430
|
}
|
|
1288
1431
|
result.data = result.data.filter(
|
|
@@ -1523,7 +1666,14 @@ export function renderSlopeChart(
|
|
|
1523
1666
|
const tooltip = createTooltip(container, palette, isDark);
|
|
1524
1667
|
|
|
1525
1668
|
// Title
|
|
1526
|
-
renderChartTitle(
|
|
1669
|
+
renderChartTitle(
|
|
1670
|
+
svg,
|
|
1671
|
+
title,
|
|
1672
|
+
parsed.titleLineNumber,
|
|
1673
|
+
width,
|
|
1674
|
+
textColor,
|
|
1675
|
+
onClickItem
|
|
1676
|
+
);
|
|
1527
1677
|
|
|
1528
1678
|
// Period column headers
|
|
1529
1679
|
for (const period of periods) {
|
|
@@ -1592,13 +1742,23 @@ export function renderSlopeChart(
|
|
|
1592
1742
|
wrappedLines = lines;
|
|
1593
1743
|
}
|
|
1594
1744
|
const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
|
|
1595
|
-
const labelHeight =
|
|
1596
|
-
|
|
1597
|
-
|
|
1745
|
+
const labelHeight =
|
|
1746
|
+
labelLineCount === 1
|
|
1747
|
+
? SLOPE_LABEL_FONT_SIZE
|
|
1748
|
+
: labelLineCount * lineHeight;
|
|
1598
1749
|
|
|
1599
1750
|
return {
|
|
1600
|
-
item,
|
|
1601
|
-
|
|
1751
|
+
item,
|
|
1752
|
+
idx,
|
|
1753
|
+
color,
|
|
1754
|
+
firstVal,
|
|
1755
|
+
lastVal,
|
|
1756
|
+
tipHtml,
|
|
1757
|
+
lastX,
|
|
1758
|
+
labelText,
|
|
1759
|
+
maxChars,
|
|
1760
|
+
wrappedLines,
|
|
1761
|
+
labelHeight,
|
|
1602
1762
|
};
|
|
1603
1763
|
});
|
|
1604
1764
|
|
|
@@ -1610,7 +1770,10 @@ export function renderSlopeChart(
|
|
|
1610
1770
|
naturalY: yScale(item.values[pi]),
|
|
1611
1771
|
height: leftLabelHeight,
|
|
1612
1772
|
}));
|
|
1613
|
-
leftLabelCollisions.set(
|
|
1773
|
+
leftLabelCollisions.set(
|
|
1774
|
+
pi,
|
|
1775
|
+
resolveVerticalCollisions(entries, 4, innerHeight)
|
|
1776
|
+
);
|
|
1614
1777
|
}
|
|
1615
1778
|
|
|
1616
1779
|
// --- Resolve right-side label collisions ---
|
|
@@ -1618,7 +1781,11 @@ export function renderSlopeChart(
|
|
|
1618
1781
|
naturalY: yScale(si.lastVal),
|
|
1619
1782
|
height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
|
|
1620
1783
|
}));
|
|
1621
|
-
const rightAdjustedY = resolveVerticalCollisions(
|
|
1784
|
+
const rightAdjustedY = resolveVerticalCollisions(
|
|
1785
|
+
rightEntries,
|
|
1786
|
+
4,
|
|
1787
|
+
innerHeight
|
|
1788
|
+
);
|
|
1622
1789
|
|
|
1623
1790
|
// Render each data series
|
|
1624
1791
|
data.forEach((item, idx) => {
|
|
@@ -1632,7 +1799,8 @@ export function renderSlopeChart(
|
|
|
1632
1799
|
.attr('data-line-number', String(item.lineNumber));
|
|
1633
1800
|
|
|
1634
1801
|
// Line
|
|
1635
|
-
seriesG
|
|
1802
|
+
seriesG
|
|
1803
|
+
.append('path')
|
|
1636
1804
|
.datum(item.values)
|
|
1637
1805
|
.attr('fill', 'none')
|
|
1638
1806
|
.attr('stroke', color)
|
|
@@ -1640,7 +1808,8 @@ export function renderSlopeChart(
|
|
|
1640
1808
|
.attr('d', lineGen);
|
|
1641
1809
|
|
|
1642
1810
|
// Invisible wider path for easier hover targeting
|
|
1643
|
-
seriesG
|
|
1811
|
+
seriesG
|
|
1812
|
+
.append('path')
|
|
1644
1813
|
.datum(item.values)
|
|
1645
1814
|
.attr('fill', 'none')
|
|
1646
1815
|
.attr('stroke', 'transparent')
|
|
@@ -1664,7 +1833,8 @@ export function renderSlopeChart(
|
|
|
1664
1833
|
const y = yScale(val);
|
|
1665
1834
|
|
|
1666
1835
|
// Point circle
|
|
1667
|
-
seriesG
|
|
1836
|
+
seriesG
|
|
1837
|
+
.append('circle')
|
|
1668
1838
|
.attr('cx', x)
|
|
1669
1839
|
.attr('cy', y)
|
|
1670
1840
|
.attr('r', 4)
|
|
@@ -1688,7 +1858,8 @@ export function renderSlopeChart(
|
|
|
1688
1858
|
const isLast = i === periods.length - 1;
|
|
1689
1859
|
if (!isLast) {
|
|
1690
1860
|
const adjustedY = leftLabelCollisions.get(i)![idx];
|
|
1691
|
-
seriesG
|
|
1861
|
+
seriesG
|
|
1862
|
+
.append('text')
|
|
1692
1863
|
.attr('x', isFirst ? x - 10 : x)
|
|
1693
1864
|
.attr('y', adjustedY)
|
|
1694
1865
|
.attr('dy', '0.35em')
|
|
@@ -1920,7 +2091,14 @@ export function renderArcDiagram(
|
|
|
1920
2091
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
1921
2092
|
|
|
1922
2093
|
// Title
|
|
1923
|
-
renderChartTitle(
|
|
2094
|
+
renderChartTitle(
|
|
2095
|
+
svg,
|
|
2096
|
+
title,
|
|
2097
|
+
parsed.titleLineNumber,
|
|
2098
|
+
width,
|
|
2099
|
+
textColor,
|
|
2100
|
+
onClickItem
|
|
2101
|
+
);
|
|
1924
2102
|
|
|
1925
2103
|
// Build adjacency map for hover interactions
|
|
1926
2104
|
const neighbors = new Map<string, Set<string>>();
|
|
@@ -2097,13 +2275,18 @@ export function renderArcDiagram(
|
|
|
2097
2275
|
const y = yScale(node)!;
|
|
2098
2276
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2099
2277
|
// Find the first link involving this node (for line number and click target)
|
|
2100
|
-
const nodeLink = links.find(
|
|
2278
|
+
const nodeLink = links.find(
|
|
2279
|
+
(l) => l.source === node || l.target === node
|
|
2280
|
+
);
|
|
2101
2281
|
|
|
2102
2282
|
const nodeG = g
|
|
2103
2283
|
.append('g')
|
|
2104
2284
|
.attr('class', 'arc-node')
|
|
2105
2285
|
.attr('data-node', node)
|
|
2106
|
-
.attr(
|
|
2286
|
+
.attr(
|
|
2287
|
+
'data-line-number',
|
|
2288
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2289
|
+
)
|
|
2107
2290
|
.style('cursor', 'pointer')
|
|
2108
2291
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2109
2292
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2232,13 +2415,18 @@ export function renderArcDiagram(
|
|
|
2232
2415
|
const x = xScale(node)!;
|
|
2233
2416
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2234
2417
|
// Find the first link involving this node (for line number and click target)
|
|
2235
|
-
const nodeLink = links.find(
|
|
2418
|
+
const nodeLink = links.find(
|
|
2419
|
+
(l) => l.source === node || l.target === node
|
|
2420
|
+
);
|
|
2236
2421
|
|
|
2237
2422
|
const nodeG = g
|
|
2238
2423
|
.append('g')
|
|
2239
2424
|
.attr('class', 'arc-node')
|
|
2240
2425
|
.attr('data-node', node)
|
|
2241
|
-
.attr(
|
|
2426
|
+
.attr(
|
|
2427
|
+
'data-line-number',
|
|
2428
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2429
|
+
)
|
|
2242
2430
|
.style('cursor', 'pointer')
|
|
2243
2431
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2244
2432
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2633,7 +2821,11 @@ export function computeTimeTicks(
|
|
|
2633
2821
|
// Iterate from the start hour boundary
|
|
2634
2822
|
const startDate = fractionalYearToDate(domainMin);
|
|
2635
2823
|
// Round down to nearest step boundary
|
|
2636
|
-
startDate.setMinutes(
|
|
2824
|
+
startDate.setMinutes(
|
|
2825
|
+
Math.floor(startDate.getMinutes() / stepMin) * stepMin,
|
|
2826
|
+
0,
|
|
2827
|
+
0
|
|
2828
|
+
);
|
|
2637
2829
|
|
|
2638
2830
|
while (true) {
|
|
2639
2831
|
const val = dateToFractionalYear(startDate);
|
|
@@ -2659,7 +2851,12 @@ export function computeTimeTicks(
|
|
|
2659
2851
|
|
|
2660
2852
|
const startDate = fractionalYearToDate(domainMin);
|
|
2661
2853
|
// Round down to nearest step boundary
|
|
2662
|
-
startDate.setHours(
|
|
2854
|
+
startDate.setHours(
|
|
2855
|
+
Math.floor(startDate.getHours() / stepHour) * stepHour,
|
|
2856
|
+
0,
|
|
2857
|
+
0,
|
|
2858
|
+
0
|
|
2859
|
+
);
|
|
2663
2860
|
|
|
2664
2861
|
while (true) {
|
|
2665
2862
|
const val = dateToFractionalYear(startDate);
|
|
@@ -3072,7 +3269,10 @@ export function renderTimeline(
|
|
|
3072
3269
|
exportDims?: D3ExportDimensions,
|
|
3073
3270
|
activeTagGroup?: string | null,
|
|
3074
3271
|
swimlaneTagGroup?: string | null,
|
|
3075
|
-
onTagStateChange?: (
|
|
3272
|
+
onTagStateChange?: (
|
|
3273
|
+
activeTagGroup: string | null,
|
|
3274
|
+
swimlaneTagGroup: string | null
|
|
3275
|
+
) => void,
|
|
3076
3276
|
viewMode?: boolean
|
|
3077
3277
|
): void {
|
|
3078
3278
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
@@ -3091,7 +3291,11 @@ export function renderTimeline(
|
|
|
3091
3291
|
if (timelineEvents.length === 0) return;
|
|
3092
3292
|
|
|
3093
3293
|
// When sort: tag is set and no explicit swimlane param, use the default
|
|
3094
|
-
if (
|
|
3294
|
+
if (
|
|
3295
|
+
swimlaneTagGroup == null &&
|
|
3296
|
+
timelineSort === 'tag' &&
|
|
3297
|
+
parsed.timelineDefaultSwimlaneTG
|
|
3298
|
+
) {
|
|
3095
3299
|
swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
|
|
3096
3300
|
}
|
|
3097
3301
|
|
|
@@ -3143,12 +3347,8 @@ export function renderTimeline(
|
|
|
3143
3347
|
|
|
3144
3348
|
// Order lanes by earliest event date
|
|
3145
3349
|
const laneEntries = [...buckets.entries()].sort((a, b) => {
|
|
3146
|
-
const aMin = Math.min(
|
|
3147
|
-
|
|
3148
|
-
);
|
|
3149
|
-
const bMin = Math.min(
|
|
3150
|
-
...b[1].map((e) => parseTimelineDate(e.date))
|
|
3151
|
-
);
|
|
3350
|
+
const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
|
|
3351
|
+
const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
|
|
3152
3352
|
return aMin - bMin;
|
|
3153
3353
|
});
|
|
3154
3354
|
|
|
@@ -3170,7 +3370,11 @@ export function renderTimeline(
|
|
|
3170
3370
|
function eventColor(ev: TimelineEvent): string {
|
|
3171
3371
|
// Tag color takes priority when a tag group is active
|
|
3172
3372
|
if (effectiveColorTG) {
|
|
3173
|
-
const tagColor = resolveTagColor(
|
|
3373
|
+
const tagColor = resolveTagColor(
|
|
3374
|
+
ev.metadata,
|
|
3375
|
+
parsed.timelineTagGroups,
|
|
3376
|
+
effectiveColorTG
|
|
3377
|
+
);
|
|
3174
3378
|
if (tagColor) return tagColor;
|
|
3175
3379
|
}
|
|
3176
3380
|
if (ev.group && groupColorMap.has(ev.group)) {
|
|
@@ -3281,16 +3485,23 @@ export function renderTimeline(
|
|
|
3281
3485
|
el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
|
|
3282
3486
|
});
|
|
3283
3487
|
g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
|
|
3284
|
-
'opacity',
|
|
3488
|
+
'opacity',
|
|
3489
|
+
FADE_OPACITY
|
|
3490
|
+
);
|
|
3491
|
+
g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
|
|
3492
|
+
'opacity',
|
|
3493
|
+
FADE_OPACITY
|
|
3285
3494
|
);
|
|
3286
|
-
g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
|
|
3287
3495
|
// Fade legend entry dots/labels that don't match (keep group pill visible)
|
|
3288
3496
|
g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
|
|
3289
3497
|
const el = d3Selection.select(this);
|
|
3290
3498
|
const entryValue = el.attr('data-legend-entry');
|
|
3291
3499
|
if (entryValue === '__group__') return; // keep group pill at full opacity
|
|
3292
3500
|
const entryGroup = el.attr('data-tag-group');
|
|
3293
|
-
el.attr(
|
|
3501
|
+
el.attr(
|
|
3502
|
+
'opacity',
|
|
3503
|
+
entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
|
|
3504
|
+
);
|
|
3294
3505
|
});
|
|
3295
3506
|
}
|
|
3296
3507
|
|
|
@@ -3311,7 +3522,8 @@ export function renderTimeline(
|
|
|
3311
3522
|
// VERTICAL orientation (time flows top→bottom)
|
|
3312
3523
|
// ================================================================
|
|
3313
3524
|
if (isVertical) {
|
|
3314
|
-
const useGroupedVertical =
|
|
3525
|
+
const useGroupedVertical =
|
|
3526
|
+
tagLanes != null ||
|
|
3315
3527
|
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
3316
3528
|
if (useGroupedVertical) {
|
|
3317
3529
|
// === GROUPED: one column/lane per group, vertical ===
|
|
@@ -3370,7 +3582,14 @@ export function renderTimeline(
|
|
|
3370
3582
|
.append('g')
|
|
3371
3583
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3372
3584
|
|
|
3373
|
-
renderChartTitle(
|
|
3585
|
+
renderChartTitle(
|
|
3586
|
+
svg,
|
|
3587
|
+
title,
|
|
3588
|
+
parsed.titleLineNumber,
|
|
3589
|
+
width,
|
|
3590
|
+
textColor,
|
|
3591
|
+
onClickItem
|
|
3592
|
+
);
|
|
3374
3593
|
|
|
3375
3594
|
renderEras(
|
|
3376
3595
|
g,
|
|
@@ -3620,7 +3839,14 @@ export function renderTimeline(
|
|
|
3620
3839
|
.append('g')
|
|
3621
3840
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3622
3841
|
|
|
3623
|
-
renderChartTitle(
|
|
3842
|
+
renderChartTitle(
|
|
3843
|
+
svg,
|
|
3844
|
+
title,
|
|
3845
|
+
parsed.titleLineNumber,
|
|
3846
|
+
width,
|
|
3847
|
+
textColor,
|
|
3848
|
+
onClickItem
|
|
3849
|
+
);
|
|
3624
3850
|
|
|
3625
3851
|
renderEras(
|
|
3626
3852
|
g,
|
|
@@ -3746,8 +3972,7 @@ export function renderTimeline(
|
|
|
3746
3972
|
if (ev.uncertain) {
|
|
3747
3973
|
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3748
3974
|
const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
|
|
3749
|
-
const defs =
|
|
3750
|
-
svg.select('defs').node() || svg.append('defs').node();
|
|
3975
|
+
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
3751
3976
|
const defsEl = d3Selection.select(defs as Element);
|
|
3752
3977
|
defsEl
|
|
3753
3978
|
.append('linearGradient')
|
|
@@ -3861,8 +4086,8 @@ export function renderTimeline(
|
|
|
3861
4086
|
const BAR_H = 22; // range bar thickness (tall enough for text inside)
|
|
3862
4087
|
const GROUP_GAP = 12; // vertical gap between group swim-lanes
|
|
3863
4088
|
|
|
3864
|
-
const useGroupedHorizontal =
|
|
3865
|
-
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
4089
|
+
const useGroupedHorizontal =
|
|
4090
|
+
tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
|
|
3866
4091
|
if (useGroupedHorizontal) {
|
|
3867
4092
|
// === GROUPED: swim-lanes stacked vertically, events on own rows ===
|
|
3868
4093
|
let lanes: Lane[];
|
|
@@ -3895,7 +4120,11 @@ export function renderTimeline(
|
|
|
3895
4120
|
// Group-sorted doesn't need legend space (group names shown on left)
|
|
3896
4121
|
const baseTopMargin = title ? 50 : 20;
|
|
3897
4122
|
const margin = {
|
|
3898
|
-
top:
|
|
4123
|
+
top:
|
|
4124
|
+
baseTopMargin +
|
|
4125
|
+
(timelineScale ? 40 : 0) +
|
|
4126
|
+
markerMargin +
|
|
4127
|
+
tagLegendReserve,
|
|
3899
4128
|
right: 40,
|
|
3900
4129
|
bottom: 40 + scaleMargin,
|
|
3901
4130
|
left: dynamicLeftMargin,
|
|
@@ -3921,7 +4150,14 @@ export function renderTimeline(
|
|
|
3921
4150
|
.append('g')
|
|
3922
4151
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3923
4152
|
|
|
3924
|
-
renderChartTitle(
|
|
4153
|
+
renderChartTitle(
|
|
4154
|
+
svg,
|
|
4155
|
+
title,
|
|
4156
|
+
parsed.titleLineNumber,
|
|
4157
|
+
width,
|
|
4158
|
+
textColor,
|
|
4159
|
+
onClickItem
|
|
4160
|
+
);
|
|
3925
4161
|
|
|
3926
4162
|
renderEras(
|
|
3927
4163
|
g,
|
|
@@ -4222,7 +4458,14 @@ export function renderTimeline(
|
|
|
4222
4458
|
.append('g')
|
|
4223
4459
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
4224
4460
|
|
|
4225
|
-
renderChartTitle(
|
|
4461
|
+
renderChartTitle(
|
|
4462
|
+
svg,
|
|
4463
|
+
title,
|
|
4464
|
+
parsed.titleLineNumber,
|
|
4465
|
+
width,
|
|
4466
|
+
textColor,
|
|
4467
|
+
onClickItem
|
|
4468
|
+
);
|
|
4226
4469
|
|
|
4227
4470
|
renderEras(
|
|
4228
4471
|
g,
|
|
@@ -4500,13 +4743,17 @@ export function renderTimeline(
|
|
|
4500
4743
|
expandedWidth: number;
|
|
4501
4744
|
};
|
|
4502
4745
|
const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
|
|
4503
|
-
const pillW =
|
|
4746
|
+
const pillW =
|
|
4747
|
+
measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4504
4748
|
// Expanded: pill + icon (unless viewMode) + entries
|
|
4505
4749
|
const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
|
|
4506
4750
|
let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
|
|
4507
4751
|
for (const entry of g.entries) {
|
|
4508
4752
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4509
|
-
entryX =
|
|
4753
|
+
entryX =
|
|
4754
|
+
textX +
|
|
4755
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
4756
|
+
LG_ENTRY_TRAIL;
|
|
4510
4757
|
}
|
|
4511
4758
|
return {
|
|
4512
4759
|
group: g,
|
|
@@ -4526,7 +4773,8 @@ export function renderTimeline(
|
|
|
4526
4773
|
y: number,
|
|
4527
4774
|
isSwimActive: boolean
|
|
4528
4775
|
) {
|
|
4529
|
-
const iconG = parent
|
|
4776
|
+
const iconG = parent
|
|
4777
|
+
.append('g')
|
|
4530
4778
|
.attr('class', 'tl-swimlane-icon')
|
|
4531
4779
|
.attr('transform', `translate(${x}, ${y})`)
|
|
4532
4780
|
.style('cursor', 'pointer');
|
|
@@ -4539,7 +4787,8 @@ export function renderTimeline(
|
|
|
4539
4787
|
{ y: 8, w: 6 },
|
|
4540
4788
|
];
|
|
4541
4789
|
for (const bar of bars) {
|
|
4542
|
-
iconG
|
|
4790
|
+
iconG
|
|
4791
|
+
.append('rect')
|
|
4543
4792
|
.attr('x', 0)
|
|
4544
4793
|
.attr('y', bar.y)
|
|
4545
4794
|
.attr('width', bar.w)
|
|
@@ -4554,8 +4803,16 @@ export function renderTimeline(
|
|
|
4554
4803
|
/** Full re-render with updated swimlane state */
|
|
4555
4804
|
function relayout() {
|
|
4556
4805
|
renderTimeline(
|
|
4557
|
-
container,
|
|
4558
|
-
|
|
4806
|
+
container,
|
|
4807
|
+
parsed,
|
|
4808
|
+
palette,
|
|
4809
|
+
isDark,
|
|
4810
|
+
onClickItem,
|
|
4811
|
+
exportDims,
|
|
4812
|
+
currentActiveGroup,
|
|
4813
|
+
currentSwimlaneGroup,
|
|
4814
|
+
onTagStateChange,
|
|
4815
|
+
viewMode
|
|
4559
4816
|
);
|
|
4560
4817
|
}
|
|
4561
4818
|
|
|
@@ -4565,7 +4822,8 @@ export function renderTimeline(
|
|
|
4565
4822
|
mainSvg.selectAll('.tl-tag-legend-container').remove();
|
|
4566
4823
|
|
|
4567
4824
|
// Effective color source: explicit color group > swimlane group
|
|
4568
|
-
const effectiveColorKey =
|
|
4825
|
+
const effectiveColorKey =
|
|
4826
|
+
(currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
|
|
4569
4827
|
|
|
4570
4828
|
// In view mode, only show the color-driving tag group (expanded, non-interactive).
|
|
4571
4829
|
// Skip the swimlane group if it's separate from the color group (lane headers already label it).
|
|
@@ -4580,32 +4838,43 @@ export function renderTimeline(
|
|
|
4580
4838
|
if (visibleGroups.length === 0) return;
|
|
4581
4839
|
|
|
4582
4840
|
// Compute total width and center horizontally in SVG
|
|
4583
|
-
const totalW =
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4841
|
+
const totalW =
|
|
4842
|
+
visibleGroups.reduce((s, lg) => {
|
|
4843
|
+
const isActive =
|
|
4844
|
+
viewMode ||
|
|
4845
|
+
(currentActiveGroup != null &&
|
|
4846
|
+
lg.group.name.toLowerCase() ===
|
|
4847
|
+
currentActiveGroup.toLowerCase());
|
|
4848
|
+
return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
|
|
4849
|
+
}, 0) +
|
|
4850
|
+
(visibleGroups.length - 1) * LG_GROUP_GAP;
|
|
4589
4851
|
|
|
4590
4852
|
let cx = (width - totalW) / 2;
|
|
4591
4853
|
|
|
4592
4854
|
// Legend container for data-legend-active attribute
|
|
4593
|
-
const legendContainer = mainSvg
|
|
4855
|
+
const legendContainer = mainSvg
|
|
4856
|
+
.append('g')
|
|
4594
4857
|
.attr('class', 'tl-tag-legend-container');
|
|
4595
4858
|
if (currentActiveGroup) {
|
|
4596
|
-
legendContainer.attr(
|
|
4859
|
+
legendContainer.attr(
|
|
4860
|
+
'data-legend-active',
|
|
4861
|
+
currentActiveGroup.toLowerCase()
|
|
4862
|
+
);
|
|
4597
4863
|
}
|
|
4598
4864
|
|
|
4599
4865
|
for (const lg of visibleGroups) {
|
|
4600
4866
|
const groupKey = lg.group.name.toLowerCase();
|
|
4601
|
-
const isActive =
|
|
4867
|
+
const isActive =
|
|
4868
|
+
viewMode ||
|
|
4602
4869
|
(currentActiveGroup != null &&
|
|
4603
4870
|
currentActiveGroup.toLowerCase() === groupKey);
|
|
4604
|
-
const isSwimActive =
|
|
4871
|
+
const isSwimActive =
|
|
4872
|
+
currentSwimlaneGroup != null &&
|
|
4605
4873
|
currentSwimlaneGroup.toLowerCase() === groupKey;
|
|
4606
4874
|
|
|
4607
4875
|
const pillLabel = lg.group.name;
|
|
4608
|
-
const pillWidth =
|
|
4876
|
+
const pillWidth =
|
|
4877
|
+
measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4609
4878
|
|
|
4610
4879
|
const gEl = legendContainer
|
|
4611
4880
|
.append('g')
|
|
@@ -4616,19 +4885,19 @@ export function renderTimeline(
|
|
|
4616
4885
|
.attr('data-legend-entry', '__group__');
|
|
4617
4886
|
|
|
4618
4887
|
if (!viewMode) {
|
|
4619
|
-
gEl
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
});
|
|
4888
|
+
gEl.style('cursor', 'pointer').on('click', () => {
|
|
4889
|
+
currentActiveGroup =
|
|
4890
|
+
currentActiveGroup === groupKey ? null : groupKey;
|
|
4891
|
+
drawLegend();
|
|
4892
|
+
recolorEvents();
|
|
4893
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4894
|
+
});
|
|
4627
4895
|
}
|
|
4628
4896
|
|
|
4629
4897
|
// Outer capsule background (active only)
|
|
4630
4898
|
if (isActive) {
|
|
4631
|
-
gEl
|
|
4899
|
+
gEl
|
|
4900
|
+
.append('rect')
|
|
4632
4901
|
.attr('width', lg.expandedWidth)
|
|
4633
4902
|
.attr('height', LG_HEIGHT)
|
|
4634
4903
|
.attr('rx', LG_HEIGHT / 2)
|
|
@@ -4640,7 +4909,8 @@ export function renderTimeline(
|
|
|
4640
4909
|
const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
|
|
4641
4910
|
|
|
4642
4911
|
// Pill background
|
|
4643
|
-
gEl
|
|
4912
|
+
gEl
|
|
4913
|
+
.append('rect')
|
|
4644
4914
|
.attr('x', pillXOff)
|
|
4645
4915
|
.attr('y', pillYOff)
|
|
4646
4916
|
.attr('width', pillWidth)
|
|
@@ -4650,7 +4920,8 @@ export function renderTimeline(
|
|
|
4650
4920
|
|
|
4651
4921
|
// Active pill border
|
|
4652
4922
|
if (isActive) {
|
|
4653
|
-
gEl
|
|
4923
|
+
gEl
|
|
4924
|
+
.append('rect')
|
|
4654
4925
|
.attr('x', pillXOff)
|
|
4655
4926
|
.attr('y', pillYOff)
|
|
4656
4927
|
.attr('width', pillWidth)
|
|
@@ -4662,7 +4933,8 @@ export function renderTimeline(
|
|
|
4662
4933
|
}
|
|
4663
4934
|
|
|
4664
4935
|
// Pill text
|
|
4665
|
-
gEl
|
|
4936
|
+
gEl
|
|
4937
|
+
.append('text')
|
|
4666
4938
|
.attr('x', pillXOff + pillWidth / 2)
|
|
4667
4939
|
.attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
|
|
4668
4940
|
.attr('font-size', LG_PILL_FONT_SIZE)
|
|
@@ -4684,7 +4956,8 @@ export function renderTimeline(
|
|
|
4684
4956
|
.attr('data-swimlane-toggle', groupKey)
|
|
4685
4957
|
.on('click', (event: MouseEvent) => {
|
|
4686
4958
|
event.stopPropagation();
|
|
4687
|
-
currentSwimlaneGroup =
|
|
4959
|
+
currentSwimlaneGroup =
|
|
4960
|
+
currentSwimlaneGroup === groupKey ? null : groupKey;
|
|
4688
4961
|
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4689
4962
|
relayout();
|
|
4690
4963
|
});
|
|
@@ -4697,7 +4970,8 @@ export function renderTimeline(
|
|
|
4697
4970
|
const tagKey = lg.group.name.toLowerCase();
|
|
4698
4971
|
const tagVal = entry.value.toLowerCase();
|
|
4699
4972
|
|
|
4700
|
-
const entryG = gEl
|
|
4973
|
+
const entryG = gEl
|
|
4974
|
+
.append('g')
|
|
4701
4975
|
.attr('class', 'tl-tag-legend-entry')
|
|
4702
4976
|
.attr('data-tag-group', tagKey)
|
|
4703
4977
|
.attr('data-legend-entry', tagVal);
|
|
@@ -4708,18 +4982,24 @@ export function renderTimeline(
|
|
|
4708
4982
|
.on('mouseenter', (event: MouseEvent) => {
|
|
4709
4983
|
event.stopPropagation();
|
|
4710
4984
|
fadeToTagValue(mainG, tagKey, tagVal);
|
|
4711
|
-
mainSvg
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4985
|
+
mainSvg
|
|
4986
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
4987
|
+
.each(function () {
|
|
4988
|
+
const el = d3Selection.select(this);
|
|
4989
|
+
const ev = el.attr('data-legend-entry');
|
|
4990
|
+
if (ev === '__group__') return;
|
|
4991
|
+
const eg = el.attr('data-tag-group');
|
|
4992
|
+
el.attr(
|
|
4993
|
+
'opacity',
|
|
4994
|
+
eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
|
|
4995
|
+
);
|
|
4996
|
+
});
|
|
4718
4997
|
})
|
|
4719
4998
|
.on('mouseleave', (event: MouseEvent) => {
|
|
4720
4999
|
event.stopPropagation();
|
|
4721
5000
|
fadeReset(mainG);
|
|
4722
|
-
mainSvg
|
|
5001
|
+
mainSvg
|
|
5002
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
4723
5003
|
.attr('opacity', 1);
|
|
4724
5004
|
})
|
|
4725
5005
|
.on('click', (event: MouseEvent) => {
|
|
@@ -4727,14 +5007,16 @@ export function renderTimeline(
|
|
|
4727
5007
|
});
|
|
4728
5008
|
}
|
|
4729
5009
|
|
|
4730
|
-
entryG
|
|
5010
|
+
entryG
|
|
5011
|
+
.append('circle')
|
|
4731
5012
|
.attr('cx', entryX + LG_DOT_R)
|
|
4732
5013
|
.attr('cy', LG_HEIGHT / 2)
|
|
4733
5014
|
.attr('r', LG_DOT_R)
|
|
4734
5015
|
.attr('fill', entry.color);
|
|
4735
5016
|
|
|
4736
5017
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4737
|
-
entryG
|
|
5018
|
+
entryG
|
|
5019
|
+
.append('text')
|
|
4738
5020
|
.attr('x', textX)
|
|
4739
5021
|
.attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
|
|
4740
5022
|
.attr('font-size', LG_ENTRY_FONT_SIZE)
|
|
@@ -4742,7 +5024,10 @@ export function renderTimeline(
|
|
|
4742
5024
|
.attr('fill', palette.textMuted)
|
|
4743
5025
|
.text(entry.value);
|
|
4744
5026
|
|
|
4745
|
-
entryX =
|
|
5027
|
+
entryX =
|
|
5028
|
+
textX +
|
|
5029
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
5030
|
+
LG_ENTRY_TRAIL;
|
|
4746
5031
|
}
|
|
4747
5032
|
}
|
|
4748
5033
|
|
|
@@ -4767,16 +5052,27 @@ export function renderTimeline(
|
|
|
4767
5052
|
let color: string;
|
|
4768
5053
|
if (colorTG) {
|
|
4769
5054
|
const tagColor = resolveTagColor(
|
|
4770
|
-
ev.metadata,
|
|
5055
|
+
ev.metadata,
|
|
5056
|
+
parsed.timelineTagGroups,
|
|
5057
|
+
colorTG
|
|
4771
5058
|
);
|
|
4772
|
-
color =
|
|
4773
|
-
|
|
5059
|
+
color =
|
|
5060
|
+
tagColor ??
|
|
5061
|
+
(ev.group && groupColorMap.has(ev.group)
|
|
5062
|
+
? groupColorMap.get(ev.group)!
|
|
5063
|
+
: textColor);
|
|
4774
5064
|
} else {
|
|
4775
|
-
color =
|
|
4776
|
-
|
|
5065
|
+
color =
|
|
5066
|
+
ev.group && groupColorMap.has(ev.group)
|
|
5067
|
+
? groupColorMap.get(ev.group)!
|
|
5068
|
+
: textColor;
|
|
4777
5069
|
}
|
|
4778
|
-
el.selectAll('rect')
|
|
4779
|
-
|
|
5070
|
+
el.selectAll('rect')
|
|
5071
|
+
.attr('fill', mix(color, bg, 30))
|
|
5072
|
+
.attr('stroke', color);
|
|
5073
|
+
el.selectAll('circle:not(.tl-event-point-outline)')
|
|
5074
|
+
.attr('fill', mix(color, bg, 30))
|
|
5075
|
+
.attr('stroke', color);
|
|
4780
5076
|
});
|
|
4781
5077
|
}
|
|
4782
5078
|
|
|
@@ -4833,7 +5129,14 @@ export function renderWordCloud(
|
|
|
4833
5129
|
|
|
4834
5130
|
const rotateFn = getRotateFn(cloudOptions.rotate);
|
|
4835
5131
|
|
|
4836
|
-
renderChartTitle(
|
|
5132
|
+
renderChartTitle(
|
|
5133
|
+
svg,
|
|
5134
|
+
title,
|
|
5135
|
+
parsed.titleLineNumber,
|
|
5136
|
+
width,
|
|
5137
|
+
textColor,
|
|
5138
|
+
onClickItem
|
|
5139
|
+
);
|
|
4837
5140
|
|
|
4838
5141
|
const g = svg
|
|
4839
5142
|
.append('g')
|
|
@@ -5140,7 +5443,8 @@ export function renderVenn(
|
|
|
5140
5443
|
const labelTextPad = 4;
|
|
5141
5444
|
|
|
5142
5445
|
for (let i = 0; i < n; i++) {
|
|
5143
|
-
const estimatedWidth =
|
|
5446
|
+
const estimatedWidth =
|
|
5447
|
+
vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
|
|
5144
5448
|
const dx = rawCircles[i].x - clusterCx;
|
|
5145
5449
|
const dy = rawCircles[i].y - clusterCy;
|
|
5146
5450
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
@@ -5167,13 +5471,27 @@ export function renderVenn(
|
|
|
5167
5471
|
const scaledR = circles[0].r;
|
|
5168
5472
|
|
|
5169
5473
|
// Suppress WebKit focus ring on interactive SVG elements
|
|
5170
|
-
svg
|
|
5474
|
+
svg
|
|
5475
|
+
.append('style')
|
|
5476
|
+
.text('circle:focus, circle:focus-visible { outline: none !important; }');
|
|
5171
5477
|
|
|
5172
5478
|
// Title
|
|
5173
|
-
renderChartTitle(
|
|
5479
|
+
renderChartTitle(
|
|
5480
|
+
svg,
|
|
5481
|
+
title,
|
|
5482
|
+
parsed.titleLineNumber,
|
|
5483
|
+
width,
|
|
5484
|
+
textColor,
|
|
5485
|
+
onClickItem
|
|
5486
|
+
);
|
|
5174
5487
|
|
|
5175
5488
|
// ── Semi-transparent filled circles (non-interactive) ──
|
|
5176
|
-
const circleEls: d3Selection.Selection<
|
|
5489
|
+
const circleEls: d3Selection.Selection<
|
|
5490
|
+
SVGCircleElement,
|
|
5491
|
+
unknown,
|
|
5492
|
+
null,
|
|
5493
|
+
undefined
|
|
5494
|
+
>[] = [];
|
|
5177
5495
|
const circleGroup = svg.append('g');
|
|
5178
5496
|
circles.forEach((c, i) => {
|
|
5179
5497
|
const el = circleGroup
|
|
@@ -5200,10 +5518,13 @@ export function renderVenn(
|
|
|
5200
5518
|
|
|
5201
5519
|
// Individual circle clipPaths
|
|
5202
5520
|
circles.forEach((c, i) => {
|
|
5203
|
-
defs
|
|
5521
|
+
defs
|
|
5522
|
+
.append('clipPath')
|
|
5204
5523
|
.attr('id', `vcp-${i}`)
|
|
5205
5524
|
.append('circle')
|
|
5206
|
-
.attr('cx', c.x)
|
|
5525
|
+
.attr('cx', c.x)
|
|
5526
|
+
.attr('cy', c.y)
|
|
5527
|
+
.attr('r', c.r);
|
|
5207
5528
|
});
|
|
5208
5529
|
|
|
5209
5530
|
// All region index-sets: exclusive then intersection subsets
|
|
@@ -5215,57 +5536,79 @@ export function renderVenn(
|
|
|
5215
5536
|
}
|
|
5216
5537
|
|
|
5217
5538
|
const overlayGroup = svg.append('g').style('pointer-events', 'none');
|
|
5218
|
-
const overlayEls = new Map<
|
|
5539
|
+
const overlayEls = new Map<
|
|
5540
|
+
string,
|
|
5541
|
+
d3Selection.Selection<SVGRectElement, unknown, null, undefined>
|
|
5542
|
+
>();
|
|
5219
5543
|
|
|
5220
5544
|
for (const idxs of regionIdxSets) {
|
|
5221
5545
|
const key = idxs.join('-');
|
|
5222
|
-
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5546
|
+
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5547
|
+
(j) => !idxs.includes(j)
|
|
5548
|
+
);
|
|
5223
5549
|
|
|
5224
5550
|
// Build nested clipPath for intersection of all idxs
|
|
5225
5551
|
let clipId = `vcp-${idxs[0]}`;
|
|
5226
5552
|
for (let k = 1; k < idxs.length; k++) {
|
|
5227
5553
|
const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
|
|
5228
5554
|
const ci = idxs[k];
|
|
5229
|
-
defs
|
|
5555
|
+
defs
|
|
5556
|
+
.append('clipPath')
|
|
5230
5557
|
.attr('id', nestedId)
|
|
5231
5558
|
.append('circle')
|
|
5232
|
-
.attr('cx', circles[ci].x)
|
|
5559
|
+
.attr('cx', circles[ci].x)
|
|
5560
|
+
.attr('cy', circles[ci].y)
|
|
5561
|
+
.attr('r', circles[ci].r)
|
|
5233
5562
|
.attr('clip-path', `url(#${clipId})`);
|
|
5234
5563
|
clipId = nestedId;
|
|
5235
5564
|
}
|
|
5236
5565
|
|
|
5237
5566
|
// Determine line number for this region (for editor sync)
|
|
5238
|
-
let regionLineNumber: number | null = null;
|
|
5567
|
+
let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
|
|
5239
5568
|
if (idxs.length === 1) {
|
|
5240
5569
|
regionLineNumber = vennSets[idxs[0]].lineNumber;
|
|
5241
5570
|
} else {
|
|
5242
|
-
const sortedNames = idxs.map(i => vennSets[i].name).sort();
|
|
5571
|
+
const sortedNames = idxs.map((i) => vennSets[i].name).sort();
|
|
5243
5572
|
const ov = vennOverlaps.find(
|
|
5244
|
-
(o) =>
|
|
5573
|
+
(o) =>
|
|
5574
|
+
o.sets.length === sortedNames.length &&
|
|
5575
|
+
o.sets.every((s, k) => s === sortedNames[k])
|
|
5245
5576
|
);
|
|
5246
5577
|
regionLineNumber = ov?.lineNumber ?? null;
|
|
5247
5578
|
}
|
|
5248
5579
|
|
|
5249
|
-
const el = overlayGroup
|
|
5250
|
-
.
|
|
5251
|
-
.attr('
|
|
5580
|
+
const el = overlayGroup
|
|
5581
|
+
.append('rect')
|
|
5582
|
+
.attr('x', 0)
|
|
5583
|
+
.attr('y', 0)
|
|
5584
|
+
.attr('width', width)
|
|
5585
|
+
.attr('height', height)
|
|
5252
5586
|
.attr('fill', 'white')
|
|
5253
5587
|
.attr('fill-opacity', 0)
|
|
5254
5588
|
.attr('class', 'venn-region-overlay')
|
|
5255
|
-
.attr(
|
|
5589
|
+
.attr(
|
|
5590
|
+
'data-line-number',
|
|
5591
|
+
regionLineNumber != null ? String(regionLineNumber) : '0'
|
|
5592
|
+
)
|
|
5256
5593
|
.attr('clip-path', `url(#${clipId})`);
|
|
5257
5594
|
|
|
5258
5595
|
if (excluded.length > 0) {
|
|
5259
5596
|
// Mask subtracts excluded circles so only the exact region shape highlights
|
|
5260
5597
|
const maskId = `vvm-${key}`;
|
|
5261
5598
|
const mask = defs.append('mask').attr('id', maskId);
|
|
5262
|
-
mask
|
|
5263
|
-
.
|
|
5264
|
-
.attr('
|
|
5599
|
+
mask
|
|
5600
|
+
.append('rect')
|
|
5601
|
+
.attr('x', 0)
|
|
5602
|
+
.attr('y', 0)
|
|
5603
|
+
.attr('width', width)
|
|
5604
|
+
.attr('height', height)
|
|
5265
5605
|
.attr('fill', 'white');
|
|
5266
5606
|
for (const j of excluded) {
|
|
5267
|
-
mask
|
|
5268
|
-
.
|
|
5607
|
+
mask
|
|
5608
|
+
.append('circle')
|
|
5609
|
+
.attr('cx', circles[j].x)
|
|
5610
|
+
.attr('cy', circles[j].y)
|
|
5611
|
+
.attr('r', circles[j].r)
|
|
5269
5612
|
.attr('fill', 'black');
|
|
5270
5613
|
}
|
|
5271
5614
|
el.attr('mask', `url(#${maskId})`);
|
|
@@ -5276,10 +5619,12 @@ export function renderVenn(
|
|
|
5276
5619
|
|
|
5277
5620
|
const showRegionOverlay = (idxs: number[]) => {
|
|
5278
5621
|
const key = [...idxs].sort((a, b) => a - b).join('-');
|
|
5279
|
-
overlayEls.forEach((el, k) =>
|
|
5622
|
+
overlayEls.forEach((el, k) =>
|
|
5623
|
+
el.attr('fill-opacity', k === key ? 0 : 0.55)
|
|
5624
|
+
);
|
|
5280
5625
|
};
|
|
5281
5626
|
const hideAllOverlays = () => {
|
|
5282
|
-
overlayEls.forEach(el => el.attr('fill-opacity', 0));
|
|
5627
|
+
overlayEls.forEach((el) => el.attr('fill-opacity', 0));
|
|
5283
5628
|
};
|
|
5284
5629
|
|
|
5285
5630
|
// ── Labels ──
|
|
@@ -5288,7 +5633,9 @@ export function renderVenn(
|
|
|
5288
5633
|
|
|
5289
5634
|
function exclusiveHSpan(px: number, py: number, ci: number): number {
|
|
5290
5635
|
const dy = py - circles[ci].y;
|
|
5291
|
-
const halfChord = Math.sqrt(
|
|
5636
|
+
const halfChord = Math.sqrt(
|
|
5637
|
+
Math.max(0, circles[ci].r * circles[ci].r - dy * dy)
|
|
5638
|
+
);
|
|
5292
5639
|
let left = circles[ci].x - halfChord;
|
|
5293
5640
|
let right = circles[ci].x + halfChord;
|
|
5294
5641
|
for (let j = 0; j < n; j++) {
|
|
@@ -5319,11 +5666,14 @@ export function renderVenn(
|
|
|
5319
5666
|
const centroid = regionCentroid(circles, inside);
|
|
5320
5667
|
|
|
5321
5668
|
const availW = exclusiveHSpan(centroid.x, centroid.y, i);
|
|
5322
|
-
const fitFont = Math.min(
|
|
5323
|
-
|
|
5669
|
+
const fitFont = Math.min(
|
|
5670
|
+
MAX_FONT,
|
|
5671
|
+
Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO))
|
|
5672
|
+
);
|
|
5324
5673
|
const estTextW = text.length * CH_RATIO * fitFont;
|
|
5325
5674
|
|
|
5326
|
-
const fitsInside =
|
|
5675
|
+
const fitsInside =
|
|
5676
|
+
estTextW + INTERNAL_PAD * 2 < availW &&
|
|
5327
5677
|
pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
|
|
5328
5678
|
pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
|
|
5329
5679
|
|
|
@@ -5342,7 +5692,13 @@ export function renderVenn(
|
|
|
5342
5692
|
let dx = c.x - gcx;
|
|
5343
5693
|
let dy = c.y - gcy;
|
|
5344
5694
|
const mag = Math.sqrt(dx * dx + dy * dy);
|
|
5345
|
-
if (mag < 1e-6) {
|
|
5695
|
+
if (mag < 1e-6) {
|
|
5696
|
+
dx = 1;
|
|
5697
|
+
dy = 0;
|
|
5698
|
+
} else {
|
|
5699
|
+
dx /= mag;
|
|
5700
|
+
dy /= mag;
|
|
5701
|
+
}
|
|
5346
5702
|
|
|
5347
5703
|
const exitX = c.x + dx * c.r;
|
|
5348
5704
|
const exitY = c.y + dy * c.r;
|
|
@@ -5353,8 +5709,10 @@ export function renderVenn(
|
|
|
5353
5709
|
|
|
5354
5710
|
labelGroup
|
|
5355
5711
|
.append('line')
|
|
5356
|
-
.attr('x1', edgeX)
|
|
5357
|
-
.attr('
|
|
5712
|
+
.attr('x1', edgeX)
|
|
5713
|
+
.attr('y1', edgeY)
|
|
5714
|
+
.attr('x2', stubEndX)
|
|
5715
|
+
.attr('y2', stubEndY)
|
|
5358
5716
|
.attr('stroke', textColor)
|
|
5359
5717
|
.attr('stroke-width', 1);
|
|
5360
5718
|
|
|
@@ -5381,7 +5739,8 @@ export function renderVenn(
|
|
|
5381
5739
|
|
|
5382
5740
|
// ── Overlap labels (inline at region centroid) ──
|
|
5383
5741
|
function overlapHSpan(py: number, idxs: number[]): number {
|
|
5384
|
-
let left = -Infinity,
|
|
5742
|
+
let left = -Infinity,
|
|
5743
|
+
right = Infinity;
|
|
5385
5744
|
for (const ci of idxs) {
|
|
5386
5745
|
const dy = py - circles[ci].y;
|
|
5387
5746
|
if (Math.abs(dy) >= circles[ci].r) return 0;
|
|
@@ -5411,8 +5770,13 @@ export function renderVenn(
|
|
|
5411
5770
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5412
5771
|
const centroid = regionCentroid(circles, inside);
|
|
5413
5772
|
const availW = overlapHSpan(centroid.y, idxs);
|
|
5414
|
-
const fitFont = Math.min(
|
|
5415
|
-
|
|
5773
|
+
const fitFont = Math.min(
|
|
5774
|
+
MAX_FONT,
|
|
5775
|
+
Math.max(
|
|
5776
|
+
MIN_FONT,
|
|
5777
|
+
(availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
|
|
5778
|
+
)
|
|
5779
|
+
);
|
|
5416
5780
|
labelGroup
|
|
5417
5781
|
.append('text')
|
|
5418
5782
|
.attr('x', centroid.x)
|
|
@@ -5441,11 +5805,16 @@ export function renderVenn(
|
|
|
5441
5805
|
.attr('data-line-number', String(vennSets[i].lineNumber))
|
|
5442
5806
|
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
5443
5807
|
.style('outline', 'none')
|
|
5444
|
-
.on('mouseenter', () => {
|
|
5445
|
-
|
|
5808
|
+
.on('mouseenter', () => {
|
|
5809
|
+
showRegionOverlay([i]);
|
|
5810
|
+
})
|
|
5811
|
+
.on('mouseleave', () => {
|
|
5812
|
+
hideAllOverlays();
|
|
5813
|
+
})
|
|
5446
5814
|
.on('click', function () {
|
|
5447
5815
|
(this as SVGElement).blur?.();
|
|
5448
|
-
if (onClickItem && vennSets[i].lineNumber)
|
|
5816
|
+
if (onClickItem && vennSets[i].lineNumber)
|
|
5817
|
+
onClickItem(vennSets[i].lineNumber);
|
|
5449
5818
|
});
|
|
5450
5819
|
});
|
|
5451
5820
|
|
|
@@ -5454,14 +5823,23 @@ export function renderVenn(
|
|
|
5454
5823
|
|
|
5455
5824
|
const subsets: { idxs: number[]; sets: string[] }[] = [];
|
|
5456
5825
|
if (n === 2) {
|
|
5457
|
-
subsets.push({
|
|
5826
|
+
subsets.push({
|
|
5827
|
+
idxs: [0, 1],
|
|
5828
|
+
sets: [vennSets[0].name, vennSets[1].name].sort(),
|
|
5829
|
+
});
|
|
5458
5830
|
} else {
|
|
5459
5831
|
for (let a = 0; a < n; a++) {
|
|
5460
5832
|
for (let b = a + 1; b < n; b++) {
|
|
5461
|
-
subsets.push({
|
|
5833
|
+
subsets.push({
|
|
5834
|
+
idxs: [a, b],
|
|
5835
|
+
sets: [vennSets[a].name, vennSets[b].name].sort(),
|
|
5836
|
+
});
|
|
5462
5837
|
}
|
|
5463
5838
|
}
|
|
5464
|
-
subsets.push({
|
|
5839
|
+
subsets.push({
|
|
5840
|
+
idxs: [0, 1, 2],
|
|
5841
|
+
sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort(),
|
|
5842
|
+
});
|
|
5465
5843
|
}
|
|
5466
5844
|
|
|
5467
5845
|
for (const subset of subsets) {
|
|
@@ -5469,7 +5847,8 @@ export function renderVenn(
|
|
|
5469
5847
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5470
5848
|
const centroid = regionCentroid(circles, inside);
|
|
5471
5849
|
const declaredOv = vennOverlaps.find(
|
|
5472
|
-
(ov) =>
|
|
5850
|
+
(ov) =>
|
|
5851
|
+
ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
|
|
5473
5852
|
);
|
|
5474
5853
|
hoverGroup
|
|
5475
5854
|
.append('circle')
|
|
@@ -5482,8 +5861,12 @@ export function renderVenn(
|
|
|
5482
5861
|
.attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
|
|
5483
5862
|
.style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
|
|
5484
5863
|
.style('outline', 'none')
|
|
5485
|
-
.on('mouseenter', () => {
|
|
5486
|
-
|
|
5864
|
+
.on('mouseenter', () => {
|
|
5865
|
+
showRegionOverlay(idxs);
|
|
5866
|
+
})
|
|
5867
|
+
.on('mouseleave', () => {
|
|
5868
|
+
hideAllOverlays();
|
|
5869
|
+
})
|
|
5487
5870
|
.on('click', function () {
|
|
5488
5871
|
(this as SVGElement).blur?.();
|
|
5489
5872
|
if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
|
|
@@ -5542,7 +5925,12 @@ export function renderQuadrant(
|
|
|
5542
5925
|
// Margins
|
|
5543
5926
|
const hasXAxis = !!quadrantXAxis;
|
|
5544
5927
|
const hasYAxis = !!quadrantYAxis;
|
|
5545
|
-
const margin = {
|
|
5928
|
+
const margin = {
|
|
5929
|
+
top: title ? 60 : 30,
|
|
5930
|
+
right: 30,
|
|
5931
|
+
bottom: hasXAxis ? 70 : 40,
|
|
5932
|
+
left: hasYAxis ? 80 : 40,
|
|
5933
|
+
};
|
|
5546
5934
|
const chartWidth = width - margin.left - margin.right;
|
|
5547
5935
|
const chartHeight = height - margin.top - margin.bottom;
|
|
5548
5936
|
|
|
@@ -5554,7 +5942,14 @@ export function renderQuadrant(
|
|
|
5554
5942
|
const tooltip = createTooltip(container, palette, isDark);
|
|
5555
5943
|
|
|
5556
5944
|
// Title
|
|
5557
|
-
renderChartTitle(
|
|
5945
|
+
renderChartTitle(
|
|
5946
|
+
svg,
|
|
5947
|
+
title,
|
|
5948
|
+
quadrantTitleLineNumber,
|
|
5949
|
+
width,
|
|
5950
|
+
textColor,
|
|
5951
|
+
onClickItem
|
|
5952
|
+
);
|
|
5558
5953
|
|
|
5559
5954
|
// Chart group (translated by margins)
|
|
5560
5955
|
const chartG = svg
|
|
@@ -5565,12 +5960,21 @@ export function renderQuadrant(
|
|
|
5565
5960
|
const mixHex = (a: string, b: string, pct: number): string => {
|
|
5566
5961
|
const parse = (h: string) => {
|
|
5567
5962
|
const r = h.replace('#', '');
|
|
5568
|
-
const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
|
|
5569
|
-
return [
|
|
5963
|
+
const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
|
|
5964
|
+
return [
|
|
5965
|
+
parseInt(f.substring(0, 2), 16),
|
|
5966
|
+
parseInt(f.substring(2, 4), 16),
|
|
5967
|
+
parseInt(f.substring(4, 6), 16),
|
|
5968
|
+
];
|
|
5570
5969
|
};
|
|
5571
|
-
const [ar,ag,ab] = parse(a),
|
|
5572
|
-
|
|
5573
|
-
|
|
5970
|
+
const [ar, ag, ab] = parse(a),
|
|
5971
|
+
[br, bg, bb] = parse(b),
|
|
5972
|
+
t = pct / 100;
|
|
5973
|
+
const c = (x: number, y: number) =>
|
|
5974
|
+
Math.round(x * t + y * (1 - t))
|
|
5975
|
+
.toString(16)
|
|
5976
|
+
.padStart(2, '0');
|
|
5977
|
+
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
5574
5978
|
};
|
|
5575
5979
|
|
|
5576
5980
|
const bg = isDark ? palette.surface : palette.bg;
|
|
@@ -5687,7 +6091,11 @@ export function renderQuadrant(
|
|
|
5687
6091
|
fontSize: number;
|
|
5688
6092
|
}
|
|
5689
6093
|
|
|
5690
|
-
const quadrantLabelLayout = (
|
|
6094
|
+
const quadrantLabelLayout = (
|
|
6095
|
+
text: string,
|
|
6096
|
+
qw: number,
|
|
6097
|
+
qh: number
|
|
6098
|
+
): QuadrantLabelLayout => {
|
|
5691
6099
|
const availW = qw - LABEL_PAD;
|
|
5692
6100
|
const availH = qh - LABEL_PAD;
|
|
5693
6101
|
const words = text.split(/\s+/);
|
|
@@ -5695,7 +6103,10 @@ export function renderQuadrant(
|
|
|
5695
6103
|
// Try single line first
|
|
5696
6104
|
if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
|
|
5697
6105
|
const fs = Math.min(LABEL_MAX_FONT, availH);
|
|
5698
|
-
return {
|
|
6106
|
+
return {
|
|
6107
|
+
lines: [text],
|
|
6108
|
+
fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
|
|
6109
|
+
};
|
|
5699
6110
|
}
|
|
5700
6111
|
|
|
5701
6112
|
// Try wrapping into 2+ lines: greedily pack words so each line fits availW
|
|
@@ -5742,7 +6153,10 @@ export function renderQuadrant(
|
|
|
5742
6153
|
const qh = chartHeight / 2;
|
|
5743
6154
|
const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
|
|
5744
6155
|
const labelLayouts = new Map(
|
|
5745
|
-
quadrantDefsWithLabel.map((d) => [
|
|
6156
|
+
quadrantDefsWithLabel.map((d) => [
|
|
6157
|
+
d.label!.text,
|
|
6158
|
+
quadrantLabelLayout(d.label!.text, qw, qh),
|
|
6159
|
+
])
|
|
5746
6160
|
);
|
|
5747
6161
|
|
|
5748
6162
|
const quadrantLabelTexts = chartG
|
|
@@ -5807,7 +6221,10 @@ export function renderQuadrant(
|
|
|
5807
6221
|
.attr('text-anchor', 'middle')
|
|
5808
6222
|
.attr('fill', textColor)
|
|
5809
6223
|
.attr('font-size', '18px')
|
|
5810
|
-
.attr(
|
|
6224
|
+
.attr(
|
|
6225
|
+
'data-line-number',
|
|
6226
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6227
|
+
)
|
|
5811
6228
|
.style(
|
|
5812
6229
|
'cursor',
|
|
5813
6230
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5823,7 +6240,10 @@ export function renderQuadrant(
|
|
|
5823
6240
|
.attr('text-anchor', 'middle')
|
|
5824
6241
|
.attr('fill', textColor)
|
|
5825
6242
|
.attr('font-size', '18px')
|
|
5826
|
-
.attr(
|
|
6243
|
+
.attr(
|
|
6244
|
+
'data-line-number',
|
|
6245
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6246
|
+
)
|
|
5827
6247
|
.style(
|
|
5828
6248
|
'cursor',
|
|
5829
6249
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5859,7 +6279,10 @@ export function renderQuadrant(
|
|
|
5859
6279
|
.attr('fill', textColor)
|
|
5860
6280
|
.attr('font-size', '18px')
|
|
5861
6281
|
.attr('transform', `rotate(-90, 22, ${yMidBottom})`)
|
|
5862
|
-
.attr(
|
|
6282
|
+
.attr(
|
|
6283
|
+
'data-line-number',
|
|
6284
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6285
|
+
)
|
|
5863
6286
|
.style(
|
|
5864
6287
|
'cursor',
|
|
5865
6288
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5876,7 +6299,10 @@ export function renderQuadrant(
|
|
|
5876
6299
|
.attr('fill', textColor)
|
|
5877
6300
|
.attr('font-size', '18px')
|
|
5878
6301
|
.attr('transform', `rotate(-90, 22, ${yMidTop})`)
|
|
5879
|
-
.attr(
|
|
6302
|
+
.attr(
|
|
6303
|
+
'data-line-number',
|
|
6304
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6305
|
+
)
|
|
5880
6306
|
.style(
|
|
5881
6307
|
'cursor',
|
|
5882
6308
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5935,7 +6361,9 @@ export function renderQuadrant(
|
|
|
5935
6361
|
const pointColor =
|
|
5936
6362
|
quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
|
|
5937
6363
|
|
|
5938
|
-
const pointG = pointsG
|
|
6364
|
+
const pointG = pointsG
|
|
6365
|
+
.append('g')
|
|
6366
|
+
.attr('class', 'point-group')
|
|
5939
6367
|
.attr('data-line-number', String(point.lineNumber));
|
|
5940
6368
|
|
|
5941
6369
|
// Circle with white fill and colored border for visibility on opaque quadrants
|
|
@@ -6024,7 +6452,10 @@ const EXPORT_HEIGHT = 800;
|
|
|
6024
6452
|
/**
|
|
6025
6453
|
* Resolves the palette for export, falling back to Nord light/dark.
|
|
6026
6454
|
*/
|
|
6027
|
-
async function resolveExportPalette(
|
|
6455
|
+
async function resolveExportPalette(
|
|
6456
|
+
theme: string,
|
|
6457
|
+
palette?: PaletteColors
|
|
6458
|
+
): Promise<PaletteColors> {
|
|
6028
6459
|
if (palette) return palette;
|
|
6029
6460
|
const { getPalette } = await import('./palettes');
|
|
6030
6461
|
return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
|
|
@@ -6086,7 +6517,13 @@ export async function renderForExport(
|
|
|
6086
6517
|
hiddenAttributes?: Set<string>;
|
|
6087
6518
|
swimlaneTagGroup?: string | null;
|
|
6088
6519
|
},
|
|
6089
|
-
options?: {
|
|
6520
|
+
options?: {
|
|
6521
|
+
branding?: boolean;
|
|
6522
|
+
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
6523
|
+
c4System?: string;
|
|
6524
|
+
c4Container?: string;
|
|
6525
|
+
tagGroup?: string;
|
|
6526
|
+
}
|
|
6090
6527
|
): Promise<string> {
|
|
6091
6528
|
// Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
|
|
6092
6529
|
const { parseDgmoChartType } = await import('./dgmo-router');
|
|
@@ -6106,7 +6543,8 @@ export async function renderForExport(
|
|
|
6106
6543
|
|
|
6107
6544
|
// Apply interactive collapse state when provided
|
|
6108
6545
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6109
|
-
const activeTagGroup =
|
|
6546
|
+
const activeTagGroup =
|
|
6547
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6110
6548
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6111
6549
|
|
|
6112
6550
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6128,7 +6566,17 @@ export async function renderForExport(
|
|
|
6128
6566
|
const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
|
|
6129
6567
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6130
6568
|
|
|
6131
|
-
renderOrg(
|
|
6569
|
+
renderOrg(
|
|
6570
|
+
container,
|
|
6571
|
+
effectiveParsed,
|
|
6572
|
+
orgLayout,
|
|
6573
|
+
effectivePalette,
|
|
6574
|
+
isDark,
|
|
6575
|
+
undefined,
|
|
6576
|
+
{ width: exportWidth, height: exportHeight },
|
|
6577
|
+
activeTagGroup,
|
|
6578
|
+
hiddenAttributes
|
|
6579
|
+
);
|
|
6132
6580
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6133
6581
|
}
|
|
6134
6582
|
|
|
@@ -6146,7 +6594,8 @@ export async function renderForExport(
|
|
|
6146
6594
|
|
|
6147
6595
|
// Apply interactive collapse state when provided
|
|
6148
6596
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6149
|
-
const activeTagGroup =
|
|
6597
|
+
const activeTagGroup =
|
|
6598
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6150
6599
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6151
6600
|
|
|
6152
6601
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6159,7 +6608,7 @@ export async function renderForExport(
|
|
|
6159
6608
|
hiddenCounts.size > 0 ? hiddenCounts : undefined,
|
|
6160
6609
|
activeTagGroup,
|
|
6161
6610
|
hiddenAttributes,
|
|
6162
|
-
true
|
|
6611
|
+
true
|
|
6163
6612
|
);
|
|
6164
6613
|
|
|
6165
6614
|
const PADDING = 20;
|
|
@@ -6168,7 +6617,17 @@ export async function renderForExport(
|
|
|
6168
6617
|
const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
|
|
6169
6618
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6170
6619
|
|
|
6171
|
-
renderSitemap(
|
|
6620
|
+
renderSitemap(
|
|
6621
|
+
container,
|
|
6622
|
+
effectiveParsed,
|
|
6623
|
+
sitemapLayout,
|
|
6624
|
+
effectivePalette,
|
|
6625
|
+
isDark,
|
|
6626
|
+
undefined,
|
|
6627
|
+
{ width: exportWidth, height: exportHeight },
|
|
6628
|
+
activeTagGroup,
|
|
6629
|
+
hiddenAttributes
|
|
6630
|
+
);
|
|
6172
6631
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6173
6632
|
}
|
|
6174
6633
|
|
|
@@ -6186,7 +6645,15 @@ export async function renderForExport(
|
|
|
6186
6645
|
container.style.left = '-9999px';
|
|
6187
6646
|
document.body.appendChild(container);
|
|
6188
6647
|
|
|
6189
|
-
renderKanban(
|
|
6648
|
+
renderKanban(
|
|
6649
|
+
container,
|
|
6650
|
+
kanbanParsed,
|
|
6651
|
+
effectivePalette,
|
|
6652
|
+
theme === 'dark',
|
|
6653
|
+
undefined,
|
|
6654
|
+
undefined,
|
|
6655
|
+
options?.tagGroup
|
|
6656
|
+
);
|
|
6190
6657
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6191
6658
|
}
|
|
6192
6659
|
|
|
@@ -6206,7 +6673,15 @@ export async function renderForExport(
|
|
|
6206
6673
|
const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
|
|
6207
6674
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6208
6675
|
|
|
6209
|
-
renderClassDiagram(
|
|
6676
|
+
renderClassDiagram(
|
|
6677
|
+
container,
|
|
6678
|
+
classParsed,
|
|
6679
|
+
classLayout,
|
|
6680
|
+
effectivePalette,
|
|
6681
|
+
theme === 'dark',
|
|
6682
|
+
undefined,
|
|
6683
|
+
{ width: exportWidth, height: exportHeight }
|
|
6684
|
+
);
|
|
6210
6685
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6211
6686
|
}
|
|
6212
6687
|
|
|
@@ -6226,14 +6701,26 @@ export async function renderForExport(
|
|
|
6226
6701
|
const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
|
|
6227
6702
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6228
6703
|
|
|
6229
|
-
renderERDiagram(
|
|
6704
|
+
renderERDiagram(
|
|
6705
|
+
container,
|
|
6706
|
+
erParsed,
|
|
6707
|
+
erLayout,
|
|
6708
|
+
effectivePalette,
|
|
6709
|
+
theme === 'dark',
|
|
6710
|
+
undefined,
|
|
6711
|
+
{ width: exportWidth, height: exportHeight },
|
|
6712
|
+
options?.tagGroup
|
|
6713
|
+
);
|
|
6230
6714
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6231
6715
|
}
|
|
6232
6716
|
|
|
6233
6717
|
if (detectedType === 'initiative-status') {
|
|
6234
|
-
const { parseInitiativeStatus } =
|
|
6235
|
-
|
|
6236
|
-
const {
|
|
6718
|
+
const { parseInitiativeStatus } =
|
|
6719
|
+
await import('./initiative-status/parser');
|
|
6720
|
+
const { layoutInitiativeStatus } =
|
|
6721
|
+
await import('./initiative-status/layout');
|
|
6722
|
+
const { renderInitiativeStatus } =
|
|
6723
|
+
await import('./initiative-status/renderer');
|
|
6237
6724
|
|
|
6238
6725
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6239
6726
|
const isParsed = parseInitiativeStatus(content);
|
|
@@ -6246,14 +6733,27 @@ export async function renderForExport(
|
|
|
6246
6733
|
const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
|
|
6247
6734
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6248
6735
|
|
|
6249
|
-
renderInitiativeStatus(
|
|
6736
|
+
renderInitiativeStatus(
|
|
6737
|
+
container,
|
|
6738
|
+
isParsed,
|
|
6739
|
+
isLayout,
|
|
6740
|
+
effectivePalette,
|
|
6741
|
+
theme === 'dark',
|
|
6742
|
+
{ exportDims: { width: exportWidth, height: exportHeight } }
|
|
6743
|
+
);
|
|
6250
6744
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6251
6745
|
}
|
|
6252
6746
|
|
|
6253
6747
|
if (detectedType === 'c4') {
|
|
6254
6748
|
const { parseC4 } = await import('./c4/parser');
|
|
6255
|
-
const {
|
|
6256
|
-
|
|
6749
|
+
const {
|
|
6750
|
+
layoutC4Context,
|
|
6751
|
+
layoutC4Containers,
|
|
6752
|
+
layoutC4Components,
|
|
6753
|
+
layoutC4Deployment,
|
|
6754
|
+
} = await import('./c4/layout');
|
|
6755
|
+
const { renderC4Context, renderC4Containers } =
|
|
6756
|
+
await import('./c4/renderer');
|
|
6257
6757
|
|
|
6258
6758
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6259
6759
|
const c4Parsed = parseC4(content, effectivePalette);
|
|
@@ -6264,13 +6764,14 @@ export async function renderForExport(
|
|
|
6264
6764
|
const c4System = options?.c4System;
|
|
6265
6765
|
const c4Container = options?.c4Container;
|
|
6266
6766
|
|
|
6267
|
-
const c4Layout =
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6767
|
+
const c4Layout =
|
|
6768
|
+
c4Level === 'deployment'
|
|
6769
|
+
? layoutC4Deployment(c4Parsed)
|
|
6770
|
+
: c4Level === 'components' && c4System && c4Container
|
|
6771
|
+
? layoutC4Components(c4Parsed, c4System, c4Container)
|
|
6772
|
+
: c4Level === 'containers' && c4System
|
|
6773
|
+
? layoutC4Containers(c4Parsed, c4System)
|
|
6774
|
+
: layoutC4Context(c4Parsed);
|
|
6274
6775
|
|
|
6275
6776
|
if (c4Layout.nodes.length === 0) return '';
|
|
6276
6777
|
|
|
@@ -6280,11 +6781,23 @@ export async function renderForExport(
|
|
|
6280
6781
|
const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
|
|
6281
6782
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6282
6783
|
|
|
6283
|
-
const renderFn =
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6784
|
+
const renderFn =
|
|
6785
|
+
c4Level === 'deployment' ||
|
|
6786
|
+
(c4Level === 'components' && c4System && c4Container) ||
|
|
6787
|
+
(c4Level === 'containers' && c4System)
|
|
6788
|
+
? renderC4Containers
|
|
6789
|
+
: renderC4Context;
|
|
6790
|
+
|
|
6791
|
+
renderFn(
|
|
6792
|
+
container,
|
|
6793
|
+
c4Parsed,
|
|
6794
|
+
c4Layout,
|
|
6795
|
+
effectivePalette,
|
|
6796
|
+
theme === 'dark',
|
|
6797
|
+
undefined,
|
|
6798
|
+
{ width: exportWidth, height: exportHeight },
|
|
6799
|
+
options?.tagGroup
|
|
6800
|
+
);
|
|
6288
6801
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6289
6802
|
}
|
|
6290
6803
|
|
|
@@ -6300,7 +6813,15 @@ export async function renderForExport(
|
|
|
6300
6813
|
const layout = layoutGraph(fcParsed);
|
|
6301
6814
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6302
6815
|
|
|
6303
|
-
renderFlowchart(
|
|
6816
|
+
renderFlowchart(
|
|
6817
|
+
container,
|
|
6818
|
+
fcParsed,
|
|
6819
|
+
layout,
|
|
6820
|
+
effectivePalette,
|
|
6821
|
+
theme === 'dark',
|
|
6822
|
+
undefined,
|
|
6823
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
6824
|
+
);
|
|
6304
6825
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6305
6826
|
}
|
|
6306
6827
|
|
|
@@ -6308,7 +6829,8 @@ export async function renderForExport(
|
|
|
6308
6829
|
const { parseInfra } = await import('./infra/parser');
|
|
6309
6830
|
const { computeInfra } = await import('./infra/compute');
|
|
6310
6831
|
const { layoutInfra } = await import('./infra/layout');
|
|
6311
|
-
const { renderInfra, computeInfraLegendGroups } =
|
|
6832
|
+
const { renderInfra, computeInfraLegendGroups } =
|
|
6833
|
+
await import('./infra/renderer');
|
|
6312
6834
|
|
|
6313
6835
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6314
6836
|
const infraParsed = parseInfra(content);
|
|
@@ -6319,13 +6841,30 @@ export async function renderForExport(
|
|
|
6319
6841
|
const activeTagGroup = options?.tagGroup ?? null;
|
|
6320
6842
|
|
|
6321
6843
|
const titleOffset = infraParsed.title ? 40 : 0;
|
|
6322
|
-
const legendGroups = computeInfraLegendGroups(
|
|
6844
|
+
const legendGroups = computeInfraLegendGroups(
|
|
6845
|
+
infraLayout.nodes,
|
|
6846
|
+
infraParsed.tagGroups,
|
|
6847
|
+
effectivePalette
|
|
6848
|
+
);
|
|
6323
6849
|
const legendOffset = legendGroups.length > 0 ? 28 : 0;
|
|
6324
6850
|
const exportWidth = infraLayout.width;
|
|
6325
6851
|
const exportHeight = infraLayout.height + titleOffset + legendOffset;
|
|
6326
6852
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6327
6853
|
|
|
6328
|
-
renderInfra(
|
|
6854
|
+
renderInfra(
|
|
6855
|
+
container,
|
|
6856
|
+
infraLayout,
|
|
6857
|
+
effectivePalette,
|
|
6858
|
+
theme === 'dark',
|
|
6859
|
+
infraParsed.title,
|
|
6860
|
+
infraParsed.titleLineNumber,
|
|
6861
|
+
infraParsed.tagGroups,
|
|
6862
|
+
activeTagGroup,
|
|
6863
|
+
false,
|
|
6864
|
+
null,
|
|
6865
|
+
null,
|
|
6866
|
+
true
|
|
6867
|
+
);
|
|
6329
6868
|
// Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
|
|
6330
6869
|
const infraSvg = container.querySelector('svg');
|
|
6331
6870
|
if (infraSvg) {
|
|
@@ -6349,7 +6888,14 @@ export async function renderForExport(
|
|
|
6349
6888
|
const EXPORT_H = 800;
|
|
6350
6889
|
const container = createExportContainer(EXPORT_W, EXPORT_H);
|
|
6351
6890
|
|
|
6352
|
-
renderGantt(
|
|
6891
|
+
renderGantt(
|
|
6892
|
+
container,
|
|
6893
|
+
resolved,
|
|
6894
|
+
effectivePalette,
|
|
6895
|
+
theme === 'dark',
|
|
6896
|
+
undefined,
|
|
6897
|
+
{ width: EXPORT_W, height: EXPORT_H }
|
|
6898
|
+
);
|
|
6353
6899
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6354
6900
|
}
|
|
6355
6901
|
|
|
@@ -6365,7 +6911,15 @@ export async function renderForExport(
|
|
|
6365
6911
|
const layout = layoutGraph(stateParsed);
|
|
6366
6912
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6367
6913
|
|
|
6368
|
-
renderState(
|
|
6914
|
+
renderState(
|
|
6915
|
+
container,
|
|
6916
|
+
stateParsed,
|
|
6917
|
+
layout,
|
|
6918
|
+
effectivePalette,
|
|
6919
|
+
theme === 'dark',
|
|
6920
|
+
undefined,
|
|
6921
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
6922
|
+
);
|
|
6369
6923
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6370
6924
|
}
|
|
6371
6925
|
|
|
@@ -6391,30 +6945,75 @@ export async function renderForExport(
|
|
|
6391
6945
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6392
6946
|
const isDark = theme === 'dark';
|
|
6393
6947
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6394
|
-
const dims: D3ExportDimensions = {
|
|
6948
|
+
const dims: D3ExportDimensions = {
|
|
6949
|
+
width: EXPORT_WIDTH,
|
|
6950
|
+
height: EXPORT_HEIGHT,
|
|
6951
|
+
};
|
|
6395
6952
|
|
|
6396
6953
|
if (parsed.type === 'sequence') {
|
|
6397
6954
|
const { parseSequenceDgmo } = await import('./sequence/parser');
|
|
6398
6955
|
const { renderSequenceDiagram } = await import('./sequence/renderer');
|
|
6399
6956
|
const seqParsed = parseSequenceDgmo(content);
|
|
6400
6957
|
if (seqParsed.error || seqParsed.participants.length === 0) return '';
|
|
6401
|
-
renderSequenceDiagram(
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6958
|
+
renderSequenceDiagram(
|
|
6959
|
+
container,
|
|
6960
|
+
seqParsed,
|
|
6961
|
+
effectivePalette,
|
|
6962
|
+
isDark,
|
|
6963
|
+
undefined,
|
|
6964
|
+
{
|
|
6965
|
+
exportWidth: EXPORT_WIDTH,
|
|
6966
|
+
activeTagGroup: options?.tagGroup,
|
|
6967
|
+
}
|
|
6968
|
+
);
|
|
6405
6969
|
} else if (parsed.type === 'wordcloud') {
|
|
6406
|
-
await renderWordCloudAsync(
|
|
6970
|
+
await renderWordCloudAsync(
|
|
6971
|
+
container,
|
|
6972
|
+
parsed,
|
|
6973
|
+
effectivePalette,
|
|
6974
|
+
isDark,
|
|
6975
|
+
dims
|
|
6976
|
+
);
|
|
6407
6977
|
} else if (parsed.type === 'arc') {
|
|
6408
|
-
renderArcDiagram(
|
|
6978
|
+
renderArcDiagram(
|
|
6979
|
+
container,
|
|
6980
|
+
parsed,
|
|
6981
|
+
effectivePalette,
|
|
6982
|
+
isDark,
|
|
6983
|
+
undefined,
|
|
6984
|
+
dims
|
|
6985
|
+
);
|
|
6409
6986
|
} else if (parsed.type === 'timeline') {
|
|
6410
|
-
renderTimeline(
|
|
6411
|
-
|
|
6987
|
+
renderTimeline(
|
|
6988
|
+
container,
|
|
6989
|
+
parsed,
|
|
6990
|
+
effectivePalette,
|
|
6991
|
+
isDark,
|
|
6992
|
+
undefined,
|
|
6993
|
+
dims,
|
|
6994
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup,
|
|
6995
|
+
orgExportState?.swimlaneTagGroup
|
|
6996
|
+
);
|
|
6412
6997
|
} else if (parsed.type === 'venn') {
|
|
6413
6998
|
renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
|
|
6414
6999
|
} else if (parsed.type === 'quadrant') {
|
|
6415
|
-
renderQuadrant(
|
|
7000
|
+
renderQuadrant(
|
|
7001
|
+
container,
|
|
7002
|
+
parsed,
|
|
7003
|
+
effectivePalette,
|
|
7004
|
+
isDark,
|
|
7005
|
+
undefined,
|
|
7006
|
+
dims
|
|
7007
|
+
);
|
|
6416
7008
|
} else {
|
|
6417
|
-
renderSlopeChart(
|
|
7009
|
+
renderSlopeChart(
|
|
7010
|
+
container,
|
|
7011
|
+
parsed,
|
|
7012
|
+
effectivePalette,
|
|
7013
|
+
isDark,
|
|
7014
|
+
undefined,
|
|
7015
|
+
dims
|
|
7016
|
+
);
|
|
6418
7017
|
}
|
|
6419
7018
|
|
|
6420
7019
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|