@diagrammo/dgmo 0.8.3 → 0.8.5
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 +452 -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 +188 -185
- package/dist/editor.cjs +338 -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 +307 -0
- package/dist/editor.js.map +1 -0
- package/dist/highlight.cjs +560 -0
- package/dist/highlight.cjs.map +1 -0
- package/dist/highlight.d.cts +32 -0
- package/dist/highlight.d.ts +32 -0
- package/dist/highlight.js +530 -0
- package/dist/highlight.js.map +1 -0
- package/dist/index.cjs +3467 -1078
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -1
- package/dist/index.d.ts +22 -1
- package/dist/index.js +3466 -1078
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +46 -37
- 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 +9 -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 +71 -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 +211 -62
- package/src/completion.ts +378 -183
- package/src/d3.ts +1044 -303
- 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-api.ts +444 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +222 -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/index.ts +96 -31
- 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,
|
|
@@ -467,8 +509,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
467
509
|
let timelineEraBlockIndent = 0;
|
|
468
510
|
let inTimelineMarkerBlock = false;
|
|
469
511
|
let timelineMarkerBlockIndent = 0;
|
|
512
|
+
let inSlopePeriodBlock = false;
|
|
470
513
|
const timelineAliasMap = new Map<string, string>();
|
|
471
|
-
const VALID_D3_TYPES = new Set([
|
|
514
|
+
const VALID_D3_TYPES = new Set([
|
|
515
|
+
'slope',
|
|
516
|
+
'wordcloud',
|
|
517
|
+
'arc',
|
|
518
|
+
'timeline',
|
|
519
|
+
'venn',
|
|
520
|
+
'quadrant',
|
|
521
|
+
'sequence',
|
|
522
|
+
]);
|
|
472
523
|
let firstLineParsed = false;
|
|
473
524
|
|
|
474
525
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -509,7 +560,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
509
560
|
lineNumber,
|
|
510
561
|
};
|
|
511
562
|
if (tagBlockMatch.alias) {
|
|
512
|
-
timelineAliasMap.set(
|
|
563
|
+
timelineAliasMap.set(
|
|
564
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
565
|
+
tagBlockMatch.name.toLowerCase()
|
|
566
|
+
);
|
|
513
567
|
}
|
|
514
568
|
result.timelineTagGroups.push(currentTimelineTagGroup);
|
|
515
569
|
continue;
|
|
@@ -526,7 +580,11 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
526
580
|
const { label, color } = extractColor(entryText, palette);
|
|
527
581
|
if (color) {
|
|
528
582
|
if (isDefault) currentTimelineTagGroup.defaultValue = label;
|
|
529
|
-
currentTimelineTagGroup.entries.push({
|
|
583
|
+
currentTimelineTagGroup.entries.push({
|
|
584
|
+
value: label,
|
|
585
|
+
color,
|
|
586
|
+
lineNumber,
|
|
587
|
+
});
|
|
530
588
|
continue;
|
|
531
589
|
}
|
|
532
590
|
}
|
|
@@ -558,9 +616,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
558
616
|
}
|
|
559
617
|
|
|
560
618
|
// Reject legacy ## group syntax
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
result.
|
|
619
|
+
if (
|
|
620
|
+
/^#{2,}\s+/.test(line) &&
|
|
621
|
+
(result.type === 'arc' || result.type === 'timeline')
|
|
622
|
+
) {
|
|
623
|
+
const name = line
|
|
624
|
+
.replace(/^#{2,}\s+/, '')
|
|
625
|
+
.replace(/\s*\([^)]*\)\s*$/, '')
|
|
626
|
+
.trim();
|
|
627
|
+
result.diagnostics.push(
|
|
628
|
+
makeDgmoError(
|
|
629
|
+
lineNumber,
|
|
630
|
+
`'## ${name}' is no longer supported. Use '[${name}]' instead`,
|
|
631
|
+
'warning'
|
|
632
|
+
)
|
|
633
|
+
);
|
|
564
634
|
continue;
|
|
565
635
|
}
|
|
566
636
|
|
|
@@ -570,10 +640,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
570
640
|
currentTimelineGroup = null;
|
|
571
641
|
}
|
|
572
642
|
|
|
573
|
-
// Arc link line: source -> target(color)
|
|
643
|
+
// Arc link line: source -> target(color) weight
|
|
574
644
|
if (result.type === 'arc') {
|
|
575
645
|
const linkMatch = line.match(
|
|
576
|
-
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(
|
|
646
|
+
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
|
|
577
647
|
);
|
|
578
648
|
if (linkMatch) {
|
|
579
649
|
const source = linkMatch[1].trim();
|
|
@@ -613,7 +683,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
613
683
|
} else {
|
|
614
684
|
if (line.startsWith('//')) continue;
|
|
615
685
|
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
|
|
686
|
+
/^(\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
687
|
);
|
|
618
688
|
if (eraEntryMatch) {
|
|
619
689
|
const colorAnnotation = eraEntryMatch[4]?.trim() || null;
|
|
@@ -678,7 +748,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
678
748
|
|
|
679
749
|
// Timeline era lines (inline): era YYYY->YYYY Label (color)
|
|
680
750
|
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
|
|
751
|
+
/^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
752
|
);
|
|
683
753
|
if (eraMatch) {
|
|
684
754
|
const colorAnnotation = eraMatch[4]?.trim() || null;
|
|
@@ -696,7 +766,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
696
766
|
|
|
697
767
|
// Timeline marker lines (inline): marker YYYY Label (color)
|
|
698
768
|
const markerMatch = line.match(
|
|
699
|
-
/^marker
|
|
769
|
+
/^marker\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
700
770
|
);
|
|
701
771
|
if (markerMatch) {
|
|
702
772
|
const colorAnnotation = markerMatch[3]?.trim() || null;
|
|
@@ -719,7 +789,7 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
719
789
|
// Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
|
|
720
790
|
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
721
791
|
const durationMatch = line.match(
|
|
722
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)
|
|
792
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?\s+(.+)$/
|
|
723
793
|
);
|
|
724
794
|
if (durationMatch) {
|
|
725
795
|
const startDate = durationMatch[1];
|
|
@@ -728,9 +798,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
728
798
|
const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
|
|
729
799
|
const endDate = addDurationToDate(startDate, amount, unit);
|
|
730
800
|
const segments = durationMatch[5].split('|');
|
|
731
|
-
const metadata =
|
|
732
|
-
|
|
733
|
-
|
|
801
|
+
const metadata =
|
|
802
|
+
segments.length > 1
|
|
803
|
+
? parsePipeMetadata(
|
|
804
|
+
['', ...segments.slice(1)],
|
|
805
|
+
timelineAliasMap,
|
|
806
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
807
|
+
)
|
|
808
|
+
: {};
|
|
734
809
|
result.timelineEvents.push({
|
|
735
810
|
date: startDate,
|
|
736
811
|
endDate,
|
|
@@ -747,13 +822,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
747
822
|
// Also supports YYYY-MM-DD HH:MM in both start and end dates
|
|
748
823
|
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
749
824
|
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})?)?)(\?)
|
|
825
|
+
/^(\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
826
|
);
|
|
752
827
|
if (rangeMatch) {
|
|
753
828
|
const segments = rangeMatch[4].split('|');
|
|
754
|
-
const metadata =
|
|
755
|
-
|
|
756
|
-
|
|
829
|
+
const metadata =
|
|
830
|
+
segments.length > 1
|
|
831
|
+
? parsePipeMetadata(
|
|
832
|
+
['', ...segments.slice(1)],
|
|
833
|
+
timelineAliasMap,
|
|
834
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
835
|
+
)
|
|
836
|
+
: {};
|
|
757
837
|
result.timelineEvents.push({
|
|
758
838
|
date: rangeMatch[1],
|
|
759
839
|
endDate: rangeMatch[2],
|
|
@@ -766,15 +846,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
766
846
|
continue;
|
|
767
847
|
}
|
|
768
848
|
|
|
769
|
-
// Point event: 1718 description
|
|
770
|
-
const pointMatch = line.match(
|
|
771
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2})?)(?:\s*:\s*|\s+)(.+)$/
|
|
772
|
-
);
|
|
849
|
+
// Point event: 1718 description
|
|
850
|
+
const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
|
|
773
851
|
if (pointMatch) {
|
|
774
852
|
const segments = pointMatch[2].split('|');
|
|
775
|
-
const metadata =
|
|
776
|
-
|
|
777
|
-
|
|
853
|
+
const metadata =
|
|
854
|
+
segments.length > 1
|
|
855
|
+
? parsePipeMetadata(
|
|
856
|
+
['', ...segments.slice(1)],
|
|
857
|
+
timelineAliasMap,
|
|
858
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
859
|
+
)
|
|
860
|
+
: {};
|
|
778
861
|
result.timelineEvents.push({
|
|
779
862
|
date: pointMatch[1],
|
|
780
863
|
endDate: null,
|
|
@@ -799,40 +882,37 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
799
882
|
if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
|
|
800
883
|
}
|
|
801
884
|
|
|
802
|
-
const segments = line
|
|
885
|
+
const segments = line
|
|
886
|
+
.split('+')
|
|
887
|
+
.map((s) => s.trim())
|
|
888
|
+
.filter(Boolean);
|
|
803
889
|
if (segments.length >= 2) {
|
|
804
890
|
// All segments except the last are pure set references
|
|
805
891
|
const rawSets = segments.slice(0, -1);
|
|
806
892
|
const lastSeg = segments[segments.length - 1];
|
|
807
893
|
|
|
808
894
|
// For the last segment, extract set reference and optional label.
|
|
809
|
-
//
|
|
810
|
-
|
|
895
|
+
// Find where the set reference ends and label begins.
|
|
896
|
+
// Try progressively shorter prefixes against known set names/aliases.
|
|
897
|
+
const words = lastSeg.split(/\s+/);
|
|
898
|
+
let matchLen = 0;
|
|
899
|
+
for (let w = words.length; w >= 1; w--) {
|
|
900
|
+
const candidate = words.slice(0, w).join(' ');
|
|
901
|
+
if (knownSetRefs.has(candidate.toLowerCase())) {
|
|
902
|
+
matchLen = w;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
811
906
|
let lastSetRef: string;
|
|
812
907
|
let label: string | null;
|
|
813
|
-
if (
|
|
814
|
-
lastSetRef =
|
|
815
|
-
label =
|
|
908
|
+
if (matchLen > 0) {
|
|
909
|
+
lastSetRef = words.slice(0, matchLen).join(' ');
|
|
910
|
+
label =
|
|
911
|
+
words.length > matchLen ? words.slice(matchLen).join(' ') : null;
|
|
816
912
|
} 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
|
-
}
|
|
913
|
+
// No known set matched — assume first word is the set ref, rest is label
|
|
914
|
+
lastSetRef = words[0];
|
|
915
|
+
label = words.length > 1 ? words.slice(1).join(' ') : null;
|
|
836
916
|
}
|
|
837
917
|
rawSets.push(lastSetRef);
|
|
838
918
|
result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
|
|
@@ -841,7 +921,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
841
921
|
}
|
|
842
922
|
|
|
843
923
|
// Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
|
|
844
|
-
const setDeclMatch = line.match(
|
|
924
|
+
const setDeclMatch = line.match(
|
|
925
|
+
/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
|
|
926
|
+
);
|
|
845
927
|
if (setDeclMatch) {
|
|
846
928
|
const name = setDeclMatch[1].trim();
|
|
847
929
|
const colorName = setDeclMatch[2]?.trim() ?? null;
|
|
@@ -849,11 +931,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
849
931
|
if (colorName) {
|
|
850
932
|
const resolved = resolveColor(colorName, palette);
|
|
851
933
|
if (resolved === null) {
|
|
852
|
-
warn(
|
|
934
|
+
warn(
|
|
935
|
+
lineNumber,
|
|
936
|
+
`Hex colors are not supported — use named colors (blue, red, green, etc.)`
|
|
937
|
+
);
|
|
853
938
|
} else if (resolved.startsWith('#')) {
|
|
854
939
|
color = resolved;
|
|
855
940
|
} else {
|
|
856
|
-
warn(
|
|
941
|
+
warn(
|
|
942
|
+
lineNumber,
|
|
943
|
+
`Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`
|
|
944
|
+
);
|
|
857
945
|
}
|
|
858
946
|
}
|
|
859
947
|
const alias = setDeclMatch[3]?.trim() ?? null;
|
|
@@ -864,8 +952,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
864
952
|
|
|
865
953
|
// Quadrant-specific parsing
|
|
866
954
|
if (result.type === 'quadrant') {
|
|
867
|
-
// x-
|
|
868
|
-
const xAxisMatch = line.match(/^x-
|
|
955
|
+
// x-label Low, High — or indented multi-line
|
|
956
|
+
const xAxisMatch = line.match(/^x-label\s+(.*)/i);
|
|
869
957
|
if (xAxisMatch) {
|
|
870
958
|
const val = xAxisMatch[1].trim();
|
|
871
959
|
let parts: string[];
|
|
@@ -883,8 +971,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
883
971
|
continue;
|
|
884
972
|
}
|
|
885
973
|
|
|
886
|
-
// y-
|
|
887
|
-
const yAxisMatch = line.match(/^y-
|
|
974
|
+
// y-label Low, High — or indented multi-line
|
|
975
|
+
const yAxisMatch = line.match(/^y-label\s+(.*)/i);
|
|
888
976
|
if (yAxisMatch) {
|
|
889
977
|
const val = yAxisMatch[1].trim();
|
|
890
978
|
let parts: string[];
|
|
@@ -902,9 +990,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
902
990
|
continue;
|
|
903
991
|
}
|
|
904
992
|
|
|
905
|
-
// Quadrant position labels: top-right
|
|
993
|
+
// Quadrant position labels: top-right Label (color)
|
|
906
994
|
const quadrantLabelRe =
|
|
907
|
-
/^(top-right|top-left|bottom-left|bottom-right)\s
|
|
995
|
+
/^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
|
|
908
996
|
const quadrantMatch = line.match(quadrantLabelRe);
|
|
909
997
|
if (quadrantMatch) {
|
|
910
998
|
const position = quadrantMatch[1].toLowerCase();
|
|
@@ -926,9 +1014,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
926
1014
|
continue;
|
|
927
1015
|
}
|
|
928
1016
|
|
|
929
|
-
// Data points: Label
|
|
1017
|
+
// Data points: Label x, y
|
|
930
1018
|
const pointMatch = line.match(
|
|
931
|
-
/^(.+?)
|
|
1019
|
+
/^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
|
|
932
1020
|
);
|
|
933
1021
|
if (pointMatch) {
|
|
934
1022
|
const label = pointMatch[1].trim();
|
|
@@ -957,7 +1045,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
957
1045
|
const firstToken = line.substring(0, spaceIdx).toLowerCase();
|
|
958
1046
|
const restValue = line.substring(spaceIdx + 1).trim();
|
|
959
1047
|
|
|
960
|
-
if (
|
|
1048
|
+
if (
|
|
1049
|
+
firstToken === 'chart' &&
|
|
1050
|
+
VALID_D3_TYPES.has(restValue.toLowerCase())
|
|
1051
|
+
) {
|
|
961
1052
|
result.type = restValue.toLowerCase() as ParsedVisualization['type'];
|
|
962
1053
|
continue;
|
|
963
1054
|
}
|
|
@@ -1009,6 +1100,164 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1009
1100
|
}
|
|
1010
1101
|
}
|
|
1011
1102
|
|
|
1103
|
+
// ── Slope chart: period directive + right-scan data rows ──
|
|
1104
|
+
if (result.type === 'slope') {
|
|
1105
|
+
// Period block: indented lines inside `period` block
|
|
1106
|
+
// (blank lines are pre-filtered at loop top, so only non-indented lines close the block)
|
|
1107
|
+
if (inSlopePeriodBlock) {
|
|
1108
|
+
if (indent > 0) {
|
|
1109
|
+
result.periods.push(line);
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
// Non-indented line → close block, fall through to process normally
|
|
1113
|
+
inSlopePeriodBlock = false;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Period directive: `period Label1 Label2` or bare `period` (block open)
|
|
1117
|
+
// Only accept before data rows start (F4: prevent keyword shadowing labels)
|
|
1118
|
+
if (result.data.length === 0) {
|
|
1119
|
+
const periodMatch = line.match(/^period\b(.*)$/i);
|
|
1120
|
+
if (periodMatch) {
|
|
1121
|
+
if (result.periods.length > 0 && !inSlopePeriodBlock) {
|
|
1122
|
+
// F5: warn on duplicate period directives
|
|
1123
|
+
warn(
|
|
1124
|
+
lineNumber,
|
|
1125
|
+
`Duplicate 'period' directive — periods are already defined`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const rest = periodMatch[1].trim();
|
|
1129
|
+
if (rest) {
|
|
1130
|
+
// One-line: `period 1715 1725`
|
|
1131
|
+
const periodLabels = rest.split(/\s+/);
|
|
1132
|
+
result.periods.push(...periodLabels);
|
|
1133
|
+
} else {
|
|
1134
|
+
// Block open: bare `period`
|
|
1135
|
+
inSlopePeriodBlock = true;
|
|
1136
|
+
}
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Migration error: bare period line (old syntax — comma-separated, no keyword)
|
|
1142
|
+
// F1: Only fire when ALL comma-separated tokens are short (≤20 chars) and non-empty
|
|
1143
|
+
if (
|
|
1144
|
+
result.periods.length === 0 &&
|
|
1145
|
+
line.includes(',') &&
|
|
1146
|
+
!line.includes(':')
|
|
1147
|
+
) {
|
|
1148
|
+
const tokens = line
|
|
1149
|
+
.split(',')
|
|
1150
|
+
.map((t) => t.trim())
|
|
1151
|
+
.filter(Boolean);
|
|
1152
|
+
const looksLikePeriods =
|
|
1153
|
+
tokens.length >= 2 && tokens.every((t) => t.length <= 20);
|
|
1154
|
+
if (looksLikePeriods) {
|
|
1155
|
+
return fail(
|
|
1156
|
+
lineNumber,
|
|
1157
|
+
`Period lines require the 'period' keyword — use 'period ${tokens.join(' ')}'`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Migration error: old colon syntax in data rows
|
|
1163
|
+
// F2: Only fire when content after colon is predominantly numeric (old "Label: val1, val2" pattern)
|
|
1164
|
+
if (line.includes(':')) {
|
|
1165
|
+
const colonPos = line.indexOf(':');
|
|
1166
|
+
const afterColon = line.substring(colonPos + 1).trim();
|
|
1167
|
+
const numericTokens = afterColon
|
|
1168
|
+
.split(/[,\s]+/)
|
|
1169
|
+
.filter((v) => /^-?\d/.test(v));
|
|
1170
|
+
// Only trigger if most tokens after the colon are numeric (old data pattern)
|
|
1171
|
+
if (numericTokens.length >= 1) {
|
|
1172
|
+
const allTokens = afterColon.split(/[,\s]+/).filter(Boolean);
|
|
1173
|
+
if (numericTokens.length >= allTokens.length * 0.5) {
|
|
1174
|
+
const label = line.substring(0, colonPos).trim();
|
|
1175
|
+
return fail(
|
|
1176
|
+
lineNumber,
|
|
1177
|
+
`Colons are no longer used in slope data rows — use '${label} ${numericTokens.join(' ')}'`
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// Right-scan data row parsing (requires periods to be known)
|
|
1184
|
+
if (result.periods.length >= 2) {
|
|
1185
|
+
const P = result.periods.length;
|
|
1186
|
+
const tokens = line.split(/\s+/);
|
|
1187
|
+
const values: number[] = [];
|
|
1188
|
+
|
|
1189
|
+
// Scan from right, capped at P values
|
|
1190
|
+
let rightIdx = tokens.length - 1;
|
|
1191
|
+
while (rightIdx >= 0 && values.length < P) {
|
|
1192
|
+
const raw = tokens[rightIdx].replace(/,/g, '');
|
|
1193
|
+
const num = parseFloat(raw);
|
|
1194
|
+
if (!isNaN(num) && /^-?\d/.test(raw)) {
|
|
1195
|
+
values.unshift(num);
|
|
1196
|
+
rightIdx--;
|
|
1197
|
+
} else {
|
|
1198
|
+
break;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (values.length < P) {
|
|
1203
|
+
warn(
|
|
1204
|
+
lineNumber,
|
|
1205
|
+
`Data row has ${values.length} numeric value(s) but ${P} period(s) are defined — expected ${P} values`
|
|
1206
|
+
);
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Remaining left tokens = label
|
|
1211
|
+
const labelTokens = tokens.slice(0, rightIdx + 1);
|
|
1212
|
+
const joinedLabel = labelTokens.join(' ');
|
|
1213
|
+
|
|
1214
|
+
if (!joinedLabel) {
|
|
1215
|
+
warn(
|
|
1216
|
+
lineNumber,
|
|
1217
|
+
`Data row has no label — add a label before the numeric values`
|
|
1218
|
+
);
|
|
1219
|
+
continue;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Color annotation: `Label (color)` → extract color
|
|
1223
|
+
const colorMatch = joinedLabel.match(/^(.+?)\(([^)]+)\)\s*$/);
|
|
1224
|
+
const labelPart = colorMatch ? colorMatch[1].trim() : joinedLabel;
|
|
1225
|
+
const colorPart = colorMatch
|
|
1226
|
+
? resolveColor(colorMatch[2].trim(), palette)
|
|
1227
|
+
: null;
|
|
1228
|
+
|
|
1229
|
+
if (!labelPart) {
|
|
1230
|
+
warn(
|
|
1231
|
+
lineNumber,
|
|
1232
|
+
`Data row has no label — add a label before the numeric values`
|
|
1233
|
+
);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// F3: Warn on purely numeric labels — likely a mistake
|
|
1238
|
+
if (/^\d[\d,.]*$/.test(labelPart)) {
|
|
1239
|
+
warn(
|
|
1240
|
+
lineNumber,
|
|
1241
|
+
`Label '${labelPart}' looks numeric — this may indicate too many values or a missing label`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
result.data.push({
|
|
1246
|
+
label: labelPart,
|
|
1247
|
+
values,
|
|
1248
|
+
color: colorPart,
|
|
1249
|
+
lineNumber,
|
|
1250
|
+
});
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// If we get here in a slope chart, it's an unrecognized line
|
|
1255
|
+
if (firstLineParsed) {
|
|
1256
|
+
warn(lineNumber, `Unexpected line: '${line}'.`);
|
|
1257
|
+
}
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1012
1261
|
// ── Colon-separated metadata / options (legacy + data lines) ──
|
|
1013
1262
|
const colonIndex = line.indexOf(':');
|
|
1014
1263
|
|
|
@@ -1114,9 +1363,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1114
1363
|
} else if (colonIndex === -1) {
|
|
1115
1364
|
// Try "word weight" or "multi-word-label weight" space-separated format
|
|
1116
1365
|
const lastSpace = line.lastIndexOf(' ');
|
|
1117
|
-
const maybeWeight =
|
|
1366
|
+
const maybeWeight =
|
|
1367
|
+
lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
|
|
1118
1368
|
if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
|
|
1119
|
-
result.words.push({
|
|
1369
|
+
result.words.push({
|
|
1370
|
+
text: line.substring(0, lastSpace).trim(),
|
|
1371
|
+
weight: maybeWeight,
|
|
1372
|
+
lineNumber,
|
|
1373
|
+
});
|
|
1120
1374
|
} else {
|
|
1121
1375
|
freeformLines.push(line);
|
|
1122
1376
|
}
|
|
@@ -1127,29 +1381,22 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1127
1381
|
continue;
|
|
1128
1382
|
}
|
|
1129
1383
|
|
|
1130
|
-
//
|
|
1131
|
-
//
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1134
|
-
line.includes(',') &&
|
|
1135
|
-
!line.includes(':')
|
|
1136
|
-
) {
|
|
1137
|
-
const periods = line
|
|
1138
|
-
.split(',')
|
|
1139
|
-
.map((p) => p.trim())
|
|
1140
|
-
.filter(Boolean);
|
|
1141
|
-
if (periods.length >= 2) {
|
|
1142
|
-
result.periods = periods;
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1384
|
+
// Catch-all: nothing matched this line
|
|
1385
|
+
// Skip on first line — chart type suggestion is handled post-loop
|
|
1386
|
+
if (firstLineParsed) {
|
|
1387
|
+
warn(lineNumber, `Unexpected line: '${line}'.`);
|
|
1145
1388
|
}
|
|
1146
1389
|
}
|
|
1147
1390
|
|
|
1148
1391
|
// Validation
|
|
1149
1392
|
if (!result.type) {
|
|
1150
1393
|
const validD3Types = [...VALID_D3_TYPES];
|
|
1151
|
-
const firstNonEmpty =
|
|
1152
|
-
|
|
1394
|
+
const firstNonEmpty =
|
|
1395
|
+
lines.find((l) => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
|
|
1396
|
+
const hint = suggest(
|
|
1397
|
+
firstNonEmpty.split(/\s/)[0].toLowerCase(),
|
|
1398
|
+
validD3Types
|
|
1399
|
+
);
|
|
1153
1400
|
let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
|
|
1154
1401
|
if (hint) msg += `. ${hint}`;
|
|
1155
1402
|
return fail(1, msg);
|
|
@@ -1166,7 +1413,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1166
1413
|
result.words = tokenizeFreeformText(freeformLines.join(' '));
|
|
1167
1414
|
}
|
|
1168
1415
|
if (result.words.length === 0) {
|
|
1169
|
-
warn(
|
|
1416
|
+
warn(
|
|
1417
|
+
1,
|
|
1418
|
+
'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text'
|
|
1419
|
+
);
|
|
1170
1420
|
}
|
|
1171
1421
|
// Apply max word limit (words are already sorted by weight desc for freeform)
|
|
1172
1422
|
if (
|
|
@@ -1183,12 +1433,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1183
1433
|
|
|
1184
1434
|
if (result.type === 'arc') {
|
|
1185
1435
|
if (result.links.length === 0) {
|
|
1186
|
-
warn(
|
|
1436
|
+
warn(
|
|
1437
|
+
1,
|
|
1438
|
+
'No links found. Add links as "Source -> Target weight" (e.g., "Alice -> Bob 5")'
|
|
1439
|
+
);
|
|
1187
1440
|
}
|
|
1188
1441
|
// Validate arc ordering vs groups
|
|
1189
1442
|
if (result.arcNodeGroups.length > 0) {
|
|
1190
1443
|
if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
|
|
1191
|
-
warn(
|
|
1444
|
+
warn(
|
|
1445
|
+
1,
|
|
1446
|
+
`Cannot use "order ${result.arcOrder}" with [Group] headers. Use "order group" or remove group headers.`
|
|
1447
|
+
);
|
|
1192
1448
|
result.arcOrder = 'group';
|
|
1193
1449
|
}
|
|
1194
1450
|
if (result.arcOrder === 'appearance') {
|
|
@@ -1200,15 +1456,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1200
1456
|
|
|
1201
1457
|
if (result.type === 'timeline') {
|
|
1202
1458
|
if (result.timelineEvents.length === 0) {
|
|
1203
|
-
warn(
|
|
1459
|
+
warn(
|
|
1460
|
+
1,
|
|
1461
|
+
'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"'
|
|
1462
|
+
);
|
|
1204
1463
|
}
|
|
1205
1464
|
// Validate tag values and inject defaults
|
|
1206
1465
|
if (result.timelineTagGroups.length > 0) {
|
|
1207
1466
|
validateTagValues(
|
|
1208
1467
|
result.timelineEvents,
|
|
1209
1468
|
result.timelineTagGroups,
|
|
1210
|
-
(line, msg) =>
|
|
1211
|
-
|
|
1469
|
+
(line, msg) =>
|
|
1470
|
+
result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
|
|
1471
|
+
suggest
|
|
1212
1472
|
);
|
|
1213
1473
|
for (const group of result.timelineTagGroups) {
|
|
1214
1474
|
if (!group.defaultValue) continue;
|
|
@@ -1226,7 +1486,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1226
1486
|
|
|
1227
1487
|
if (result.type === 'venn') {
|
|
1228
1488
|
if (result.vennSets.length < 2) {
|
|
1229
|
-
return fail(
|
|
1489
|
+
return fail(
|
|
1490
|
+
1,
|
|
1491
|
+
'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")'
|
|
1492
|
+
);
|
|
1230
1493
|
}
|
|
1231
1494
|
if (result.vennSets.length > 3) {
|
|
1232
1495
|
return fail(1, 'Venn diagrams support 2–3 sets');
|
|
@@ -1240,7 +1503,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1240
1503
|
if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
|
|
1241
1504
|
}
|
|
1242
1505
|
const resolveSetRef = (ref: string): string | null =>
|
|
1243
|
-
setNameLower.get(ref.toLowerCase()) ??
|
|
1506
|
+
setNameLower.get(ref.toLowerCase()) ??
|
|
1507
|
+
aliasLower.get(ref.toLowerCase()) ??
|
|
1508
|
+
null;
|
|
1244
1509
|
|
|
1245
1510
|
// Resolve intersection set references; drop invalid ones with a diagnostic
|
|
1246
1511
|
const validOverlaps: VennOverlap[] = [];
|
|
@@ -1250,8 +1515,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1250
1515
|
for (const ref of ov.sets) {
|
|
1251
1516
|
const resolved = resolveSetRef(ref);
|
|
1252
1517
|
if (!resolved) {
|
|
1253
|
-
result.diagnostics.push(
|
|
1254
|
-
|
|
1518
|
+
result.diagnostics.push(
|
|
1519
|
+
makeDgmoError(
|
|
1520
|
+
ov.lineNumber,
|
|
1521
|
+
`Intersection references unknown set or alias "${ref}"`
|
|
1522
|
+
)
|
|
1523
|
+
);
|
|
1524
|
+
if (!result.error)
|
|
1525
|
+
result.error = formatDgmoError(
|
|
1526
|
+
result.diagnostics[result.diagnostics.length - 1]
|
|
1527
|
+
);
|
|
1255
1528
|
valid = false;
|
|
1256
1529
|
break;
|
|
1257
1530
|
}
|
|
@@ -1265,24 +1538,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1265
1538
|
|
|
1266
1539
|
if (result.type === 'quadrant') {
|
|
1267
1540
|
if (result.quadrantPoints.length === 0) {
|
|
1268
|
-
warn(
|
|
1541
|
+
warn(
|
|
1542
|
+
1,
|
|
1543
|
+
'No data points found. Add points as "Label x, y" (e.g., "Item A 0.5, 0.7")'
|
|
1544
|
+
);
|
|
1269
1545
|
}
|
|
1270
1546
|
return result;
|
|
1271
1547
|
}
|
|
1272
1548
|
|
|
1273
1549
|
// Slope chart validation
|
|
1274
1550
|
if (result.periods.length < 2) {
|
|
1275
|
-
return fail(
|
|
1551
|
+
return fail(
|
|
1552
|
+
1,
|
|
1553
|
+
"Missing 'period' directive. Add 'period 2020 2024' before data rows (minimum 2 periods required)"
|
|
1554
|
+
);
|
|
1276
1555
|
}
|
|
1277
1556
|
|
|
1278
1557
|
if (result.data.length === 0) {
|
|
1279
|
-
warn(
|
|
1558
|
+
warn(
|
|
1559
|
+
1,
|
|
1560
|
+
"No data lines found. Add data as 'Label value1 value2' (e.g., 'Blackbeard 40 4')"
|
|
1561
|
+
);
|
|
1280
1562
|
}
|
|
1281
1563
|
|
|
1282
1564
|
// Validate value counts match period count — warn and skip mismatched items
|
|
1283
1565
|
for (const item of result.data) {
|
|
1284
1566
|
if (item.values.length !== result.periods.length) {
|
|
1285
|
-
warn(
|
|
1567
|
+
warn(
|
|
1568
|
+
item.lineNumber,
|
|
1569
|
+
`Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`
|
|
1570
|
+
);
|
|
1286
1571
|
}
|
|
1287
1572
|
}
|
|
1288
1573
|
result.data = result.data.filter(
|
|
@@ -1523,7 +1808,14 @@ export function renderSlopeChart(
|
|
|
1523
1808
|
const tooltip = createTooltip(container, palette, isDark);
|
|
1524
1809
|
|
|
1525
1810
|
// Title
|
|
1526
|
-
renderChartTitle(
|
|
1811
|
+
renderChartTitle(
|
|
1812
|
+
svg,
|
|
1813
|
+
title,
|
|
1814
|
+
parsed.titleLineNumber,
|
|
1815
|
+
width,
|
|
1816
|
+
textColor,
|
|
1817
|
+
onClickItem
|
|
1818
|
+
);
|
|
1527
1819
|
|
|
1528
1820
|
// Period column headers
|
|
1529
1821
|
for (const period of periods) {
|
|
@@ -1592,13 +1884,23 @@ export function renderSlopeChart(
|
|
|
1592
1884
|
wrappedLines = lines;
|
|
1593
1885
|
}
|
|
1594
1886
|
const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
|
|
1595
|
-
const labelHeight =
|
|
1596
|
-
|
|
1597
|
-
|
|
1887
|
+
const labelHeight =
|
|
1888
|
+
labelLineCount === 1
|
|
1889
|
+
? SLOPE_LABEL_FONT_SIZE
|
|
1890
|
+
: labelLineCount * lineHeight;
|
|
1598
1891
|
|
|
1599
1892
|
return {
|
|
1600
|
-
item,
|
|
1601
|
-
|
|
1893
|
+
item,
|
|
1894
|
+
idx,
|
|
1895
|
+
color,
|
|
1896
|
+
firstVal,
|
|
1897
|
+
lastVal,
|
|
1898
|
+
tipHtml,
|
|
1899
|
+
lastX,
|
|
1900
|
+
labelText,
|
|
1901
|
+
maxChars,
|
|
1902
|
+
wrappedLines,
|
|
1903
|
+
labelHeight,
|
|
1602
1904
|
};
|
|
1603
1905
|
});
|
|
1604
1906
|
|
|
@@ -1610,7 +1912,10 @@ export function renderSlopeChart(
|
|
|
1610
1912
|
naturalY: yScale(item.values[pi]),
|
|
1611
1913
|
height: leftLabelHeight,
|
|
1612
1914
|
}));
|
|
1613
|
-
leftLabelCollisions.set(
|
|
1915
|
+
leftLabelCollisions.set(
|
|
1916
|
+
pi,
|
|
1917
|
+
resolveVerticalCollisions(entries, 4, innerHeight)
|
|
1918
|
+
);
|
|
1614
1919
|
}
|
|
1615
1920
|
|
|
1616
1921
|
// --- Resolve right-side label collisions ---
|
|
@@ -1618,7 +1923,11 @@ export function renderSlopeChart(
|
|
|
1618
1923
|
naturalY: yScale(si.lastVal),
|
|
1619
1924
|
height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
|
|
1620
1925
|
}));
|
|
1621
|
-
const rightAdjustedY = resolveVerticalCollisions(
|
|
1926
|
+
const rightAdjustedY = resolveVerticalCollisions(
|
|
1927
|
+
rightEntries,
|
|
1928
|
+
4,
|
|
1929
|
+
innerHeight
|
|
1930
|
+
);
|
|
1622
1931
|
|
|
1623
1932
|
// Render each data series
|
|
1624
1933
|
data.forEach((item, idx) => {
|
|
@@ -1632,7 +1941,8 @@ export function renderSlopeChart(
|
|
|
1632
1941
|
.attr('data-line-number', String(item.lineNumber));
|
|
1633
1942
|
|
|
1634
1943
|
// Line
|
|
1635
|
-
seriesG
|
|
1944
|
+
seriesG
|
|
1945
|
+
.append('path')
|
|
1636
1946
|
.datum(item.values)
|
|
1637
1947
|
.attr('fill', 'none')
|
|
1638
1948
|
.attr('stroke', color)
|
|
@@ -1640,7 +1950,8 @@ export function renderSlopeChart(
|
|
|
1640
1950
|
.attr('d', lineGen);
|
|
1641
1951
|
|
|
1642
1952
|
// Invisible wider path for easier hover targeting
|
|
1643
|
-
seriesG
|
|
1953
|
+
seriesG
|
|
1954
|
+
.append('path')
|
|
1644
1955
|
.datum(item.values)
|
|
1645
1956
|
.attr('fill', 'none')
|
|
1646
1957
|
.attr('stroke', 'transparent')
|
|
@@ -1664,7 +1975,8 @@ export function renderSlopeChart(
|
|
|
1664
1975
|
const y = yScale(val);
|
|
1665
1976
|
|
|
1666
1977
|
// Point circle
|
|
1667
|
-
seriesG
|
|
1978
|
+
seriesG
|
|
1979
|
+
.append('circle')
|
|
1668
1980
|
.attr('cx', x)
|
|
1669
1981
|
.attr('cy', y)
|
|
1670
1982
|
.attr('r', 4)
|
|
@@ -1688,7 +2000,8 @@ export function renderSlopeChart(
|
|
|
1688
2000
|
const isLast = i === periods.length - 1;
|
|
1689
2001
|
if (!isLast) {
|
|
1690
2002
|
const adjustedY = leftLabelCollisions.get(i)![idx];
|
|
1691
|
-
seriesG
|
|
2003
|
+
seriesG
|
|
2004
|
+
.append('text')
|
|
1692
2005
|
.attr('x', isFirst ? x - 10 : x)
|
|
1693
2006
|
.attr('y', adjustedY)
|
|
1694
2007
|
.attr('dy', '0.35em')
|
|
@@ -1920,7 +2233,14 @@ export function renderArcDiagram(
|
|
|
1920
2233
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
1921
2234
|
|
|
1922
2235
|
// Title
|
|
1923
|
-
renderChartTitle(
|
|
2236
|
+
renderChartTitle(
|
|
2237
|
+
svg,
|
|
2238
|
+
title,
|
|
2239
|
+
parsed.titleLineNumber,
|
|
2240
|
+
width,
|
|
2241
|
+
textColor,
|
|
2242
|
+
onClickItem
|
|
2243
|
+
);
|
|
1924
2244
|
|
|
1925
2245
|
// Build adjacency map for hover interactions
|
|
1926
2246
|
const neighbors = new Map<string, Set<string>>();
|
|
@@ -2097,13 +2417,18 @@ export function renderArcDiagram(
|
|
|
2097
2417
|
const y = yScale(node)!;
|
|
2098
2418
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2099
2419
|
// Find the first link involving this node (for line number and click target)
|
|
2100
|
-
const nodeLink = links.find(
|
|
2420
|
+
const nodeLink = links.find(
|
|
2421
|
+
(l) => l.source === node || l.target === node
|
|
2422
|
+
);
|
|
2101
2423
|
|
|
2102
2424
|
const nodeG = g
|
|
2103
2425
|
.append('g')
|
|
2104
2426
|
.attr('class', 'arc-node')
|
|
2105
2427
|
.attr('data-node', node)
|
|
2106
|
-
.attr(
|
|
2428
|
+
.attr(
|
|
2429
|
+
'data-line-number',
|
|
2430
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2431
|
+
)
|
|
2107
2432
|
.style('cursor', 'pointer')
|
|
2108
2433
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2109
2434
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2232,13 +2557,18 @@ export function renderArcDiagram(
|
|
|
2232
2557
|
const x = xScale(node)!;
|
|
2233
2558
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2234
2559
|
// Find the first link involving this node (for line number and click target)
|
|
2235
|
-
const nodeLink = links.find(
|
|
2560
|
+
const nodeLink = links.find(
|
|
2561
|
+
(l) => l.source === node || l.target === node
|
|
2562
|
+
);
|
|
2236
2563
|
|
|
2237
2564
|
const nodeG = g
|
|
2238
2565
|
.append('g')
|
|
2239
2566
|
.attr('class', 'arc-node')
|
|
2240
2567
|
.attr('data-node', node)
|
|
2241
|
-
.attr(
|
|
2568
|
+
.attr(
|
|
2569
|
+
'data-line-number',
|
|
2570
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2571
|
+
)
|
|
2242
2572
|
.style('cursor', 'pointer')
|
|
2243
2573
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2244
2574
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2633,7 +2963,11 @@ export function computeTimeTicks(
|
|
|
2633
2963
|
// Iterate from the start hour boundary
|
|
2634
2964
|
const startDate = fractionalYearToDate(domainMin);
|
|
2635
2965
|
// Round down to nearest step boundary
|
|
2636
|
-
startDate.setMinutes(
|
|
2966
|
+
startDate.setMinutes(
|
|
2967
|
+
Math.floor(startDate.getMinutes() / stepMin) * stepMin,
|
|
2968
|
+
0,
|
|
2969
|
+
0
|
|
2970
|
+
);
|
|
2637
2971
|
|
|
2638
2972
|
while (true) {
|
|
2639
2973
|
const val = dateToFractionalYear(startDate);
|
|
@@ -2659,7 +2993,12 @@ export function computeTimeTicks(
|
|
|
2659
2993
|
|
|
2660
2994
|
const startDate = fractionalYearToDate(domainMin);
|
|
2661
2995
|
// Round down to nearest step boundary
|
|
2662
|
-
startDate.setHours(
|
|
2996
|
+
startDate.setHours(
|
|
2997
|
+
Math.floor(startDate.getHours() / stepHour) * stepHour,
|
|
2998
|
+
0,
|
|
2999
|
+
0,
|
|
3000
|
+
0
|
|
3001
|
+
);
|
|
2663
3002
|
|
|
2664
3003
|
while (true) {
|
|
2665
3004
|
const val = dateToFractionalYear(startDate);
|
|
@@ -3072,7 +3411,10 @@ export function renderTimeline(
|
|
|
3072
3411
|
exportDims?: D3ExportDimensions,
|
|
3073
3412
|
activeTagGroup?: string | null,
|
|
3074
3413
|
swimlaneTagGroup?: string | null,
|
|
3075
|
-
onTagStateChange?: (
|
|
3414
|
+
onTagStateChange?: (
|
|
3415
|
+
activeTagGroup: string | null,
|
|
3416
|
+
swimlaneTagGroup: string | null
|
|
3417
|
+
) => void,
|
|
3076
3418
|
viewMode?: boolean
|
|
3077
3419
|
): void {
|
|
3078
3420
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
@@ -3091,7 +3433,11 @@ export function renderTimeline(
|
|
|
3091
3433
|
if (timelineEvents.length === 0) return;
|
|
3092
3434
|
|
|
3093
3435
|
// When sort: tag is set and no explicit swimlane param, use the default
|
|
3094
|
-
if (
|
|
3436
|
+
if (
|
|
3437
|
+
swimlaneTagGroup == null &&
|
|
3438
|
+
timelineSort === 'tag' &&
|
|
3439
|
+
parsed.timelineDefaultSwimlaneTG
|
|
3440
|
+
) {
|
|
3095
3441
|
swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
|
|
3096
3442
|
}
|
|
3097
3443
|
|
|
@@ -3143,12 +3489,8 @@ export function renderTimeline(
|
|
|
3143
3489
|
|
|
3144
3490
|
// Order lanes by earliest event date
|
|
3145
3491
|
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
|
-
);
|
|
3492
|
+
const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
|
|
3493
|
+
const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
|
|
3152
3494
|
return aMin - bMin;
|
|
3153
3495
|
});
|
|
3154
3496
|
|
|
@@ -3170,7 +3512,11 @@ export function renderTimeline(
|
|
|
3170
3512
|
function eventColor(ev: TimelineEvent): string {
|
|
3171
3513
|
// Tag color takes priority when a tag group is active
|
|
3172
3514
|
if (effectiveColorTG) {
|
|
3173
|
-
const tagColor = resolveTagColor(
|
|
3515
|
+
const tagColor = resolveTagColor(
|
|
3516
|
+
ev.metadata,
|
|
3517
|
+
parsed.timelineTagGroups,
|
|
3518
|
+
effectiveColorTG
|
|
3519
|
+
);
|
|
3174
3520
|
if (tagColor) return tagColor;
|
|
3175
3521
|
}
|
|
3176
3522
|
if (ev.group && groupColorMap.has(ev.group)) {
|
|
@@ -3281,16 +3627,23 @@ export function renderTimeline(
|
|
|
3281
3627
|
el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
|
|
3282
3628
|
});
|
|
3283
3629
|
g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
|
|
3284
|
-
'opacity',
|
|
3630
|
+
'opacity',
|
|
3631
|
+
FADE_OPACITY
|
|
3632
|
+
);
|
|
3633
|
+
g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
|
|
3634
|
+
'opacity',
|
|
3635
|
+
FADE_OPACITY
|
|
3285
3636
|
);
|
|
3286
|
-
g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
|
|
3287
3637
|
// Fade legend entry dots/labels that don't match (keep group pill visible)
|
|
3288
3638
|
g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
|
|
3289
3639
|
const el = d3Selection.select(this);
|
|
3290
3640
|
const entryValue = el.attr('data-legend-entry');
|
|
3291
3641
|
if (entryValue === '__group__') return; // keep group pill at full opacity
|
|
3292
3642
|
const entryGroup = el.attr('data-tag-group');
|
|
3293
|
-
el.attr(
|
|
3643
|
+
el.attr(
|
|
3644
|
+
'opacity',
|
|
3645
|
+
entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
|
|
3646
|
+
);
|
|
3294
3647
|
});
|
|
3295
3648
|
}
|
|
3296
3649
|
|
|
@@ -3311,7 +3664,8 @@ export function renderTimeline(
|
|
|
3311
3664
|
// VERTICAL orientation (time flows top→bottom)
|
|
3312
3665
|
// ================================================================
|
|
3313
3666
|
if (isVertical) {
|
|
3314
|
-
const useGroupedVertical =
|
|
3667
|
+
const useGroupedVertical =
|
|
3668
|
+
tagLanes != null ||
|
|
3315
3669
|
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
3316
3670
|
if (useGroupedVertical) {
|
|
3317
3671
|
// === GROUPED: one column/lane per group, vertical ===
|
|
@@ -3370,7 +3724,14 @@ export function renderTimeline(
|
|
|
3370
3724
|
.append('g')
|
|
3371
3725
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3372
3726
|
|
|
3373
|
-
renderChartTitle(
|
|
3727
|
+
renderChartTitle(
|
|
3728
|
+
svg,
|
|
3729
|
+
title,
|
|
3730
|
+
parsed.titleLineNumber,
|
|
3731
|
+
width,
|
|
3732
|
+
textColor,
|
|
3733
|
+
onClickItem
|
|
3734
|
+
);
|
|
3374
3735
|
|
|
3375
3736
|
renderEras(
|
|
3376
3737
|
g,
|
|
@@ -3620,7 +3981,14 @@ export function renderTimeline(
|
|
|
3620
3981
|
.append('g')
|
|
3621
3982
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3622
3983
|
|
|
3623
|
-
renderChartTitle(
|
|
3984
|
+
renderChartTitle(
|
|
3985
|
+
svg,
|
|
3986
|
+
title,
|
|
3987
|
+
parsed.titleLineNumber,
|
|
3988
|
+
width,
|
|
3989
|
+
textColor,
|
|
3990
|
+
onClickItem
|
|
3991
|
+
);
|
|
3624
3992
|
|
|
3625
3993
|
renderEras(
|
|
3626
3994
|
g,
|
|
@@ -3746,8 +4114,7 @@ export function renderTimeline(
|
|
|
3746
4114
|
if (ev.uncertain) {
|
|
3747
4115
|
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3748
4116
|
const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
|
|
3749
|
-
const defs =
|
|
3750
|
-
svg.select('defs').node() || svg.append('defs').node();
|
|
4117
|
+
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
3751
4118
|
const defsEl = d3Selection.select(defs as Element);
|
|
3752
4119
|
defsEl
|
|
3753
4120
|
.append('linearGradient')
|
|
@@ -3861,8 +4228,8 @@ export function renderTimeline(
|
|
|
3861
4228
|
const BAR_H = 22; // range bar thickness (tall enough for text inside)
|
|
3862
4229
|
const GROUP_GAP = 12; // vertical gap between group swim-lanes
|
|
3863
4230
|
|
|
3864
|
-
const useGroupedHorizontal =
|
|
3865
|
-
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
4231
|
+
const useGroupedHorizontal =
|
|
4232
|
+
tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
|
|
3866
4233
|
if (useGroupedHorizontal) {
|
|
3867
4234
|
// === GROUPED: swim-lanes stacked vertically, events on own rows ===
|
|
3868
4235
|
let lanes: Lane[];
|
|
@@ -3895,7 +4262,11 @@ export function renderTimeline(
|
|
|
3895
4262
|
// Group-sorted doesn't need legend space (group names shown on left)
|
|
3896
4263
|
const baseTopMargin = title ? 50 : 20;
|
|
3897
4264
|
const margin = {
|
|
3898
|
-
top:
|
|
4265
|
+
top:
|
|
4266
|
+
baseTopMargin +
|
|
4267
|
+
(timelineScale ? 40 : 0) +
|
|
4268
|
+
markerMargin +
|
|
4269
|
+
tagLegendReserve,
|
|
3899
4270
|
right: 40,
|
|
3900
4271
|
bottom: 40 + scaleMargin,
|
|
3901
4272
|
left: dynamicLeftMargin,
|
|
@@ -3921,7 +4292,14 @@ export function renderTimeline(
|
|
|
3921
4292
|
.append('g')
|
|
3922
4293
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3923
4294
|
|
|
3924
|
-
renderChartTitle(
|
|
4295
|
+
renderChartTitle(
|
|
4296
|
+
svg,
|
|
4297
|
+
title,
|
|
4298
|
+
parsed.titleLineNumber,
|
|
4299
|
+
width,
|
|
4300
|
+
textColor,
|
|
4301
|
+
onClickItem
|
|
4302
|
+
);
|
|
3925
4303
|
|
|
3926
4304
|
renderEras(
|
|
3927
4305
|
g,
|
|
@@ -4222,7 +4600,14 @@ export function renderTimeline(
|
|
|
4222
4600
|
.append('g')
|
|
4223
4601
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
4224
4602
|
|
|
4225
|
-
renderChartTitle(
|
|
4603
|
+
renderChartTitle(
|
|
4604
|
+
svg,
|
|
4605
|
+
title,
|
|
4606
|
+
parsed.titleLineNumber,
|
|
4607
|
+
width,
|
|
4608
|
+
textColor,
|
|
4609
|
+
onClickItem
|
|
4610
|
+
);
|
|
4226
4611
|
|
|
4227
4612
|
renderEras(
|
|
4228
4613
|
g,
|
|
@@ -4500,13 +4885,17 @@ export function renderTimeline(
|
|
|
4500
4885
|
expandedWidth: number;
|
|
4501
4886
|
};
|
|
4502
4887
|
const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
|
|
4503
|
-
const pillW =
|
|
4888
|
+
const pillW =
|
|
4889
|
+
measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4504
4890
|
// Expanded: pill + icon (unless viewMode) + entries
|
|
4505
4891
|
const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
|
|
4506
4892
|
let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
|
|
4507
4893
|
for (const entry of g.entries) {
|
|
4508
4894
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4509
|
-
entryX =
|
|
4895
|
+
entryX =
|
|
4896
|
+
textX +
|
|
4897
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
4898
|
+
LG_ENTRY_TRAIL;
|
|
4510
4899
|
}
|
|
4511
4900
|
return {
|
|
4512
4901
|
group: g,
|
|
@@ -4526,7 +4915,8 @@ export function renderTimeline(
|
|
|
4526
4915
|
y: number,
|
|
4527
4916
|
isSwimActive: boolean
|
|
4528
4917
|
) {
|
|
4529
|
-
const iconG = parent
|
|
4918
|
+
const iconG = parent
|
|
4919
|
+
.append('g')
|
|
4530
4920
|
.attr('class', 'tl-swimlane-icon')
|
|
4531
4921
|
.attr('transform', `translate(${x}, ${y})`)
|
|
4532
4922
|
.style('cursor', 'pointer');
|
|
@@ -4539,7 +4929,8 @@ export function renderTimeline(
|
|
|
4539
4929
|
{ y: 8, w: 6 },
|
|
4540
4930
|
];
|
|
4541
4931
|
for (const bar of bars) {
|
|
4542
|
-
iconG
|
|
4932
|
+
iconG
|
|
4933
|
+
.append('rect')
|
|
4543
4934
|
.attr('x', 0)
|
|
4544
4935
|
.attr('y', bar.y)
|
|
4545
4936
|
.attr('width', bar.w)
|
|
@@ -4554,8 +4945,16 @@ export function renderTimeline(
|
|
|
4554
4945
|
/** Full re-render with updated swimlane state */
|
|
4555
4946
|
function relayout() {
|
|
4556
4947
|
renderTimeline(
|
|
4557
|
-
container,
|
|
4558
|
-
|
|
4948
|
+
container,
|
|
4949
|
+
parsed,
|
|
4950
|
+
palette,
|
|
4951
|
+
isDark,
|
|
4952
|
+
onClickItem,
|
|
4953
|
+
exportDims,
|
|
4954
|
+
currentActiveGroup,
|
|
4955
|
+
currentSwimlaneGroup,
|
|
4956
|
+
onTagStateChange,
|
|
4957
|
+
viewMode
|
|
4559
4958
|
);
|
|
4560
4959
|
}
|
|
4561
4960
|
|
|
@@ -4565,7 +4964,8 @@ export function renderTimeline(
|
|
|
4565
4964
|
mainSvg.selectAll('.tl-tag-legend-container').remove();
|
|
4566
4965
|
|
|
4567
4966
|
// Effective color source: explicit color group > swimlane group
|
|
4568
|
-
const effectiveColorKey =
|
|
4967
|
+
const effectiveColorKey =
|
|
4968
|
+
(currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
|
|
4569
4969
|
|
|
4570
4970
|
// In view mode, only show the color-driving tag group (expanded, non-interactive).
|
|
4571
4971
|
// Skip the swimlane group if it's separate from the color group (lane headers already label it).
|
|
@@ -4580,32 +4980,43 @@ export function renderTimeline(
|
|
|
4580
4980
|
if (visibleGroups.length === 0) return;
|
|
4581
4981
|
|
|
4582
4982
|
// Compute total width and center horizontally in SVG
|
|
4583
|
-
const totalW =
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4983
|
+
const totalW =
|
|
4984
|
+
visibleGroups.reduce((s, lg) => {
|
|
4985
|
+
const isActive =
|
|
4986
|
+
viewMode ||
|
|
4987
|
+
(currentActiveGroup != null &&
|
|
4988
|
+
lg.group.name.toLowerCase() ===
|
|
4989
|
+
currentActiveGroup.toLowerCase());
|
|
4990
|
+
return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
|
|
4991
|
+
}, 0) +
|
|
4992
|
+
(visibleGroups.length - 1) * LG_GROUP_GAP;
|
|
4589
4993
|
|
|
4590
4994
|
let cx = (width - totalW) / 2;
|
|
4591
4995
|
|
|
4592
4996
|
// Legend container for data-legend-active attribute
|
|
4593
|
-
const legendContainer = mainSvg
|
|
4997
|
+
const legendContainer = mainSvg
|
|
4998
|
+
.append('g')
|
|
4594
4999
|
.attr('class', 'tl-tag-legend-container');
|
|
4595
5000
|
if (currentActiveGroup) {
|
|
4596
|
-
legendContainer.attr(
|
|
5001
|
+
legendContainer.attr(
|
|
5002
|
+
'data-legend-active',
|
|
5003
|
+
currentActiveGroup.toLowerCase()
|
|
5004
|
+
);
|
|
4597
5005
|
}
|
|
4598
5006
|
|
|
4599
5007
|
for (const lg of visibleGroups) {
|
|
4600
5008
|
const groupKey = lg.group.name.toLowerCase();
|
|
4601
|
-
const isActive =
|
|
5009
|
+
const isActive =
|
|
5010
|
+
viewMode ||
|
|
4602
5011
|
(currentActiveGroup != null &&
|
|
4603
5012
|
currentActiveGroup.toLowerCase() === groupKey);
|
|
4604
|
-
const isSwimActive =
|
|
5013
|
+
const isSwimActive =
|
|
5014
|
+
currentSwimlaneGroup != null &&
|
|
4605
5015
|
currentSwimlaneGroup.toLowerCase() === groupKey;
|
|
4606
5016
|
|
|
4607
5017
|
const pillLabel = lg.group.name;
|
|
4608
|
-
const pillWidth =
|
|
5018
|
+
const pillWidth =
|
|
5019
|
+
measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4609
5020
|
|
|
4610
5021
|
const gEl = legendContainer
|
|
4611
5022
|
.append('g')
|
|
@@ -4616,19 +5027,19 @@ export function renderTimeline(
|
|
|
4616
5027
|
.attr('data-legend-entry', '__group__');
|
|
4617
5028
|
|
|
4618
5029
|
if (!viewMode) {
|
|
4619
|
-
gEl
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
});
|
|
5030
|
+
gEl.style('cursor', 'pointer').on('click', () => {
|
|
5031
|
+
currentActiveGroup =
|
|
5032
|
+
currentActiveGroup === groupKey ? null : groupKey;
|
|
5033
|
+
drawLegend();
|
|
5034
|
+
recolorEvents();
|
|
5035
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
5036
|
+
});
|
|
4627
5037
|
}
|
|
4628
5038
|
|
|
4629
5039
|
// Outer capsule background (active only)
|
|
4630
5040
|
if (isActive) {
|
|
4631
|
-
gEl
|
|
5041
|
+
gEl
|
|
5042
|
+
.append('rect')
|
|
4632
5043
|
.attr('width', lg.expandedWidth)
|
|
4633
5044
|
.attr('height', LG_HEIGHT)
|
|
4634
5045
|
.attr('rx', LG_HEIGHT / 2)
|
|
@@ -4640,7 +5051,8 @@ export function renderTimeline(
|
|
|
4640
5051
|
const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
|
|
4641
5052
|
|
|
4642
5053
|
// Pill background
|
|
4643
|
-
gEl
|
|
5054
|
+
gEl
|
|
5055
|
+
.append('rect')
|
|
4644
5056
|
.attr('x', pillXOff)
|
|
4645
5057
|
.attr('y', pillYOff)
|
|
4646
5058
|
.attr('width', pillWidth)
|
|
@@ -4650,7 +5062,8 @@ export function renderTimeline(
|
|
|
4650
5062
|
|
|
4651
5063
|
// Active pill border
|
|
4652
5064
|
if (isActive) {
|
|
4653
|
-
gEl
|
|
5065
|
+
gEl
|
|
5066
|
+
.append('rect')
|
|
4654
5067
|
.attr('x', pillXOff)
|
|
4655
5068
|
.attr('y', pillYOff)
|
|
4656
5069
|
.attr('width', pillWidth)
|
|
@@ -4662,7 +5075,8 @@ export function renderTimeline(
|
|
|
4662
5075
|
}
|
|
4663
5076
|
|
|
4664
5077
|
// Pill text
|
|
4665
|
-
gEl
|
|
5078
|
+
gEl
|
|
5079
|
+
.append('text')
|
|
4666
5080
|
.attr('x', pillXOff + pillWidth / 2)
|
|
4667
5081
|
.attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
|
|
4668
5082
|
.attr('font-size', LG_PILL_FONT_SIZE)
|
|
@@ -4684,7 +5098,8 @@ export function renderTimeline(
|
|
|
4684
5098
|
.attr('data-swimlane-toggle', groupKey)
|
|
4685
5099
|
.on('click', (event: MouseEvent) => {
|
|
4686
5100
|
event.stopPropagation();
|
|
4687
|
-
currentSwimlaneGroup =
|
|
5101
|
+
currentSwimlaneGroup =
|
|
5102
|
+
currentSwimlaneGroup === groupKey ? null : groupKey;
|
|
4688
5103
|
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4689
5104
|
relayout();
|
|
4690
5105
|
});
|
|
@@ -4697,7 +5112,8 @@ export function renderTimeline(
|
|
|
4697
5112
|
const tagKey = lg.group.name.toLowerCase();
|
|
4698
5113
|
const tagVal = entry.value.toLowerCase();
|
|
4699
5114
|
|
|
4700
|
-
const entryG = gEl
|
|
5115
|
+
const entryG = gEl
|
|
5116
|
+
.append('g')
|
|
4701
5117
|
.attr('class', 'tl-tag-legend-entry')
|
|
4702
5118
|
.attr('data-tag-group', tagKey)
|
|
4703
5119
|
.attr('data-legend-entry', tagVal);
|
|
@@ -4708,18 +5124,24 @@ export function renderTimeline(
|
|
|
4708
5124
|
.on('mouseenter', (event: MouseEvent) => {
|
|
4709
5125
|
event.stopPropagation();
|
|
4710
5126
|
fadeToTagValue(mainG, tagKey, tagVal);
|
|
4711
|
-
mainSvg
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
5127
|
+
mainSvg
|
|
5128
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
5129
|
+
.each(function () {
|
|
5130
|
+
const el = d3Selection.select(this);
|
|
5131
|
+
const ev = el.attr('data-legend-entry');
|
|
5132
|
+
if (ev === '__group__') return;
|
|
5133
|
+
const eg = el.attr('data-tag-group');
|
|
5134
|
+
el.attr(
|
|
5135
|
+
'opacity',
|
|
5136
|
+
eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
|
|
5137
|
+
);
|
|
5138
|
+
});
|
|
4718
5139
|
})
|
|
4719
5140
|
.on('mouseleave', (event: MouseEvent) => {
|
|
4720
5141
|
event.stopPropagation();
|
|
4721
5142
|
fadeReset(mainG);
|
|
4722
|
-
mainSvg
|
|
5143
|
+
mainSvg
|
|
5144
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
4723
5145
|
.attr('opacity', 1);
|
|
4724
5146
|
})
|
|
4725
5147
|
.on('click', (event: MouseEvent) => {
|
|
@@ -4727,14 +5149,16 @@ export function renderTimeline(
|
|
|
4727
5149
|
});
|
|
4728
5150
|
}
|
|
4729
5151
|
|
|
4730
|
-
entryG
|
|
5152
|
+
entryG
|
|
5153
|
+
.append('circle')
|
|
4731
5154
|
.attr('cx', entryX + LG_DOT_R)
|
|
4732
5155
|
.attr('cy', LG_HEIGHT / 2)
|
|
4733
5156
|
.attr('r', LG_DOT_R)
|
|
4734
5157
|
.attr('fill', entry.color);
|
|
4735
5158
|
|
|
4736
5159
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4737
|
-
entryG
|
|
5160
|
+
entryG
|
|
5161
|
+
.append('text')
|
|
4738
5162
|
.attr('x', textX)
|
|
4739
5163
|
.attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
|
|
4740
5164
|
.attr('font-size', LG_ENTRY_FONT_SIZE)
|
|
@@ -4742,7 +5166,10 @@ export function renderTimeline(
|
|
|
4742
5166
|
.attr('fill', palette.textMuted)
|
|
4743
5167
|
.text(entry.value);
|
|
4744
5168
|
|
|
4745
|
-
entryX =
|
|
5169
|
+
entryX =
|
|
5170
|
+
textX +
|
|
5171
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
5172
|
+
LG_ENTRY_TRAIL;
|
|
4746
5173
|
}
|
|
4747
5174
|
}
|
|
4748
5175
|
|
|
@@ -4767,16 +5194,27 @@ export function renderTimeline(
|
|
|
4767
5194
|
let color: string;
|
|
4768
5195
|
if (colorTG) {
|
|
4769
5196
|
const tagColor = resolveTagColor(
|
|
4770
|
-
ev.metadata,
|
|
5197
|
+
ev.metadata,
|
|
5198
|
+
parsed.timelineTagGroups,
|
|
5199
|
+
colorTG
|
|
4771
5200
|
);
|
|
4772
|
-
color =
|
|
4773
|
-
|
|
5201
|
+
color =
|
|
5202
|
+
tagColor ??
|
|
5203
|
+
(ev.group && groupColorMap.has(ev.group)
|
|
5204
|
+
? groupColorMap.get(ev.group)!
|
|
5205
|
+
: textColor);
|
|
4774
5206
|
} else {
|
|
4775
|
-
color =
|
|
4776
|
-
|
|
5207
|
+
color =
|
|
5208
|
+
ev.group && groupColorMap.has(ev.group)
|
|
5209
|
+
? groupColorMap.get(ev.group)!
|
|
5210
|
+
: textColor;
|
|
4777
5211
|
}
|
|
4778
|
-
el.selectAll('rect')
|
|
4779
|
-
|
|
5212
|
+
el.selectAll('rect')
|
|
5213
|
+
.attr('fill', mix(color, bg, 30))
|
|
5214
|
+
.attr('stroke', color);
|
|
5215
|
+
el.selectAll('circle:not(.tl-event-point-outline)')
|
|
5216
|
+
.attr('fill', mix(color, bg, 30))
|
|
5217
|
+
.attr('stroke', color);
|
|
4780
5218
|
});
|
|
4781
5219
|
}
|
|
4782
5220
|
|
|
@@ -4833,7 +5271,14 @@ export function renderWordCloud(
|
|
|
4833
5271
|
|
|
4834
5272
|
const rotateFn = getRotateFn(cloudOptions.rotate);
|
|
4835
5273
|
|
|
4836
|
-
renderChartTitle(
|
|
5274
|
+
renderChartTitle(
|
|
5275
|
+
svg,
|
|
5276
|
+
title,
|
|
5277
|
+
parsed.titleLineNumber,
|
|
5278
|
+
width,
|
|
5279
|
+
textColor,
|
|
5280
|
+
onClickItem
|
|
5281
|
+
);
|
|
4837
5282
|
|
|
4838
5283
|
const g = svg
|
|
4839
5284
|
.append('g')
|
|
@@ -5140,7 +5585,8 @@ export function renderVenn(
|
|
|
5140
5585
|
const labelTextPad = 4;
|
|
5141
5586
|
|
|
5142
5587
|
for (let i = 0; i < n; i++) {
|
|
5143
|
-
const estimatedWidth =
|
|
5588
|
+
const estimatedWidth =
|
|
5589
|
+
vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
|
|
5144
5590
|
const dx = rawCircles[i].x - clusterCx;
|
|
5145
5591
|
const dy = rawCircles[i].y - clusterCy;
|
|
5146
5592
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
@@ -5167,13 +5613,27 @@ export function renderVenn(
|
|
|
5167
5613
|
const scaledR = circles[0].r;
|
|
5168
5614
|
|
|
5169
5615
|
// Suppress WebKit focus ring on interactive SVG elements
|
|
5170
|
-
svg
|
|
5616
|
+
svg
|
|
5617
|
+
.append('style')
|
|
5618
|
+
.text('circle:focus, circle:focus-visible { outline: none !important; }');
|
|
5171
5619
|
|
|
5172
5620
|
// Title
|
|
5173
|
-
renderChartTitle(
|
|
5621
|
+
renderChartTitle(
|
|
5622
|
+
svg,
|
|
5623
|
+
title,
|
|
5624
|
+
parsed.titleLineNumber,
|
|
5625
|
+
width,
|
|
5626
|
+
textColor,
|
|
5627
|
+
onClickItem
|
|
5628
|
+
);
|
|
5174
5629
|
|
|
5175
5630
|
// ── Semi-transparent filled circles (non-interactive) ──
|
|
5176
|
-
const circleEls: d3Selection.Selection<
|
|
5631
|
+
const circleEls: d3Selection.Selection<
|
|
5632
|
+
SVGCircleElement,
|
|
5633
|
+
unknown,
|
|
5634
|
+
null,
|
|
5635
|
+
undefined
|
|
5636
|
+
>[] = [];
|
|
5177
5637
|
const circleGroup = svg.append('g');
|
|
5178
5638
|
circles.forEach((c, i) => {
|
|
5179
5639
|
const el = circleGroup
|
|
@@ -5200,10 +5660,13 @@ export function renderVenn(
|
|
|
5200
5660
|
|
|
5201
5661
|
// Individual circle clipPaths
|
|
5202
5662
|
circles.forEach((c, i) => {
|
|
5203
|
-
defs
|
|
5663
|
+
defs
|
|
5664
|
+
.append('clipPath')
|
|
5204
5665
|
.attr('id', `vcp-${i}`)
|
|
5205
5666
|
.append('circle')
|
|
5206
|
-
.attr('cx', c.x)
|
|
5667
|
+
.attr('cx', c.x)
|
|
5668
|
+
.attr('cy', c.y)
|
|
5669
|
+
.attr('r', c.r);
|
|
5207
5670
|
});
|
|
5208
5671
|
|
|
5209
5672
|
// All region index-sets: exclusive then intersection subsets
|
|
@@ -5215,57 +5678,79 @@ export function renderVenn(
|
|
|
5215
5678
|
}
|
|
5216
5679
|
|
|
5217
5680
|
const overlayGroup = svg.append('g').style('pointer-events', 'none');
|
|
5218
|
-
const overlayEls = new Map<
|
|
5681
|
+
const overlayEls = new Map<
|
|
5682
|
+
string,
|
|
5683
|
+
d3Selection.Selection<SVGRectElement, unknown, null, undefined>
|
|
5684
|
+
>();
|
|
5219
5685
|
|
|
5220
5686
|
for (const idxs of regionIdxSets) {
|
|
5221
5687
|
const key = idxs.join('-');
|
|
5222
|
-
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5688
|
+
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5689
|
+
(j) => !idxs.includes(j)
|
|
5690
|
+
);
|
|
5223
5691
|
|
|
5224
5692
|
// Build nested clipPath for intersection of all idxs
|
|
5225
5693
|
let clipId = `vcp-${idxs[0]}`;
|
|
5226
5694
|
for (let k = 1; k < idxs.length; k++) {
|
|
5227
5695
|
const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
|
|
5228
5696
|
const ci = idxs[k];
|
|
5229
|
-
defs
|
|
5697
|
+
defs
|
|
5698
|
+
.append('clipPath')
|
|
5230
5699
|
.attr('id', nestedId)
|
|
5231
5700
|
.append('circle')
|
|
5232
|
-
.attr('cx', circles[ci].x)
|
|
5701
|
+
.attr('cx', circles[ci].x)
|
|
5702
|
+
.attr('cy', circles[ci].y)
|
|
5703
|
+
.attr('r', circles[ci].r)
|
|
5233
5704
|
.attr('clip-path', `url(#${clipId})`);
|
|
5234
5705
|
clipId = nestedId;
|
|
5235
5706
|
}
|
|
5236
5707
|
|
|
5237
5708
|
// Determine line number for this region (for editor sync)
|
|
5238
|
-
let regionLineNumber: number | null = null;
|
|
5709
|
+
let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
|
|
5239
5710
|
if (idxs.length === 1) {
|
|
5240
5711
|
regionLineNumber = vennSets[idxs[0]].lineNumber;
|
|
5241
5712
|
} else {
|
|
5242
|
-
const sortedNames = idxs.map(i => vennSets[i].name).sort();
|
|
5713
|
+
const sortedNames = idxs.map((i) => vennSets[i].name).sort();
|
|
5243
5714
|
const ov = vennOverlaps.find(
|
|
5244
|
-
(o) =>
|
|
5715
|
+
(o) =>
|
|
5716
|
+
o.sets.length === sortedNames.length &&
|
|
5717
|
+
o.sets.every((s, k) => s === sortedNames[k])
|
|
5245
5718
|
);
|
|
5246
5719
|
regionLineNumber = ov?.lineNumber ?? null;
|
|
5247
5720
|
}
|
|
5248
5721
|
|
|
5249
|
-
const el = overlayGroup
|
|
5250
|
-
.
|
|
5251
|
-
.attr('
|
|
5722
|
+
const el = overlayGroup
|
|
5723
|
+
.append('rect')
|
|
5724
|
+
.attr('x', 0)
|
|
5725
|
+
.attr('y', 0)
|
|
5726
|
+
.attr('width', width)
|
|
5727
|
+
.attr('height', height)
|
|
5252
5728
|
.attr('fill', 'white')
|
|
5253
5729
|
.attr('fill-opacity', 0)
|
|
5254
5730
|
.attr('class', 'venn-region-overlay')
|
|
5255
|
-
.attr(
|
|
5731
|
+
.attr(
|
|
5732
|
+
'data-line-number',
|
|
5733
|
+
regionLineNumber != null ? String(regionLineNumber) : '0'
|
|
5734
|
+
)
|
|
5256
5735
|
.attr('clip-path', `url(#${clipId})`);
|
|
5257
5736
|
|
|
5258
5737
|
if (excluded.length > 0) {
|
|
5259
5738
|
// Mask subtracts excluded circles so only the exact region shape highlights
|
|
5260
5739
|
const maskId = `vvm-${key}`;
|
|
5261
5740
|
const mask = defs.append('mask').attr('id', maskId);
|
|
5262
|
-
mask
|
|
5263
|
-
.
|
|
5264
|
-
.attr('
|
|
5741
|
+
mask
|
|
5742
|
+
.append('rect')
|
|
5743
|
+
.attr('x', 0)
|
|
5744
|
+
.attr('y', 0)
|
|
5745
|
+
.attr('width', width)
|
|
5746
|
+
.attr('height', height)
|
|
5265
5747
|
.attr('fill', 'white');
|
|
5266
5748
|
for (const j of excluded) {
|
|
5267
|
-
mask
|
|
5268
|
-
.
|
|
5749
|
+
mask
|
|
5750
|
+
.append('circle')
|
|
5751
|
+
.attr('cx', circles[j].x)
|
|
5752
|
+
.attr('cy', circles[j].y)
|
|
5753
|
+
.attr('r', circles[j].r)
|
|
5269
5754
|
.attr('fill', 'black');
|
|
5270
5755
|
}
|
|
5271
5756
|
el.attr('mask', `url(#${maskId})`);
|
|
@@ -5276,10 +5761,12 @@ export function renderVenn(
|
|
|
5276
5761
|
|
|
5277
5762
|
const showRegionOverlay = (idxs: number[]) => {
|
|
5278
5763
|
const key = [...idxs].sort((a, b) => a - b).join('-');
|
|
5279
|
-
overlayEls.forEach((el, k) =>
|
|
5764
|
+
overlayEls.forEach((el, k) =>
|
|
5765
|
+
el.attr('fill-opacity', k === key ? 0 : 0.55)
|
|
5766
|
+
);
|
|
5280
5767
|
};
|
|
5281
5768
|
const hideAllOverlays = () => {
|
|
5282
|
-
overlayEls.forEach(el => el.attr('fill-opacity', 0));
|
|
5769
|
+
overlayEls.forEach((el) => el.attr('fill-opacity', 0));
|
|
5283
5770
|
};
|
|
5284
5771
|
|
|
5285
5772
|
// ── Labels ──
|
|
@@ -5288,7 +5775,9 @@ export function renderVenn(
|
|
|
5288
5775
|
|
|
5289
5776
|
function exclusiveHSpan(px: number, py: number, ci: number): number {
|
|
5290
5777
|
const dy = py - circles[ci].y;
|
|
5291
|
-
const halfChord = Math.sqrt(
|
|
5778
|
+
const halfChord = Math.sqrt(
|
|
5779
|
+
Math.max(0, circles[ci].r * circles[ci].r - dy * dy)
|
|
5780
|
+
);
|
|
5292
5781
|
let left = circles[ci].x - halfChord;
|
|
5293
5782
|
let right = circles[ci].x + halfChord;
|
|
5294
5783
|
for (let j = 0; j < n; j++) {
|
|
@@ -5319,11 +5808,14 @@ export function renderVenn(
|
|
|
5319
5808
|
const centroid = regionCentroid(circles, inside);
|
|
5320
5809
|
|
|
5321
5810
|
const availW = exclusiveHSpan(centroid.x, centroid.y, i);
|
|
5322
|
-
const fitFont = Math.min(
|
|
5323
|
-
|
|
5811
|
+
const fitFont = Math.min(
|
|
5812
|
+
MAX_FONT,
|
|
5813
|
+
Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO))
|
|
5814
|
+
);
|
|
5324
5815
|
const estTextW = text.length * CH_RATIO * fitFont;
|
|
5325
5816
|
|
|
5326
|
-
const fitsInside =
|
|
5817
|
+
const fitsInside =
|
|
5818
|
+
estTextW + INTERNAL_PAD * 2 < availW &&
|
|
5327
5819
|
pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
|
|
5328
5820
|
pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
|
|
5329
5821
|
|
|
@@ -5342,7 +5834,13 @@ export function renderVenn(
|
|
|
5342
5834
|
let dx = c.x - gcx;
|
|
5343
5835
|
let dy = c.y - gcy;
|
|
5344
5836
|
const mag = Math.sqrt(dx * dx + dy * dy);
|
|
5345
|
-
if (mag < 1e-6) {
|
|
5837
|
+
if (mag < 1e-6) {
|
|
5838
|
+
dx = 1;
|
|
5839
|
+
dy = 0;
|
|
5840
|
+
} else {
|
|
5841
|
+
dx /= mag;
|
|
5842
|
+
dy /= mag;
|
|
5843
|
+
}
|
|
5346
5844
|
|
|
5347
5845
|
const exitX = c.x + dx * c.r;
|
|
5348
5846
|
const exitY = c.y + dy * c.r;
|
|
@@ -5353,8 +5851,10 @@ export function renderVenn(
|
|
|
5353
5851
|
|
|
5354
5852
|
labelGroup
|
|
5355
5853
|
.append('line')
|
|
5356
|
-
.attr('x1', edgeX)
|
|
5357
|
-
.attr('
|
|
5854
|
+
.attr('x1', edgeX)
|
|
5855
|
+
.attr('y1', edgeY)
|
|
5856
|
+
.attr('x2', stubEndX)
|
|
5857
|
+
.attr('y2', stubEndY)
|
|
5358
5858
|
.attr('stroke', textColor)
|
|
5359
5859
|
.attr('stroke-width', 1);
|
|
5360
5860
|
|
|
@@ -5381,7 +5881,8 @@ export function renderVenn(
|
|
|
5381
5881
|
|
|
5382
5882
|
// ── Overlap labels (inline at region centroid) ──
|
|
5383
5883
|
function overlapHSpan(py: number, idxs: number[]): number {
|
|
5384
|
-
let left = -Infinity,
|
|
5884
|
+
let left = -Infinity,
|
|
5885
|
+
right = Infinity;
|
|
5385
5886
|
for (const ci of idxs) {
|
|
5386
5887
|
const dy = py - circles[ci].y;
|
|
5387
5888
|
if (Math.abs(dy) >= circles[ci].r) return 0;
|
|
@@ -5411,8 +5912,13 @@ export function renderVenn(
|
|
|
5411
5912
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5412
5913
|
const centroid = regionCentroid(circles, inside);
|
|
5413
5914
|
const availW = overlapHSpan(centroid.y, idxs);
|
|
5414
|
-
const fitFont = Math.min(
|
|
5415
|
-
|
|
5915
|
+
const fitFont = Math.min(
|
|
5916
|
+
MAX_FONT,
|
|
5917
|
+
Math.max(
|
|
5918
|
+
MIN_FONT,
|
|
5919
|
+
(availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
|
|
5920
|
+
)
|
|
5921
|
+
);
|
|
5416
5922
|
labelGroup
|
|
5417
5923
|
.append('text')
|
|
5418
5924
|
.attr('x', centroid.x)
|
|
@@ -5441,11 +5947,16 @@ export function renderVenn(
|
|
|
5441
5947
|
.attr('data-line-number', String(vennSets[i].lineNumber))
|
|
5442
5948
|
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
5443
5949
|
.style('outline', 'none')
|
|
5444
|
-
.on('mouseenter', () => {
|
|
5445
|
-
|
|
5950
|
+
.on('mouseenter', () => {
|
|
5951
|
+
showRegionOverlay([i]);
|
|
5952
|
+
})
|
|
5953
|
+
.on('mouseleave', () => {
|
|
5954
|
+
hideAllOverlays();
|
|
5955
|
+
})
|
|
5446
5956
|
.on('click', function () {
|
|
5447
5957
|
(this as SVGElement).blur?.();
|
|
5448
|
-
if (onClickItem && vennSets[i].lineNumber)
|
|
5958
|
+
if (onClickItem && vennSets[i].lineNumber)
|
|
5959
|
+
onClickItem(vennSets[i].lineNumber);
|
|
5449
5960
|
});
|
|
5450
5961
|
});
|
|
5451
5962
|
|
|
@@ -5454,14 +5965,23 @@ export function renderVenn(
|
|
|
5454
5965
|
|
|
5455
5966
|
const subsets: { idxs: number[]; sets: string[] }[] = [];
|
|
5456
5967
|
if (n === 2) {
|
|
5457
|
-
subsets.push({
|
|
5968
|
+
subsets.push({
|
|
5969
|
+
idxs: [0, 1],
|
|
5970
|
+
sets: [vennSets[0].name, vennSets[1].name].sort(),
|
|
5971
|
+
});
|
|
5458
5972
|
} else {
|
|
5459
5973
|
for (let a = 0; a < n; a++) {
|
|
5460
5974
|
for (let b = a + 1; b < n; b++) {
|
|
5461
|
-
subsets.push({
|
|
5975
|
+
subsets.push({
|
|
5976
|
+
idxs: [a, b],
|
|
5977
|
+
sets: [vennSets[a].name, vennSets[b].name].sort(),
|
|
5978
|
+
});
|
|
5462
5979
|
}
|
|
5463
5980
|
}
|
|
5464
|
-
subsets.push({
|
|
5981
|
+
subsets.push({
|
|
5982
|
+
idxs: [0, 1, 2],
|
|
5983
|
+
sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort(),
|
|
5984
|
+
});
|
|
5465
5985
|
}
|
|
5466
5986
|
|
|
5467
5987
|
for (const subset of subsets) {
|
|
@@ -5469,7 +5989,8 @@ export function renderVenn(
|
|
|
5469
5989
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5470
5990
|
const centroid = regionCentroid(circles, inside);
|
|
5471
5991
|
const declaredOv = vennOverlaps.find(
|
|
5472
|
-
(ov) =>
|
|
5992
|
+
(ov) =>
|
|
5993
|
+
ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
|
|
5473
5994
|
);
|
|
5474
5995
|
hoverGroup
|
|
5475
5996
|
.append('circle')
|
|
@@ -5482,8 +6003,12 @@ export function renderVenn(
|
|
|
5482
6003
|
.attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
|
|
5483
6004
|
.style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
|
|
5484
6005
|
.style('outline', 'none')
|
|
5485
|
-
.on('mouseenter', () => {
|
|
5486
|
-
|
|
6006
|
+
.on('mouseenter', () => {
|
|
6007
|
+
showRegionOverlay(idxs);
|
|
6008
|
+
})
|
|
6009
|
+
.on('mouseleave', () => {
|
|
6010
|
+
hideAllOverlays();
|
|
6011
|
+
})
|
|
5487
6012
|
.on('click', function () {
|
|
5488
6013
|
(this as SVGElement).blur?.();
|
|
5489
6014
|
if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
|
|
@@ -5542,7 +6067,12 @@ export function renderQuadrant(
|
|
|
5542
6067
|
// Margins
|
|
5543
6068
|
const hasXAxis = !!quadrantXAxis;
|
|
5544
6069
|
const hasYAxis = !!quadrantYAxis;
|
|
5545
|
-
const margin = {
|
|
6070
|
+
const margin = {
|
|
6071
|
+
top: title ? 60 : 30,
|
|
6072
|
+
right: 30,
|
|
6073
|
+
bottom: hasXAxis ? 70 : 40,
|
|
6074
|
+
left: hasYAxis ? 80 : 40,
|
|
6075
|
+
};
|
|
5546
6076
|
const chartWidth = width - margin.left - margin.right;
|
|
5547
6077
|
const chartHeight = height - margin.top - margin.bottom;
|
|
5548
6078
|
|
|
@@ -5554,7 +6084,14 @@ export function renderQuadrant(
|
|
|
5554
6084
|
const tooltip = createTooltip(container, palette, isDark);
|
|
5555
6085
|
|
|
5556
6086
|
// Title
|
|
5557
|
-
renderChartTitle(
|
|
6087
|
+
renderChartTitle(
|
|
6088
|
+
svg,
|
|
6089
|
+
title,
|
|
6090
|
+
quadrantTitleLineNumber,
|
|
6091
|
+
width,
|
|
6092
|
+
textColor,
|
|
6093
|
+
onClickItem
|
|
6094
|
+
);
|
|
5558
6095
|
|
|
5559
6096
|
// Chart group (translated by margins)
|
|
5560
6097
|
const chartG = svg
|
|
@@ -5565,12 +6102,21 @@ export function renderQuadrant(
|
|
|
5565
6102
|
const mixHex = (a: string, b: string, pct: number): string => {
|
|
5566
6103
|
const parse = (h: string) => {
|
|
5567
6104
|
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 [
|
|
6105
|
+
const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
|
|
6106
|
+
return [
|
|
6107
|
+
parseInt(f.substring(0, 2), 16),
|
|
6108
|
+
parseInt(f.substring(2, 4), 16),
|
|
6109
|
+
parseInt(f.substring(4, 6), 16),
|
|
6110
|
+
];
|
|
5570
6111
|
};
|
|
5571
|
-
const [ar,ag,ab] = parse(a),
|
|
5572
|
-
|
|
5573
|
-
|
|
6112
|
+
const [ar, ag, ab] = parse(a),
|
|
6113
|
+
[br, bg, bb] = parse(b),
|
|
6114
|
+
t = pct / 100;
|
|
6115
|
+
const c = (x: number, y: number) =>
|
|
6116
|
+
Math.round(x * t + y * (1 - t))
|
|
6117
|
+
.toString(16)
|
|
6118
|
+
.padStart(2, '0');
|
|
6119
|
+
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
5574
6120
|
};
|
|
5575
6121
|
|
|
5576
6122
|
const bg = isDark ? palette.surface : palette.bg;
|
|
@@ -5687,7 +6233,11 @@ export function renderQuadrant(
|
|
|
5687
6233
|
fontSize: number;
|
|
5688
6234
|
}
|
|
5689
6235
|
|
|
5690
|
-
const quadrantLabelLayout = (
|
|
6236
|
+
const quadrantLabelLayout = (
|
|
6237
|
+
text: string,
|
|
6238
|
+
qw: number,
|
|
6239
|
+
qh: number
|
|
6240
|
+
): QuadrantLabelLayout => {
|
|
5691
6241
|
const availW = qw - LABEL_PAD;
|
|
5692
6242
|
const availH = qh - LABEL_PAD;
|
|
5693
6243
|
const words = text.split(/\s+/);
|
|
@@ -5695,7 +6245,10 @@ export function renderQuadrant(
|
|
|
5695
6245
|
// Try single line first
|
|
5696
6246
|
if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
|
|
5697
6247
|
const fs = Math.min(LABEL_MAX_FONT, availH);
|
|
5698
|
-
return {
|
|
6248
|
+
return {
|
|
6249
|
+
lines: [text],
|
|
6250
|
+
fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
|
|
6251
|
+
};
|
|
5699
6252
|
}
|
|
5700
6253
|
|
|
5701
6254
|
// Try wrapping into 2+ lines: greedily pack words so each line fits availW
|
|
@@ -5742,7 +6295,10 @@ export function renderQuadrant(
|
|
|
5742
6295
|
const qh = chartHeight / 2;
|
|
5743
6296
|
const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
|
|
5744
6297
|
const labelLayouts = new Map(
|
|
5745
|
-
quadrantDefsWithLabel.map((d) => [
|
|
6298
|
+
quadrantDefsWithLabel.map((d) => [
|
|
6299
|
+
d.label!.text,
|
|
6300
|
+
quadrantLabelLayout(d.label!.text, qw, qh),
|
|
6301
|
+
])
|
|
5746
6302
|
);
|
|
5747
6303
|
|
|
5748
6304
|
const quadrantLabelTexts = chartG
|
|
@@ -5807,7 +6363,10 @@ export function renderQuadrant(
|
|
|
5807
6363
|
.attr('text-anchor', 'middle')
|
|
5808
6364
|
.attr('fill', textColor)
|
|
5809
6365
|
.attr('font-size', '18px')
|
|
5810
|
-
.attr(
|
|
6366
|
+
.attr(
|
|
6367
|
+
'data-line-number',
|
|
6368
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6369
|
+
)
|
|
5811
6370
|
.style(
|
|
5812
6371
|
'cursor',
|
|
5813
6372
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5823,7 +6382,10 @@ export function renderQuadrant(
|
|
|
5823
6382
|
.attr('text-anchor', 'middle')
|
|
5824
6383
|
.attr('fill', textColor)
|
|
5825
6384
|
.attr('font-size', '18px')
|
|
5826
|
-
.attr(
|
|
6385
|
+
.attr(
|
|
6386
|
+
'data-line-number',
|
|
6387
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6388
|
+
)
|
|
5827
6389
|
.style(
|
|
5828
6390
|
'cursor',
|
|
5829
6391
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5859,7 +6421,10 @@ export function renderQuadrant(
|
|
|
5859
6421
|
.attr('fill', textColor)
|
|
5860
6422
|
.attr('font-size', '18px')
|
|
5861
6423
|
.attr('transform', `rotate(-90, 22, ${yMidBottom})`)
|
|
5862
|
-
.attr(
|
|
6424
|
+
.attr(
|
|
6425
|
+
'data-line-number',
|
|
6426
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6427
|
+
)
|
|
5863
6428
|
.style(
|
|
5864
6429
|
'cursor',
|
|
5865
6430
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5876,7 +6441,10 @@ export function renderQuadrant(
|
|
|
5876
6441
|
.attr('fill', textColor)
|
|
5877
6442
|
.attr('font-size', '18px')
|
|
5878
6443
|
.attr('transform', `rotate(-90, 22, ${yMidTop})`)
|
|
5879
|
-
.attr(
|
|
6444
|
+
.attr(
|
|
6445
|
+
'data-line-number',
|
|
6446
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6447
|
+
)
|
|
5880
6448
|
.style(
|
|
5881
6449
|
'cursor',
|
|
5882
6450
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5935,7 +6503,9 @@ export function renderQuadrant(
|
|
|
5935
6503
|
const pointColor =
|
|
5936
6504
|
quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
|
|
5937
6505
|
|
|
5938
|
-
const pointG = pointsG
|
|
6506
|
+
const pointG = pointsG
|
|
6507
|
+
.append('g')
|
|
6508
|
+
.attr('class', 'point-group')
|
|
5939
6509
|
.attr('data-line-number', String(point.lineNumber));
|
|
5940
6510
|
|
|
5941
6511
|
// Circle with white fill and colored border for visibility on opaque quadrants
|
|
@@ -6024,7 +6594,10 @@ const EXPORT_HEIGHT = 800;
|
|
|
6024
6594
|
/**
|
|
6025
6595
|
* Resolves the palette for export, falling back to Nord light/dark.
|
|
6026
6596
|
*/
|
|
6027
|
-
async function resolveExportPalette(
|
|
6597
|
+
async function resolveExportPalette(
|
|
6598
|
+
theme: string,
|
|
6599
|
+
palette?: PaletteColors
|
|
6600
|
+
): Promise<PaletteColors> {
|
|
6028
6601
|
if (palette) return palette;
|
|
6029
6602
|
const { getPalette } = await import('./palettes');
|
|
6030
6603
|
return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
|
|
@@ -6086,7 +6659,13 @@ export async function renderForExport(
|
|
|
6086
6659
|
hiddenAttributes?: Set<string>;
|
|
6087
6660
|
swimlaneTagGroup?: string | null;
|
|
6088
6661
|
},
|
|
6089
|
-
options?: {
|
|
6662
|
+
options?: {
|
|
6663
|
+
branding?: boolean;
|
|
6664
|
+
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
6665
|
+
c4System?: string;
|
|
6666
|
+
c4Container?: string;
|
|
6667
|
+
tagGroup?: string;
|
|
6668
|
+
}
|
|
6090
6669
|
): Promise<string> {
|
|
6091
6670
|
// Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
|
|
6092
6671
|
const { parseDgmoChartType } = await import('./dgmo-router');
|
|
@@ -6106,7 +6685,8 @@ export async function renderForExport(
|
|
|
6106
6685
|
|
|
6107
6686
|
// Apply interactive collapse state when provided
|
|
6108
6687
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6109
|
-
const activeTagGroup =
|
|
6688
|
+
const activeTagGroup =
|
|
6689
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6110
6690
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6111
6691
|
|
|
6112
6692
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6128,7 +6708,17 @@ export async function renderForExport(
|
|
|
6128
6708
|
const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
|
|
6129
6709
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6130
6710
|
|
|
6131
|
-
renderOrg(
|
|
6711
|
+
renderOrg(
|
|
6712
|
+
container,
|
|
6713
|
+
effectiveParsed,
|
|
6714
|
+
orgLayout,
|
|
6715
|
+
effectivePalette,
|
|
6716
|
+
isDark,
|
|
6717
|
+
undefined,
|
|
6718
|
+
{ width: exportWidth, height: exportHeight },
|
|
6719
|
+
activeTagGroup,
|
|
6720
|
+
hiddenAttributes
|
|
6721
|
+
);
|
|
6132
6722
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6133
6723
|
}
|
|
6134
6724
|
|
|
@@ -6146,7 +6736,8 @@ export async function renderForExport(
|
|
|
6146
6736
|
|
|
6147
6737
|
// Apply interactive collapse state when provided
|
|
6148
6738
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6149
|
-
const activeTagGroup =
|
|
6739
|
+
const activeTagGroup =
|
|
6740
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6150
6741
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6151
6742
|
|
|
6152
6743
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6159,7 +6750,7 @@ export async function renderForExport(
|
|
|
6159
6750
|
hiddenCounts.size > 0 ? hiddenCounts : undefined,
|
|
6160
6751
|
activeTagGroup,
|
|
6161
6752
|
hiddenAttributes,
|
|
6162
|
-
true
|
|
6753
|
+
true
|
|
6163
6754
|
);
|
|
6164
6755
|
|
|
6165
6756
|
const PADDING = 20;
|
|
@@ -6168,7 +6759,17 @@ export async function renderForExport(
|
|
|
6168
6759
|
const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
|
|
6169
6760
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6170
6761
|
|
|
6171
|
-
renderSitemap(
|
|
6762
|
+
renderSitemap(
|
|
6763
|
+
container,
|
|
6764
|
+
effectiveParsed,
|
|
6765
|
+
sitemapLayout,
|
|
6766
|
+
effectivePalette,
|
|
6767
|
+
isDark,
|
|
6768
|
+
undefined,
|
|
6769
|
+
{ width: exportWidth, height: exportHeight },
|
|
6770
|
+
activeTagGroup,
|
|
6771
|
+
hiddenAttributes
|
|
6772
|
+
);
|
|
6172
6773
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6173
6774
|
}
|
|
6174
6775
|
|
|
@@ -6186,7 +6787,15 @@ export async function renderForExport(
|
|
|
6186
6787
|
container.style.left = '-9999px';
|
|
6187
6788
|
document.body.appendChild(container);
|
|
6188
6789
|
|
|
6189
|
-
renderKanban(
|
|
6790
|
+
renderKanban(
|
|
6791
|
+
container,
|
|
6792
|
+
kanbanParsed,
|
|
6793
|
+
effectivePalette,
|
|
6794
|
+
theme === 'dark',
|
|
6795
|
+
undefined,
|
|
6796
|
+
undefined,
|
|
6797
|
+
options?.tagGroup
|
|
6798
|
+
);
|
|
6190
6799
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6191
6800
|
}
|
|
6192
6801
|
|
|
@@ -6206,7 +6815,15 @@ export async function renderForExport(
|
|
|
6206
6815
|
const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
|
|
6207
6816
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6208
6817
|
|
|
6209
|
-
renderClassDiagram(
|
|
6818
|
+
renderClassDiagram(
|
|
6819
|
+
container,
|
|
6820
|
+
classParsed,
|
|
6821
|
+
classLayout,
|
|
6822
|
+
effectivePalette,
|
|
6823
|
+
theme === 'dark',
|
|
6824
|
+
undefined,
|
|
6825
|
+
{ width: exportWidth, height: exportHeight }
|
|
6826
|
+
);
|
|
6210
6827
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6211
6828
|
}
|
|
6212
6829
|
|
|
@@ -6226,14 +6843,26 @@ export async function renderForExport(
|
|
|
6226
6843
|
const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
|
|
6227
6844
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6228
6845
|
|
|
6229
|
-
renderERDiagram(
|
|
6846
|
+
renderERDiagram(
|
|
6847
|
+
container,
|
|
6848
|
+
erParsed,
|
|
6849
|
+
erLayout,
|
|
6850
|
+
effectivePalette,
|
|
6851
|
+
theme === 'dark',
|
|
6852
|
+
undefined,
|
|
6853
|
+
{ width: exportWidth, height: exportHeight },
|
|
6854
|
+
options?.tagGroup
|
|
6855
|
+
);
|
|
6230
6856
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6231
6857
|
}
|
|
6232
6858
|
|
|
6233
6859
|
if (detectedType === 'initiative-status') {
|
|
6234
|
-
const { parseInitiativeStatus } =
|
|
6235
|
-
|
|
6236
|
-
const {
|
|
6860
|
+
const { parseInitiativeStatus } =
|
|
6861
|
+
await import('./initiative-status/parser');
|
|
6862
|
+
const { layoutInitiativeStatus } =
|
|
6863
|
+
await import('./initiative-status/layout');
|
|
6864
|
+
const { renderInitiativeStatus } =
|
|
6865
|
+
await import('./initiative-status/renderer');
|
|
6237
6866
|
|
|
6238
6867
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6239
6868
|
const isParsed = parseInitiativeStatus(content);
|
|
@@ -6246,14 +6875,27 @@ export async function renderForExport(
|
|
|
6246
6875
|
const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
|
|
6247
6876
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6248
6877
|
|
|
6249
|
-
renderInitiativeStatus(
|
|
6878
|
+
renderInitiativeStatus(
|
|
6879
|
+
container,
|
|
6880
|
+
isParsed,
|
|
6881
|
+
isLayout,
|
|
6882
|
+
effectivePalette,
|
|
6883
|
+
theme === 'dark',
|
|
6884
|
+
{ exportDims: { width: exportWidth, height: exportHeight } }
|
|
6885
|
+
);
|
|
6250
6886
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6251
6887
|
}
|
|
6252
6888
|
|
|
6253
6889
|
if (detectedType === 'c4') {
|
|
6254
6890
|
const { parseC4 } = await import('./c4/parser');
|
|
6255
|
-
const {
|
|
6256
|
-
|
|
6891
|
+
const {
|
|
6892
|
+
layoutC4Context,
|
|
6893
|
+
layoutC4Containers,
|
|
6894
|
+
layoutC4Components,
|
|
6895
|
+
layoutC4Deployment,
|
|
6896
|
+
} = await import('./c4/layout');
|
|
6897
|
+
const { renderC4Context, renderC4Containers } =
|
|
6898
|
+
await import('./c4/renderer');
|
|
6257
6899
|
|
|
6258
6900
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6259
6901
|
const c4Parsed = parseC4(content, effectivePalette);
|
|
@@ -6264,13 +6906,14 @@ export async function renderForExport(
|
|
|
6264
6906
|
const c4System = options?.c4System;
|
|
6265
6907
|
const c4Container = options?.c4Container;
|
|
6266
6908
|
|
|
6267
|
-
const c4Layout =
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6909
|
+
const c4Layout =
|
|
6910
|
+
c4Level === 'deployment'
|
|
6911
|
+
? layoutC4Deployment(c4Parsed)
|
|
6912
|
+
: c4Level === 'components' && c4System && c4Container
|
|
6913
|
+
? layoutC4Components(c4Parsed, c4System, c4Container)
|
|
6914
|
+
: c4Level === 'containers' && c4System
|
|
6915
|
+
? layoutC4Containers(c4Parsed, c4System)
|
|
6916
|
+
: layoutC4Context(c4Parsed);
|
|
6274
6917
|
|
|
6275
6918
|
if (c4Layout.nodes.length === 0) return '';
|
|
6276
6919
|
|
|
@@ -6280,11 +6923,23 @@ export async function renderForExport(
|
|
|
6280
6923
|
const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
|
|
6281
6924
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6282
6925
|
|
|
6283
|
-
const renderFn =
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6926
|
+
const renderFn =
|
|
6927
|
+
c4Level === 'deployment' ||
|
|
6928
|
+
(c4Level === 'components' && c4System && c4Container) ||
|
|
6929
|
+
(c4Level === 'containers' && c4System)
|
|
6930
|
+
? renderC4Containers
|
|
6931
|
+
: renderC4Context;
|
|
6932
|
+
|
|
6933
|
+
renderFn(
|
|
6934
|
+
container,
|
|
6935
|
+
c4Parsed,
|
|
6936
|
+
c4Layout,
|
|
6937
|
+
effectivePalette,
|
|
6938
|
+
theme === 'dark',
|
|
6939
|
+
undefined,
|
|
6940
|
+
{ width: exportWidth, height: exportHeight },
|
|
6941
|
+
options?.tagGroup
|
|
6942
|
+
);
|
|
6288
6943
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6289
6944
|
}
|
|
6290
6945
|
|
|
@@ -6300,7 +6955,15 @@ export async function renderForExport(
|
|
|
6300
6955
|
const layout = layoutGraph(fcParsed);
|
|
6301
6956
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6302
6957
|
|
|
6303
|
-
renderFlowchart(
|
|
6958
|
+
renderFlowchart(
|
|
6959
|
+
container,
|
|
6960
|
+
fcParsed,
|
|
6961
|
+
layout,
|
|
6962
|
+
effectivePalette,
|
|
6963
|
+
theme === 'dark',
|
|
6964
|
+
undefined,
|
|
6965
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
6966
|
+
);
|
|
6304
6967
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6305
6968
|
}
|
|
6306
6969
|
|
|
@@ -6308,7 +6971,8 @@ export async function renderForExport(
|
|
|
6308
6971
|
const { parseInfra } = await import('./infra/parser');
|
|
6309
6972
|
const { computeInfra } = await import('./infra/compute');
|
|
6310
6973
|
const { layoutInfra } = await import('./infra/layout');
|
|
6311
|
-
const { renderInfra, computeInfraLegendGroups } =
|
|
6974
|
+
const { renderInfra, computeInfraLegendGroups } =
|
|
6975
|
+
await import('./infra/renderer');
|
|
6312
6976
|
|
|
6313
6977
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6314
6978
|
const infraParsed = parseInfra(content);
|
|
@@ -6319,13 +6983,30 @@ export async function renderForExport(
|
|
|
6319
6983
|
const activeTagGroup = options?.tagGroup ?? null;
|
|
6320
6984
|
|
|
6321
6985
|
const titleOffset = infraParsed.title ? 40 : 0;
|
|
6322
|
-
const legendGroups = computeInfraLegendGroups(
|
|
6986
|
+
const legendGroups = computeInfraLegendGroups(
|
|
6987
|
+
infraLayout.nodes,
|
|
6988
|
+
infraParsed.tagGroups,
|
|
6989
|
+
effectivePalette
|
|
6990
|
+
);
|
|
6323
6991
|
const legendOffset = legendGroups.length > 0 ? 28 : 0;
|
|
6324
6992
|
const exportWidth = infraLayout.width;
|
|
6325
6993
|
const exportHeight = infraLayout.height + titleOffset + legendOffset;
|
|
6326
6994
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6327
6995
|
|
|
6328
|
-
renderInfra(
|
|
6996
|
+
renderInfra(
|
|
6997
|
+
container,
|
|
6998
|
+
infraLayout,
|
|
6999
|
+
effectivePalette,
|
|
7000
|
+
theme === 'dark',
|
|
7001
|
+
infraParsed.title,
|
|
7002
|
+
infraParsed.titleLineNumber,
|
|
7003
|
+
infraParsed.tagGroups,
|
|
7004
|
+
activeTagGroup,
|
|
7005
|
+
false,
|
|
7006
|
+
null,
|
|
7007
|
+
null,
|
|
7008
|
+
true
|
|
7009
|
+
);
|
|
6329
7010
|
// Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
|
|
6330
7011
|
const infraSvg = container.querySelector('svg');
|
|
6331
7012
|
if (infraSvg) {
|
|
@@ -6349,7 +7030,14 @@ export async function renderForExport(
|
|
|
6349
7030
|
const EXPORT_H = 800;
|
|
6350
7031
|
const container = createExportContainer(EXPORT_W, EXPORT_H);
|
|
6351
7032
|
|
|
6352
|
-
renderGantt(
|
|
7033
|
+
renderGantt(
|
|
7034
|
+
container,
|
|
7035
|
+
resolved,
|
|
7036
|
+
effectivePalette,
|
|
7037
|
+
theme === 'dark',
|
|
7038
|
+
undefined,
|
|
7039
|
+
{ width: EXPORT_W, height: EXPORT_H }
|
|
7040
|
+
);
|
|
6353
7041
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6354
7042
|
}
|
|
6355
7043
|
|
|
@@ -6365,7 +7053,15 @@ export async function renderForExport(
|
|
|
6365
7053
|
const layout = layoutGraph(stateParsed);
|
|
6366
7054
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6367
7055
|
|
|
6368
|
-
renderState(
|
|
7056
|
+
renderState(
|
|
7057
|
+
container,
|
|
7058
|
+
stateParsed,
|
|
7059
|
+
layout,
|
|
7060
|
+
effectivePalette,
|
|
7061
|
+
theme === 'dark',
|
|
7062
|
+
undefined,
|
|
7063
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
7064
|
+
);
|
|
6369
7065
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6370
7066
|
}
|
|
6371
7067
|
|
|
@@ -6391,30 +7087,75 @@ export async function renderForExport(
|
|
|
6391
7087
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6392
7088
|
const isDark = theme === 'dark';
|
|
6393
7089
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6394
|
-
const dims: D3ExportDimensions = {
|
|
7090
|
+
const dims: D3ExportDimensions = {
|
|
7091
|
+
width: EXPORT_WIDTH,
|
|
7092
|
+
height: EXPORT_HEIGHT,
|
|
7093
|
+
};
|
|
6395
7094
|
|
|
6396
7095
|
if (parsed.type === 'sequence') {
|
|
6397
7096
|
const { parseSequenceDgmo } = await import('./sequence/parser');
|
|
6398
7097
|
const { renderSequenceDiagram } = await import('./sequence/renderer');
|
|
6399
7098
|
const seqParsed = parseSequenceDgmo(content);
|
|
6400
7099
|
if (seqParsed.error || seqParsed.participants.length === 0) return '';
|
|
6401
|
-
renderSequenceDiagram(
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
7100
|
+
renderSequenceDiagram(
|
|
7101
|
+
container,
|
|
7102
|
+
seqParsed,
|
|
7103
|
+
effectivePalette,
|
|
7104
|
+
isDark,
|
|
7105
|
+
undefined,
|
|
7106
|
+
{
|
|
7107
|
+
exportWidth: EXPORT_WIDTH,
|
|
7108
|
+
activeTagGroup: options?.tagGroup,
|
|
7109
|
+
}
|
|
7110
|
+
);
|
|
6405
7111
|
} else if (parsed.type === 'wordcloud') {
|
|
6406
|
-
await renderWordCloudAsync(
|
|
7112
|
+
await renderWordCloudAsync(
|
|
7113
|
+
container,
|
|
7114
|
+
parsed,
|
|
7115
|
+
effectivePalette,
|
|
7116
|
+
isDark,
|
|
7117
|
+
dims
|
|
7118
|
+
);
|
|
6407
7119
|
} else if (parsed.type === 'arc') {
|
|
6408
|
-
renderArcDiagram(
|
|
7120
|
+
renderArcDiagram(
|
|
7121
|
+
container,
|
|
7122
|
+
parsed,
|
|
7123
|
+
effectivePalette,
|
|
7124
|
+
isDark,
|
|
7125
|
+
undefined,
|
|
7126
|
+
dims
|
|
7127
|
+
);
|
|
6409
7128
|
} else if (parsed.type === 'timeline') {
|
|
6410
|
-
renderTimeline(
|
|
6411
|
-
|
|
7129
|
+
renderTimeline(
|
|
7130
|
+
container,
|
|
7131
|
+
parsed,
|
|
7132
|
+
effectivePalette,
|
|
7133
|
+
isDark,
|
|
7134
|
+
undefined,
|
|
7135
|
+
dims,
|
|
7136
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup,
|
|
7137
|
+
orgExportState?.swimlaneTagGroup
|
|
7138
|
+
);
|
|
6412
7139
|
} else if (parsed.type === 'venn') {
|
|
6413
7140
|
renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
|
|
6414
7141
|
} else if (parsed.type === 'quadrant') {
|
|
6415
|
-
renderQuadrant(
|
|
7142
|
+
renderQuadrant(
|
|
7143
|
+
container,
|
|
7144
|
+
parsed,
|
|
7145
|
+
effectivePalette,
|
|
7146
|
+
isDark,
|
|
7147
|
+
undefined,
|
|
7148
|
+
dims
|
|
7149
|
+
);
|
|
6416
7150
|
} else {
|
|
6417
|
-
renderSlopeChart(
|
|
7151
|
+
renderSlopeChart(
|
|
7152
|
+
container,
|
|
7153
|
+
parsed,
|
|
7154
|
+
effectivePalette,
|
|
7155
|
+
isDark,
|
|
7156
|
+
undefined,
|
|
7157
|
+
dims
|
|
7158
|
+
);
|
|
6418
7159
|
}
|
|
6419
7160
|
|
|
6420
7161
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|