@diagrammo/dgmo 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/dgmo-diagram-this.md +60 -0
- package/.claude/commands/dgmo-document-project.md +128 -0
- package/.claude/commands/dgmo.md +185 -50
- package/.cursorrules +32 -37
- package/.github/copilot-instructions.md +35 -44
- package/.windsurfrules +32 -37
- package/README.md +4 -4
- package/dist/cli.cjs +189 -194
- package/dist/editor.cjs +336 -0
- package/dist/editor.cjs.map +1 -0
- package/dist/editor.d.cts +27 -0
- package/dist/editor.d.ts +27 -0
- package/dist/editor.js +305 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.cjs +3699 -1564
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -6
- package/dist/index.d.ts +7 -6
- package/dist/index.js +3699 -1564
- package/dist/index.js.map +1 -1
- package/docs/language-reference.md +822 -1060
- package/gallery/fixtures/arc.dgmo +18 -0
- package/gallery/fixtures/area.dgmo +19 -0
- package/gallery/fixtures/bar-stacked.dgmo +10 -0
- package/gallery/fixtures/bar.dgmo +10 -0
- package/gallery/fixtures/c4-full.dgmo +52 -0
- package/gallery/fixtures/c4.dgmo +17 -0
- package/gallery/fixtures/chord.dgmo +12 -0
- package/gallery/fixtures/class-basic.dgmo +14 -0
- package/gallery/fixtures/class-full.dgmo +43 -0
- package/gallery/fixtures/doughnut.dgmo +8 -0
- package/gallery/fixtures/flowchart-basic.dgmo +3 -0
- package/gallery/fixtures/flowchart-colors.dgmo +5 -0
- package/gallery/fixtures/flowchart-complex.dgmo +17 -0
- package/gallery/fixtures/flowchart-decision.dgmo +5 -0
- package/gallery/fixtures/flowchart-full.dgmo +13 -0
- package/gallery/fixtures/flowchart-groups.dgmo +10 -0
- package/gallery/fixtures/flowchart-loop.dgmo +7 -0
- package/gallery/fixtures/flowchart-nested.dgmo +7 -0
- package/gallery/fixtures/flowchart-shapes.dgmo +5 -0
- package/gallery/fixtures/function.dgmo +8 -0
- package/gallery/fixtures/funnel.dgmo +7 -0
- package/gallery/fixtures/gantt-full.dgmo +49 -0
- package/gallery/fixtures/gantt.dgmo +42 -0
- package/gallery/fixtures/heatmap.dgmo +8 -0
- package/gallery/fixtures/infra-full.dgmo +78 -0
- package/gallery/fixtures/infra-overload.dgmo +25 -0
- package/gallery/fixtures/infra.dgmo +47 -0
- package/gallery/fixtures/initiative-status-full.dgmo +46 -0
- package/gallery/fixtures/initiative-status-phases.dgmo +29 -0
- package/gallery/fixtures/initiative-status.dgmo +9 -0
- package/gallery/fixtures/line.dgmo +19 -0
- package/gallery/fixtures/multi-line.dgmo +11 -0
- package/gallery/fixtures/org-basic.dgmo +16 -0
- package/gallery/fixtures/org-full.dgmo +69 -0
- package/gallery/fixtures/org-teams.dgmo +25 -0
- package/gallery/fixtures/pie.dgmo +9 -0
- package/gallery/fixtures/polar-area.dgmo +8 -0
- package/gallery/fixtures/quadrant.dgmo +18 -0
- package/gallery/fixtures/radar.dgmo +8 -0
- package/gallery/fixtures/sankey.dgmo +31 -0
- package/gallery/fixtures/scatter.dgmo +21 -0
- package/gallery/fixtures/sequence-tags-protocols.dgmo +45 -0
- package/gallery/fixtures/sequence-tags.dgmo +41 -0
- package/gallery/fixtures/sequence.dgmo +35 -0
- package/gallery/fixtures/sitemap-basic.dgmo +12 -0
- package/gallery/fixtures/sitemap-full.dgmo +156 -0
- package/gallery/fixtures/slope.dgmo +8 -0
- package/gallery/fixtures/spr-eras.dgmo +62 -0
- package/gallery/fixtures/state.dgmo +30 -0
- package/gallery/fixtures/timeline-intraday.dgmo +14 -0
- package/gallery/fixtures/timeline.dgmo +32 -0
- package/gallery/fixtures/venn.dgmo +10 -0
- package/gallery/fixtures/wordcloud.dgmo +24 -0
- package/package.json +51 -2
- package/src/c4/layout.ts +372 -90
- package/src/c4/parser.ts +113 -62
- package/src/chart.ts +149 -64
- package/src/class/parser.ts +84 -28
- package/src/class/renderer.ts +2 -2
- package/src/cli.ts +179 -77
- package/src/completion.ts +381 -182
- package/src/d3.ts +1026 -428
- package/src/dgmo-mermaid.ts +16 -13
- package/src/dgmo-router.ts +70 -24
- package/src/echarts.ts +682 -169
- package/src/editor/dgmo.grammar +69 -0
- package/src/editor/dgmo.grammar.d.ts +2 -0
- package/src/editor/dgmo.grammar.js +18 -0
- package/src/editor/dgmo.grammar.terms.d.ts +5 -0
- package/src/editor/dgmo.grammar.terms.js +35 -0
- package/src/editor/highlight.ts +36 -0
- package/src/editor/index.ts +28 -0
- package/src/editor/keywords.ts +220 -0
- package/src/editor/tokens.ts +30 -0
- package/src/er/parser.ts +55 -29
- package/src/er/renderer.ts +112 -53
- package/src/gantt/calculator.ts +91 -29
- package/src/gantt/parser.ts +291 -97
- package/src/gantt/renderer.ts +1120 -350
- package/src/graph/flowchart-parser.ts +48 -75
- package/src/graph/state-parser.ts +54 -27
- package/src/infra/parser.ts +161 -177
- package/src/infra/renderer.ts +723 -271
- package/src/infra/types.ts +0 -1
- package/src/initiative-status/parser.ts +144 -56
- package/src/kanban/parser.ts +27 -19
- package/src/org/layout.ts +111 -44
- package/src/org/parser.ts +71 -27
- package/src/org/resolver.ts +3 -3
- package/src/palettes/index.ts +3 -2
- package/src/render.ts +1 -2
- package/src/sequence/parser.ts +209 -100
- package/src/sitemap/parser.ts +73 -44
- 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 +82 -72
- package/src/utils/tag-groups.ts +4 -41
- package/src/infra/serialize.ts +0 -67
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,
|
|
@@ -463,8 +505,20 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
463
505
|
let currentArcGroup: string | null = null;
|
|
464
506
|
let currentTimelineGroup: string | null = null;
|
|
465
507
|
let currentTimelineTagGroup: TagGroup | null = null;
|
|
508
|
+
let inTimelineEraBlock = false;
|
|
509
|
+
let timelineEraBlockIndent = 0;
|
|
510
|
+
let inTimelineMarkerBlock = false;
|
|
511
|
+
let timelineMarkerBlockIndent = 0;
|
|
466
512
|
const timelineAliasMap = new Map<string, string>();
|
|
467
|
-
const VALID_D3_TYPES = new Set([
|
|
513
|
+
const VALID_D3_TYPES = new Set([
|
|
514
|
+
'slope',
|
|
515
|
+
'wordcloud',
|
|
516
|
+
'arc',
|
|
517
|
+
'timeline',
|
|
518
|
+
'venn',
|
|
519
|
+
'quadrant',
|
|
520
|
+
'sequence',
|
|
521
|
+
]);
|
|
468
522
|
let firstLineParsed = false;
|
|
469
523
|
|
|
470
524
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -494,14 +548,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
494
548
|
// Not a bare chart type — fall through to normal parsing
|
|
495
549
|
}
|
|
496
550
|
|
|
497
|
-
// Timeline tag group heading: `tag
|
|
551
|
+
// Timeline tag group heading: `tag Name [alias X]`
|
|
498
552
|
if (result.type === 'timeline' && indent === 0) {
|
|
499
553
|
const tagBlockMatch = matchTagBlockHeading(line);
|
|
500
554
|
if (tagBlockMatch) {
|
|
501
|
-
if (tagBlockMatch.deprecated) {
|
|
502
|
-
result.diagnostics.push(makeDgmoError(lineNumber,
|
|
503
|
-
`'## ${tagBlockMatch.name}' is deprecated for tag groups — use 'tag: ${tagBlockMatch.name}' instead`, 'warning'));
|
|
504
|
-
}
|
|
505
555
|
currentTimelineTagGroup = {
|
|
506
556
|
name: tagBlockMatch.name,
|
|
507
557
|
alias: tagBlockMatch.alias,
|
|
@@ -509,7 +559,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
509
559
|
lineNumber,
|
|
510
560
|
};
|
|
511
561
|
if (tagBlockMatch.alias) {
|
|
512
|
-
timelineAliasMap.set(
|
|
562
|
+
timelineAliasMap.set(
|
|
563
|
+
tagBlockMatch.alias.toLowerCase(),
|
|
564
|
+
tagBlockMatch.name.toLowerCase()
|
|
565
|
+
);
|
|
513
566
|
}
|
|
514
567
|
result.timelineTagGroups.push(currentTimelineTagGroup);
|
|
515
568
|
continue;
|
|
@@ -526,7 +579,11 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
526
579
|
const { label, color } = extractColor(entryText, palette);
|
|
527
580
|
if (color) {
|
|
528
581
|
if (isDefault) currentTimelineTagGroup.defaultValue = label;
|
|
529
|
-
currentTimelineTagGroup.entries.push({
|
|
582
|
+
currentTimelineTagGroup.entries.push({
|
|
583
|
+
value: label,
|
|
584
|
+
color,
|
|
585
|
+
lineNumber,
|
|
586
|
+
});
|
|
530
587
|
continue;
|
|
531
588
|
}
|
|
532
589
|
}
|
|
@@ -558,9 +615,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
558
615
|
}
|
|
559
616
|
|
|
560
617
|
// Reject legacy ## group syntax
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
result.
|
|
618
|
+
if (
|
|
619
|
+
/^#{2,}\s+/.test(line) &&
|
|
620
|
+
(result.type === 'arc' || result.type === 'timeline')
|
|
621
|
+
) {
|
|
622
|
+
const name = line
|
|
623
|
+
.replace(/^#{2,}\s+/, '')
|
|
624
|
+
.replace(/\s*\([^)]*\)\s*$/, '')
|
|
625
|
+
.trim();
|
|
626
|
+
result.diagnostics.push(
|
|
627
|
+
makeDgmoError(
|
|
628
|
+
lineNumber,
|
|
629
|
+
`'## ${name}' is no longer supported. Use '[${name}]' instead`,
|
|
630
|
+
'warning'
|
|
631
|
+
)
|
|
632
|
+
);
|
|
564
633
|
continue;
|
|
565
634
|
}
|
|
566
635
|
|
|
@@ -570,10 +639,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
570
639
|
currentTimelineGroup = null;
|
|
571
640
|
}
|
|
572
641
|
|
|
573
|
-
// Arc link line: source -> target(color)
|
|
642
|
+
// Arc link line: source -> target(color) weight
|
|
574
643
|
if (result.type === 'arc') {
|
|
575
644
|
const linkMatch = line.match(
|
|
576
|
-
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(
|
|
645
|
+
/^(.+?)\s*->\s*(.+?)(?:\(([^)]+)\))?\s*(?:\s+(\d+(?:\.\d+)?))?$/
|
|
577
646
|
);
|
|
578
647
|
if (linkMatch) {
|
|
579
648
|
const source = linkMatch[1].trim();
|
|
@@ -605,10 +674,80 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
605
674
|
}
|
|
606
675
|
}
|
|
607
676
|
|
|
608
|
-
// Timeline era
|
|
677
|
+
// Timeline era block entries (indented under bare `era`)
|
|
678
|
+
if (result.type === 'timeline' && inTimelineEraBlock) {
|
|
679
|
+
if (indent <= timelineEraBlockIndent) {
|
|
680
|
+
inTimelineEraBlock = false;
|
|
681
|
+
// fall through to process this line normally
|
|
682
|
+
} else {
|
|
683
|
+
if (line.startsWith('//')) continue;
|
|
684
|
+
const eraEntryMatch = line.match(
|
|
685
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
686
|
+
);
|
|
687
|
+
if (eraEntryMatch) {
|
|
688
|
+
const colorAnnotation = eraEntryMatch[4]?.trim() || null;
|
|
689
|
+
result.timelineEras.push({
|
|
690
|
+
startDate: eraEntryMatch[1],
|
|
691
|
+
endDate: eraEntryMatch[2],
|
|
692
|
+
label: eraEntryMatch[3].trim(),
|
|
693
|
+
color: colorAnnotation
|
|
694
|
+
? resolveColor(colorAnnotation, palette)
|
|
695
|
+
: null,
|
|
696
|
+
lineNumber,
|
|
697
|
+
});
|
|
698
|
+
} else {
|
|
699
|
+
warn(lineNumber, `Unrecognized era entry: "${line}"`);
|
|
700
|
+
}
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Timeline marker block entries (indented under bare `marker`)
|
|
706
|
+
if (result.type === 'timeline' && inTimelineMarkerBlock) {
|
|
707
|
+
if (indent <= timelineMarkerBlockIndent) {
|
|
708
|
+
inTimelineMarkerBlock = false;
|
|
709
|
+
// fall through to process this line normally
|
|
710
|
+
} else {
|
|
711
|
+
if (line.startsWith('//')) continue;
|
|
712
|
+
const markerEntryMatch = line.match(
|
|
713
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
714
|
+
);
|
|
715
|
+
if (markerEntryMatch) {
|
|
716
|
+
const colorAnnotation = markerEntryMatch[3]?.trim() || null;
|
|
717
|
+
result.timelineMarkers.push({
|
|
718
|
+
date: markerEntryMatch[1],
|
|
719
|
+
label: markerEntryMatch[2].trim(),
|
|
720
|
+
color: colorAnnotation
|
|
721
|
+
? resolveColor(colorAnnotation, palette)
|
|
722
|
+
: null,
|
|
723
|
+
lineNumber,
|
|
724
|
+
});
|
|
725
|
+
} else {
|
|
726
|
+
warn(lineNumber, `Unrecognized marker entry: "${line}"`);
|
|
727
|
+
}
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Timeline era/marker block starters and inline forms
|
|
609
733
|
if (result.type === 'timeline') {
|
|
734
|
+
// Bare `era` keyword starts a block
|
|
735
|
+
if (line.toLowerCase() === 'era') {
|
|
736
|
+
inTimelineEraBlock = true;
|
|
737
|
+
timelineEraBlockIndent = indent;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Bare `marker` keyword starts a block
|
|
742
|
+
if (line.toLowerCase() === 'marker') {
|
|
743
|
+
inTimelineMarkerBlock = true;
|
|
744
|
+
timelineMarkerBlockIndent = indent;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Timeline era lines (inline): era YYYY->YYYY Label (color)
|
|
610
749
|
const eraMatch = line.match(
|
|
611
|
-
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
750
|
+
/^era\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
612
751
|
);
|
|
613
752
|
if (eraMatch) {
|
|
614
753
|
const colorAnnotation = eraMatch[4]?.trim() || null;
|
|
@@ -624,9 +763,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
624
763
|
continue;
|
|
625
764
|
}
|
|
626
765
|
|
|
627
|
-
// Timeline marker lines: marker YYYY Label (color)
|
|
766
|
+
// Timeline marker lines (inline): marker YYYY Label (color)
|
|
628
767
|
const markerMatch = line.match(
|
|
629
|
-
/^marker
|
|
768
|
+
/^marker\s+(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s+(.+?)(?:\s*\(([^)]+)\))?\s*$/
|
|
630
769
|
);
|
|
631
770
|
if (markerMatch) {
|
|
632
771
|
const colorAnnotation = markerMatch[3]?.trim() || null;
|
|
@@ -647,8 +786,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
647
786
|
// Duration event: 2026-07-15->30d: description (d=days, w=weeks, m=months, y=years, h=hours, min=minutes)
|
|
648
787
|
// Supports decimals up to 2 places (e.g., 1.25y = 1 year 3 months)
|
|
649
788
|
// Supports uncertain end with ? suffix (e.g., ->3m?: fades out the last 20%)
|
|
789
|
+
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
650
790
|
const durationMatch = line.match(
|
|
651
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s
|
|
791
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d+(?:\.\d{1,2})?)(min|[dwmyh])(\?)?\s+(.+)$/
|
|
652
792
|
);
|
|
653
793
|
if (durationMatch) {
|
|
654
794
|
const startDate = durationMatch[1];
|
|
@@ -657,9 +797,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
657
797
|
const unit = durationMatch[3] as 'd' | 'w' | 'm' | 'y' | 'h' | 'min';
|
|
658
798
|
const endDate = addDurationToDate(startDate, amount, unit);
|
|
659
799
|
const segments = durationMatch[5].split('|');
|
|
660
|
-
const metadata =
|
|
661
|
-
|
|
662
|
-
|
|
800
|
+
const metadata =
|
|
801
|
+
segments.length > 1
|
|
802
|
+
? parsePipeMetadata(
|
|
803
|
+
['', ...segments.slice(1)],
|
|
804
|
+
timelineAliasMap,
|
|
805
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
806
|
+
)
|
|
807
|
+
: {};
|
|
663
808
|
result.timelineEvents.push({
|
|
664
809
|
date: startDate,
|
|
665
810
|
endDate,
|
|
@@ -673,14 +818,21 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
673
818
|
}
|
|
674
819
|
|
|
675
820
|
// Range event: 1655->1667 description (supports uncertain end: 1655->1667?)
|
|
821
|
+
// Also supports YYYY-MM-DD HH:MM in both start and end dates
|
|
822
|
+
// Accepts both -> (hyphen) and –> (en-dash U+2013)
|
|
676
823
|
const rangeMatch = line.match(
|
|
677
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s
|
|
824
|
+
/^(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)\s*(?:->|\u2013>)\s*(\d{4}(?:-\d{2})?(?:-\d{2}(?: \d{2}:\d{2})?)?)(\?)?\s+(.+)$/
|
|
678
825
|
);
|
|
679
826
|
if (rangeMatch) {
|
|
680
827
|
const segments = rangeMatch[4].split('|');
|
|
681
|
-
const metadata =
|
|
682
|
-
|
|
683
|
-
|
|
828
|
+
const metadata =
|
|
829
|
+
segments.length > 1
|
|
830
|
+
? parsePipeMetadata(
|
|
831
|
+
['', ...segments.slice(1)],
|
|
832
|
+
timelineAliasMap,
|
|
833
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
834
|
+
)
|
|
835
|
+
: {};
|
|
684
836
|
result.timelineEvents.push({
|
|
685
837
|
date: rangeMatch[1],
|
|
686
838
|
endDate: rangeMatch[2],
|
|
@@ -693,15 +845,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
693
845
|
continue;
|
|
694
846
|
}
|
|
695
847
|
|
|
696
|
-
// Point event: 1718 description
|
|
697
|
-
const pointMatch = line.match(
|
|
698
|
-
/^(\d{4}(?:-\d{2})?(?:-\d{2})?)(?:\s*:\s*|\s+)(.+)$/
|
|
699
|
-
);
|
|
848
|
+
// Point event: 1718 description
|
|
849
|
+
const pointMatch = line.match(/^(\d{4}(?:-\d{2})?(?:-\d{2})?)\s+(.+)$/);
|
|
700
850
|
if (pointMatch) {
|
|
701
851
|
const segments = pointMatch[2].split('|');
|
|
702
|
-
const metadata =
|
|
703
|
-
|
|
704
|
-
|
|
852
|
+
const metadata =
|
|
853
|
+
segments.length > 1
|
|
854
|
+
? parsePipeMetadata(
|
|
855
|
+
['', ...segments.slice(1)],
|
|
856
|
+
timelineAliasMap,
|
|
857
|
+
() => warn(lineNumber, MULTIPLE_PIPE_ERROR)
|
|
858
|
+
)
|
|
859
|
+
: {};
|
|
705
860
|
result.timelineEvents.push({
|
|
706
861
|
date: pointMatch[1],
|
|
707
862
|
endDate: null,
|
|
@@ -716,27 +871,58 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
716
871
|
|
|
717
872
|
// Venn diagram DSL
|
|
718
873
|
if (result.type === 'venn') {
|
|
719
|
-
// Intersection line: "A + B
|
|
874
|
+
// Intersection line: "A + B Label" / "A + B" / "A + B + C Label"
|
|
875
|
+
// Also accepts deprecated colon syntax: "A + B: Label"
|
|
720
876
|
if (/\+/.test(line)) {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
label = line.substring(colonIdx + 1).trim() || null;
|
|
727
|
-
} else {
|
|
728
|
-
setsPart = line.trim();
|
|
729
|
-
label = null;
|
|
877
|
+
// Build lookup of known set names and aliases for label extraction
|
|
878
|
+
const knownSetRefs = new Set<string>();
|
|
879
|
+
for (const s of result.vennSets) {
|
|
880
|
+
knownSetRefs.add(s.name.toLowerCase());
|
|
881
|
+
if (s.alias) knownSetRefs.add(s.alias.toLowerCase());
|
|
730
882
|
}
|
|
731
|
-
|
|
732
|
-
|
|
883
|
+
|
|
884
|
+
const segments = line
|
|
885
|
+
.split('+')
|
|
886
|
+
.map((s) => s.trim())
|
|
887
|
+
.filter(Boolean);
|
|
888
|
+
if (segments.length >= 2) {
|
|
889
|
+
// All segments except the last are pure set references
|
|
890
|
+
const rawSets = segments.slice(0, -1);
|
|
891
|
+
const lastSeg = segments[segments.length - 1];
|
|
892
|
+
|
|
893
|
+
// For the last segment, extract set reference and optional label.
|
|
894
|
+
// Find where the set reference ends and label begins.
|
|
895
|
+
// Try progressively shorter prefixes against known set names/aliases.
|
|
896
|
+
const words = lastSeg.split(/\s+/);
|
|
897
|
+
let matchLen = 0;
|
|
898
|
+
for (let w = words.length; w >= 1; w--) {
|
|
899
|
+
const candidate = words.slice(0, w).join(' ');
|
|
900
|
+
if (knownSetRefs.has(candidate.toLowerCase())) {
|
|
901
|
+
matchLen = w;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
let lastSetRef: string;
|
|
906
|
+
let label: string | null;
|
|
907
|
+
if (matchLen > 0) {
|
|
908
|
+
lastSetRef = words.slice(0, matchLen).join(' ');
|
|
909
|
+
label =
|
|
910
|
+
words.length > matchLen ? words.slice(matchLen).join(' ') : null;
|
|
911
|
+
} else {
|
|
912
|
+
// No known set matched — assume first word is the set ref, rest is label
|
|
913
|
+
lastSetRef = words[0];
|
|
914
|
+
label = words.length > 1 ? words.slice(1).join(' ') : null;
|
|
915
|
+
}
|
|
916
|
+
rawSets.push(lastSetRef);
|
|
733
917
|
result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
|
|
734
918
|
continue;
|
|
735
919
|
}
|
|
736
920
|
}
|
|
737
921
|
|
|
738
922
|
// Set declaration: "Name(color) alias x" / "Name alias x" / "Name(color)" / "Name"
|
|
739
|
-
const setDeclMatch = line.match(
|
|
923
|
+
const setDeclMatch = line.match(
|
|
924
|
+
/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i
|
|
925
|
+
);
|
|
740
926
|
if (setDeclMatch) {
|
|
741
927
|
const name = setDeclMatch[1].trim();
|
|
742
928
|
const colorName = setDeclMatch[2]?.trim() ?? null;
|
|
@@ -744,11 +930,17 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
744
930
|
if (colorName) {
|
|
745
931
|
const resolved = resolveColor(colorName, palette);
|
|
746
932
|
if (resolved === null) {
|
|
747
|
-
warn(
|
|
933
|
+
warn(
|
|
934
|
+
lineNumber,
|
|
935
|
+
`Hex colors are not supported — use named colors (blue, red, green, etc.)`
|
|
936
|
+
);
|
|
748
937
|
} else if (resolved.startsWith('#')) {
|
|
749
938
|
color = resolved;
|
|
750
939
|
} else {
|
|
751
|
-
warn(
|
|
940
|
+
warn(
|
|
941
|
+
lineNumber,
|
|
942
|
+
`Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`
|
|
943
|
+
);
|
|
752
944
|
}
|
|
753
945
|
}
|
|
754
946
|
const alias = setDeclMatch[3]?.trim() ?? null;
|
|
@@ -759,8 +951,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
759
951
|
|
|
760
952
|
// Quadrant-specific parsing
|
|
761
953
|
if (result.type === 'quadrant') {
|
|
762
|
-
// x-
|
|
763
|
-
const xAxisMatch = line.match(/^x-
|
|
954
|
+
// x-label Low, High — or indented multi-line
|
|
955
|
+
const xAxisMatch = line.match(/^x-label\s+(.*)/i);
|
|
764
956
|
if (xAxisMatch) {
|
|
765
957
|
const val = xAxisMatch[1].trim();
|
|
766
958
|
let parts: string[];
|
|
@@ -778,8 +970,8 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
778
970
|
continue;
|
|
779
971
|
}
|
|
780
972
|
|
|
781
|
-
// y-
|
|
782
|
-
const yAxisMatch = line.match(/^y-
|
|
973
|
+
// y-label Low, High — or indented multi-line
|
|
974
|
+
const yAxisMatch = line.match(/^y-label\s+(.*)/i);
|
|
783
975
|
if (yAxisMatch) {
|
|
784
976
|
const val = yAxisMatch[1].trim();
|
|
785
977
|
let parts: string[];
|
|
@@ -797,9 +989,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
797
989
|
continue;
|
|
798
990
|
}
|
|
799
991
|
|
|
800
|
-
// Quadrant position labels: top-right
|
|
992
|
+
// Quadrant position labels: top-right Label (color)
|
|
801
993
|
const quadrantLabelRe =
|
|
802
|
-
/^(top-right|top-left|bottom-left|bottom-right)\s
|
|
994
|
+
/^(top-right|top-left|bottom-left|bottom-right)\s+(.+)/i;
|
|
803
995
|
const quadrantMatch = line.match(quadrantLabelRe);
|
|
804
996
|
if (quadrantMatch) {
|
|
805
997
|
const position = quadrantMatch[1].toLowerCase();
|
|
@@ -821,9 +1013,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
821
1013
|
continue;
|
|
822
1014
|
}
|
|
823
1015
|
|
|
824
|
-
// Data points: Label
|
|
1016
|
+
// Data points: Label x, y
|
|
825
1017
|
const pointMatch = line.match(
|
|
826
|
-
/^(.+?)
|
|
1018
|
+
/^(.+?)\s+([0-9]*\.?[0-9]+)\s*,\s*([0-9]*\.?[0-9]+)\s*$/
|
|
827
1019
|
);
|
|
828
1020
|
if (pointMatch) {
|
|
829
1021
|
const label = pointMatch[1].trim();
|
|
@@ -852,7 +1044,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
852
1044
|
const firstToken = line.substring(0, spaceIdx).toLowerCase();
|
|
853
1045
|
const restValue = line.substring(spaceIdx + 1).trim();
|
|
854
1046
|
|
|
855
|
-
if (
|
|
1047
|
+
if (
|
|
1048
|
+
firstToken === 'chart' &&
|
|
1049
|
+
VALID_D3_TYPES.has(restValue.toLowerCase())
|
|
1050
|
+
) {
|
|
856
1051
|
result.type = restValue.toLowerCase() as ParsedVisualization['type'];
|
|
857
1052
|
continue;
|
|
858
1053
|
}
|
|
@@ -866,20 +1061,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
866
1061
|
continue;
|
|
867
1062
|
}
|
|
868
1063
|
|
|
869
|
-
if (firstToken === 'orientation' || firstToken === 'direction') {
|
|
870
|
-
if (result.type === 'arc' || result.type === 'timeline') {
|
|
871
|
-
const vLower = restValue.toLowerCase();
|
|
872
|
-
if (vLower === 'horizontal' || vLower === 'vertical') {
|
|
873
|
-
result.orientation = vLower;
|
|
874
|
-
} else {
|
|
875
|
-
const dir = normalizeDirection(restValue);
|
|
876
|
-
if (dir === 'LR') result.orientation = 'horizontal';
|
|
877
|
-
else if (dir === 'TB') result.orientation = 'vertical';
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
continue;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
1064
|
if (firstToken === 'order') {
|
|
884
1065
|
const v = restValue.toLowerCase();
|
|
885
1066
|
if (v === 'name' || v === 'group' || v === 'degree') {
|
|
@@ -888,29 +1069,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
888
1069
|
continue;
|
|
889
1070
|
}
|
|
890
1071
|
|
|
891
|
-
if (firstToken === 'sort') {
|
|
892
|
-
const vLower = restValue.toLowerCase();
|
|
893
|
-
if (vLower === 'time' || vLower === 'group') {
|
|
894
|
-
result.timelineSort = vLower;
|
|
895
|
-
} else if (vLower === 'tag' || vLower.startsWith('tag:')) {
|
|
896
|
-
result.timelineSort = 'tag';
|
|
897
|
-
if (vLower.startsWith('tag:')) {
|
|
898
|
-
const groupRef = restValue.substring(4).trim();
|
|
899
|
-
if (groupRef) {
|
|
900
|
-
result.timelineDefaultSwimlaneTG = groupRef;
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
continue;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (firstToken === 'swimlanes') {
|
|
908
|
-
const v = restValue.toLowerCase();
|
|
909
|
-
if (v === 'on') result.timelineSwimlanes = true;
|
|
910
|
-
else if (v === 'off') result.timelineSwimlanes = false;
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
1072
|
if (firstToken === 'rotate') {
|
|
915
1073
|
const v = restValue.toLowerCase();
|
|
916
1074
|
if (v === 'none' || v === 'mixed' || v === 'angled') {
|
|
@@ -951,23 +1109,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
951
1109
|
// Check for color annotation in raw key: "Label(color)"
|
|
952
1110
|
const colorMatch = rawKey.match(/^(.+?)\(([^)]+)\)\s*$/);
|
|
953
1111
|
|
|
954
|
-
if (key === 'chart') {
|
|
955
|
-
const value = line
|
|
956
|
-
.substring(colonIndex + 1)
|
|
957
|
-
.trim()
|
|
958
|
-
.toLowerCase();
|
|
959
|
-
if (VALID_D3_TYPES.has(value)) {
|
|
960
|
-
result.type = value as ParsedVisualization['type'];
|
|
961
|
-
} else {
|
|
962
|
-
const validD3Types = [...VALID_D3_TYPES];
|
|
963
|
-
let msg = `Unsupported chart type: ${value}. Supported types: ${validD3Types.join(', ')}`;
|
|
964
|
-
const hint = suggest(value, validD3Types);
|
|
965
|
-
if (hint) msg += `. ${hint}`;
|
|
966
|
-
return fail(lineNumber, msg);
|
|
967
|
-
}
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
1112
|
if (key === 'title') {
|
|
972
1113
|
result.title = line.substring(colonIndex + 1).trim();
|
|
973
1114
|
result.titleLineNumber = lineNumber;
|
|
@@ -977,23 +1118,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
977
1118
|
continue;
|
|
978
1119
|
}
|
|
979
1120
|
|
|
980
|
-
if (key === 'orientation' || key === 'direction') {
|
|
981
|
-
// Only arc and timeline support orientation
|
|
982
|
-
if (result.type === 'arc' || result.type === 'timeline') {
|
|
983
|
-
const raw = line.substring(colonIndex + 1).trim();
|
|
984
|
-
// Accept horizontal/vertical directly, or LR/TB via normalizeDirection
|
|
985
|
-
const vLower = raw.toLowerCase();
|
|
986
|
-
if (vLower === 'horizontal' || vLower === 'vertical') {
|
|
987
|
-
result.orientation = vLower;
|
|
988
|
-
} else {
|
|
989
|
-
const dir = normalizeDirection(raw);
|
|
990
|
-
if (dir === 'LR') result.orientation = 'horizontal';
|
|
991
|
-
else if (dir === 'TB') result.orientation = 'vertical';
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
1121
|
if (key === 'order') {
|
|
998
1122
|
const v = line
|
|
999
1123
|
.substring(colonIndex + 1)
|
|
@@ -1005,39 +1129,6 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1005
1129
|
continue;
|
|
1006
1130
|
}
|
|
1007
1131
|
|
|
1008
|
-
if (key === 'sort') {
|
|
1009
|
-
const v = line
|
|
1010
|
-
.substring(colonIndex + 1)
|
|
1011
|
-
.trim();
|
|
1012
|
-
const vLower = v.toLowerCase();
|
|
1013
|
-
if (vLower === 'time' || vLower === 'group') {
|
|
1014
|
-
result.timelineSort = vLower;
|
|
1015
|
-
} else if (vLower === 'tag' || vLower.startsWith('tag:')) {
|
|
1016
|
-
result.timelineSort = 'tag';
|
|
1017
|
-
if (vLower.startsWith('tag:')) {
|
|
1018
|
-
// Extract group name (preserving original case for display)
|
|
1019
|
-
const groupRef = v.substring(4).trim();
|
|
1020
|
-
if (groupRef) {
|
|
1021
|
-
result.timelineDefaultSwimlaneTG = groupRef;
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (key === 'swimlanes') {
|
|
1029
|
-
const v = line
|
|
1030
|
-
.substring(colonIndex + 1)
|
|
1031
|
-
.trim()
|
|
1032
|
-
.toLowerCase();
|
|
1033
|
-
if (v === 'on') {
|
|
1034
|
-
result.timelineSwimlanes = true;
|
|
1035
|
-
} else if (v === 'off') {
|
|
1036
|
-
result.timelineSwimlanes = false;
|
|
1037
|
-
}
|
|
1038
|
-
continue;
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
1132
|
if (key === 'rotate') {
|
|
1042
1133
|
const v = line
|
|
1043
1134
|
.substring(colonIndex + 1)
|
|
@@ -1092,22 +1183,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1092
1183
|
}
|
|
1093
1184
|
|
|
1094
1185
|
if (allNumeric && numericValues.length > 0) {
|
|
1095
|
-
//
|
|
1096
|
-
if (result.type
|
|
1097
|
-
result.words.push({
|
|
1098
|
-
text: labelPart,
|
|
1099
|
-
weight: numericValues[0],
|
|
1100
|
-
lineNumber,
|
|
1101
|
-
});
|
|
1102
|
-
} else {
|
|
1186
|
+
// Wordcloud does not use colon data format — skip to freeform handling
|
|
1187
|
+
if (result.type !== 'wordcloud') {
|
|
1103
1188
|
result.data.push({
|
|
1104
1189
|
label: labelPart,
|
|
1105
1190
|
values: numericValues,
|
|
1106
1191
|
color: colorPart,
|
|
1107
1192
|
lineNumber,
|
|
1108
1193
|
});
|
|
1194
|
+
continue;
|
|
1109
1195
|
}
|
|
1110
|
-
continue;
|
|
1111
1196
|
}
|
|
1112
1197
|
}
|
|
1113
1198
|
|
|
@@ -1119,9 +1204,14 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1119
1204
|
} else if (colonIndex === -1) {
|
|
1120
1205
|
// Try "word weight" or "multi-word-label weight" space-separated format
|
|
1121
1206
|
const lastSpace = line.lastIndexOf(' ');
|
|
1122
|
-
const maybeWeight =
|
|
1207
|
+
const maybeWeight =
|
|
1208
|
+
lastSpace >= 0 ? parseFloat(line.substring(lastSpace + 1)) : NaN;
|
|
1123
1209
|
if (lastSpace >= 0 && !isNaN(maybeWeight) && maybeWeight > 0) {
|
|
1124
|
-
result.words.push({
|
|
1210
|
+
result.words.push({
|
|
1211
|
+
text: line.substring(0, lastSpace).trim(),
|
|
1212
|
+
weight: maybeWeight,
|
|
1213
|
+
lineNumber,
|
|
1214
|
+
});
|
|
1125
1215
|
} else {
|
|
1126
1216
|
freeformLines.push(line);
|
|
1127
1217
|
}
|
|
@@ -1148,13 +1238,23 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1148
1238
|
continue;
|
|
1149
1239
|
}
|
|
1150
1240
|
}
|
|
1241
|
+
|
|
1242
|
+
// Catch-all: nothing matched this line
|
|
1243
|
+
// Skip on first line — chart type suggestion is handled post-loop
|
|
1244
|
+
if (firstLineParsed) {
|
|
1245
|
+
warn(lineNumber, `Unexpected line: '${line}'.`);
|
|
1246
|
+
}
|
|
1151
1247
|
}
|
|
1152
1248
|
|
|
1153
1249
|
// Validation
|
|
1154
1250
|
if (!result.type) {
|
|
1155
1251
|
const validD3Types = [...VALID_D3_TYPES];
|
|
1156
|
-
const firstNonEmpty =
|
|
1157
|
-
|
|
1252
|
+
const firstNonEmpty =
|
|
1253
|
+
lines.find((l) => l.trim() && !l.trim().startsWith('//'))?.trim() ?? '';
|
|
1254
|
+
const hint = suggest(
|
|
1255
|
+
firstNonEmpty.split(/\s/)[0].toLowerCase(),
|
|
1256
|
+
validD3Types
|
|
1257
|
+
);
|
|
1158
1258
|
let msg = `Unsupported chart type: "${firstNonEmpty.split(/\s/)[0]}". Supported types: ${validD3Types.join(', ')}`;
|
|
1159
1259
|
if (hint) msg += `. ${hint}`;
|
|
1160
1260
|
return fail(1, msg);
|
|
@@ -1171,7 +1271,10 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1171
1271
|
result.words = tokenizeFreeformText(freeformLines.join(' '));
|
|
1172
1272
|
}
|
|
1173
1273
|
if (result.words.length === 0) {
|
|
1174
|
-
warn(
|
|
1274
|
+
warn(
|
|
1275
|
+
1,
|
|
1276
|
+
'No words found. Add words as "word weight" (space-separated), one per line, or paste freeform text'
|
|
1277
|
+
);
|
|
1175
1278
|
}
|
|
1176
1279
|
// Apply max word limit (words are already sorted by weight desc for freeform)
|
|
1177
1280
|
if (
|
|
@@ -1188,12 +1291,18 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1188
1291
|
|
|
1189
1292
|
if (result.type === 'arc') {
|
|
1190
1293
|
if (result.links.length === 0) {
|
|
1191
|
-
warn(
|
|
1294
|
+
warn(
|
|
1295
|
+
1,
|
|
1296
|
+
'No links found. Add links as "Source -> Target weight" (e.g., "Alice -> Bob 5")'
|
|
1297
|
+
);
|
|
1192
1298
|
}
|
|
1193
1299
|
// Validate arc ordering vs groups
|
|
1194
1300
|
if (result.arcNodeGroups.length > 0) {
|
|
1195
1301
|
if (result.arcOrder === 'name' || result.arcOrder === 'degree') {
|
|
1196
|
-
warn(
|
|
1302
|
+
warn(
|
|
1303
|
+
1,
|
|
1304
|
+
`Cannot use "order ${result.arcOrder}" with [Group] headers. Use "order group" or remove group headers.`
|
|
1305
|
+
);
|
|
1197
1306
|
result.arcOrder = 'group';
|
|
1198
1307
|
}
|
|
1199
1308
|
if (result.arcOrder === 'appearance') {
|
|
@@ -1205,15 +1314,19 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1205
1314
|
|
|
1206
1315
|
if (result.type === 'timeline') {
|
|
1207
1316
|
if (result.timelineEvents.length === 0) {
|
|
1208
|
-
warn(
|
|
1317
|
+
warn(
|
|
1318
|
+
1,
|
|
1319
|
+
'No events found. Add events as "YYYY: description" or "YYYY->YYYY: description"'
|
|
1320
|
+
);
|
|
1209
1321
|
}
|
|
1210
1322
|
// Validate tag values and inject defaults
|
|
1211
1323
|
if (result.timelineTagGroups.length > 0) {
|
|
1212
1324
|
validateTagValues(
|
|
1213
1325
|
result.timelineEvents,
|
|
1214
1326
|
result.timelineTagGroups,
|
|
1215
|
-
(line, msg) =>
|
|
1216
|
-
|
|
1327
|
+
(line, msg) =>
|
|
1328
|
+
result.diagnostics.push(makeDgmoError(line, msg, 'warning')),
|
|
1329
|
+
suggest
|
|
1217
1330
|
);
|
|
1218
1331
|
for (const group of result.timelineTagGroups) {
|
|
1219
1332
|
if (!group.defaultValue) continue;
|
|
@@ -1226,35 +1339,15 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1226
1339
|
}
|
|
1227
1340
|
}
|
|
1228
1341
|
|
|
1229
|
-
// Resolve sort: tag default swimlane group
|
|
1230
|
-
if (result.timelineSort === 'tag') {
|
|
1231
|
-
if (result.timelineTagGroups.length === 0) {
|
|
1232
|
-
warn(1, '"sort: tag" requires at least one tag group definition');
|
|
1233
|
-
result.timelineSort = 'time';
|
|
1234
|
-
} else if (result.timelineDefaultSwimlaneTG) {
|
|
1235
|
-
// Resolve alias → full group name
|
|
1236
|
-
const ref = result.timelineDefaultSwimlaneTG.toLowerCase();
|
|
1237
|
-
const match = result.timelineTagGroups.find(
|
|
1238
|
-
(g) => g.name.toLowerCase() === ref || g.alias?.toLowerCase() === ref
|
|
1239
|
-
);
|
|
1240
|
-
if (match) {
|
|
1241
|
-
result.timelineDefaultSwimlaneTG = match.name;
|
|
1242
|
-
} else {
|
|
1243
|
-
warn(1, `"sort: tag:${result.timelineDefaultSwimlaneTG}" — no tag group matches "${result.timelineDefaultSwimlaneTG}"`);
|
|
1244
|
-
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1245
|
-
}
|
|
1246
|
-
} else {
|
|
1247
|
-
// Default to first tag group
|
|
1248
|
-
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
1342
|
return result;
|
|
1253
1343
|
}
|
|
1254
1344
|
|
|
1255
1345
|
if (result.type === 'venn') {
|
|
1256
1346
|
if (result.vennSets.length < 2) {
|
|
1257
|
-
return fail(
|
|
1347
|
+
return fail(
|
|
1348
|
+
1,
|
|
1349
|
+
'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")'
|
|
1350
|
+
);
|
|
1258
1351
|
}
|
|
1259
1352
|
if (result.vennSets.length > 3) {
|
|
1260
1353
|
return fail(1, 'Venn diagrams support 2–3 sets');
|
|
@@ -1268,7 +1361,9 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1268
1361
|
if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
|
|
1269
1362
|
}
|
|
1270
1363
|
const resolveSetRef = (ref: string): string | null =>
|
|
1271
|
-
setNameLower.get(ref.toLowerCase()) ??
|
|
1364
|
+
setNameLower.get(ref.toLowerCase()) ??
|
|
1365
|
+
aliasLower.get(ref.toLowerCase()) ??
|
|
1366
|
+
null;
|
|
1272
1367
|
|
|
1273
1368
|
// Resolve intersection set references; drop invalid ones with a diagnostic
|
|
1274
1369
|
const validOverlaps: VennOverlap[] = [];
|
|
@@ -1278,8 +1373,16 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1278
1373
|
for (const ref of ov.sets) {
|
|
1279
1374
|
const resolved = resolveSetRef(ref);
|
|
1280
1375
|
if (!resolved) {
|
|
1281
|
-
result.diagnostics.push(
|
|
1282
|
-
|
|
1376
|
+
result.diagnostics.push(
|
|
1377
|
+
makeDgmoError(
|
|
1378
|
+
ov.lineNumber,
|
|
1379
|
+
`Intersection references unknown set or alias "${ref}"`
|
|
1380
|
+
)
|
|
1381
|
+
);
|
|
1382
|
+
if (!result.error)
|
|
1383
|
+
result.error = formatDgmoError(
|
|
1384
|
+
result.diagnostics[result.diagnostics.length - 1]
|
|
1385
|
+
);
|
|
1283
1386
|
valid = false;
|
|
1284
1387
|
break;
|
|
1285
1388
|
}
|
|
@@ -1293,24 +1396,36 @@ export function parseVisualization(content: string, palette?: PaletteColors): Pa
|
|
|
1293
1396
|
|
|
1294
1397
|
if (result.type === 'quadrant') {
|
|
1295
1398
|
if (result.quadrantPoints.length === 0) {
|
|
1296
|
-
warn(
|
|
1399
|
+
warn(
|
|
1400
|
+
1,
|
|
1401
|
+
'No data points found. Add points as "Label x, y" (e.g., "Item A 0.5, 0.7")'
|
|
1402
|
+
);
|
|
1297
1403
|
}
|
|
1298
1404
|
return result;
|
|
1299
1405
|
}
|
|
1300
1406
|
|
|
1301
1407
|
// Slope chart validation
|
|
1302
1408
|
if (result.periods.length < 2) {
|
|
1303
|
-
return fail(
|
|
1409
|
+
return fail(
|
|
1410
|
+
1,
|
|
1411
|
+
'Missing or invalid periods line. Provide at least 2 comma-separated period labels (e.g., "2020, 2024")'
|
|
1412
|
+
);
|
|
1304
1413
|
}
|
|
1305
1414
|
|
|
1306
1415
|
if (result.data.length === 0) {
|
|
1307
|
-
warn(
|
|
1416
|
+
warn(
|
|
1417
|
+
1,
|
|
1418
|
+
'No data lines found. Add data as "Label: value1, value2" (e.g., "Apple: 25, 35")'
|
|
1419
|
+
);
|
|
1308
1420
|
}
|
|
1309
1421
|
|
|
1310
1422
|
// Validate value counts match period count — warn and skip mismatched items
|
|
1311
1423
|
for (const item of result.data) {
|
|
1312
1424
|
if (item.values.length !== result.periods.length) {
|
|
1313
|
-
warn(
|
|
1425
|
+
warn(
|
|
1426
|
+
item.lineNumber,
|
|
1427
|
+
`Data item "${item.label}" has ${item.values.length} value(s) but ${result.periods.length} period(s) are defined`
|
|
1428
|
+
);
|
|
1314
1429
|
}
|
|
1315
1430
|
}
|
|
1316
1431
|
result.data = result.data.filter(
|
|
@@ -1551,7 +1666,14 @@ export function renderSlopeChart(
|
|
|
1551
1666
|
const tooltip = createTooltip(container, palette, isDark);
|
|
1552
1667
|
|
|
1553
1668
|
// Title
|
|
1554
|
-
renderChartTitle(
|
|
1669
|
+
renderChartTitle(
|
|
1670
|
+
svg,
|
|
1671
|
+
title,
|
|
1672
|
+
parsed.titleLineNumber,
|
|
1673
|
+
width,
|
|
1674
|
+
textColor,
|
|
1675
|
+
onClickItem
|
|
1676
|
+
);
|
|
1555
1677
|
|
|
1556
1678
|
// Period column headers
|
|
1557
1679
|
for (const period of periods) {
|
|
@@ -1620,13 +1742,23 @@ export function renderSlopeChart(
|
|
|
1620
1742
|
wrappedLines = lines;
|
|
1621
1743
|
}
|
|
1622
1744
|
const lineHeight = SLOPE_LABEL_FONT_SIZE * 1.2;
|
|
1623
|
-
const labelHeight =
|
|
1624
|
-
|
|
1625
|
-
|
|
1745
|
+
const labelHeight =
|
|
1746
|
+
labelLineCount === 1
|
|
1747
|
+
? SLOPE_LABEL_FONT_SIZE
|
|
1748
|
+
: labelLineCount * lineHeight;
|
|
1626
1749
|
|
|
1627
1750
|
return {
|
|
1628
|
-
item,
|
|
1629
|
-
|
|
1751
|
+
item,
|
|
1752
|
+
idx,
|
|
1753
|
+
color,
|
|
1754
|
+
firstVal,
|
|
1755
|
+
lastVal,
|
|
1756
|
+
tipHtml,
|
|
1757
|
+
lastX,
|
|
1758
|
+
labelText,
|
|
1759
|
+
maxChars,
|
|
1760
|
+
wrappedLines,
|
|
1761
|
+
labelHeight,
|
|
1630
1762
|
};
|
|
1631
1763
|
});
|
|
1632
1764
|
|
|
@@ -1638,7 +1770,10 @@ export function renderSlopeChart(
|
|
|
1638
1770
|
naturalY: yScale(item.values[pi]),
|
|
1639
1771
|
height: leftLabelHeight,
|
|
1640
1772
|
}));
|
|
1641
|
-
leftLabelCollisions.set(
|
|
1773
|
+
leftLabelCollisions.set(
|
|
1774
|
+
pi,
|
|
1775
|
+
resolveVerticalCollisions(entries, 4, innerHeight)
|
|
1776
|
+
);
|
|
1642
1777
|
}
|
|
1643
1778
|
|
|
1644
1779
|
// --- Resolve right-side label collisions ---
|
|
@@ -1646,7 +1781,11 @@ export function renderSlopeChart(
|
|
|
1646
1781
|
naturalY: yScale(si.lastVal),
|
|
1647
1782
|
height: Math.max(si.labelHeight, SLOPE_LABEL_FONT_SIZE * 1.4),
|
|
1648
1783
|
}));
|
|
1649
|
-
const rightAdjustedY = resolveVerticalCollisions(
|
|
1784
|
+
const rightAdjustedY = resolveVerticalCollisions(
|
|
1785
|
+
rightEntries,
|
|
1786
|
+
4,
|
|
1787
|
+
innerHeight
|
|
1788
|
+
);
|
|
1650
1789
|
|
|
1651
1790
|
// Render each data series
|
|
1652
1791
|
data.forEach((item, idx) => {
|
|
@@ -1660,7 +1799,8 @@ export function renderSlopeChart(
|
|
|
1660
1799
|
.attr('data-line-number', String(item.lineNumber));
|
|
1661
1800
|
|
|
1662
1801
|
// Line
|
|
1663
|
-
seriesG
|
|
1802
|
+
seriesG
|
|
1803
|
+
.append('path')
|
|
1664
1804
|
.datum(item.values)
|
|
1665
1805
|
.attr('fill', 'none')
|
|
1666
1806
|
.attr('stroke', color)
|
|
@@ -1668,7 +1808,8 @@ export function renderSlopeChart(
|
|
|
1668
1808
|
.attr('d', lineGen);
|
|
1669
1809
|
|
|
1670
1810
|
// Invisible wider path for easier hover targeting
|
|
1671
|
-
seriesG
|
|
1811
|
+
seriesG
|
|
1812
|
+
.append('path')
|
|
1672
1813
|
.datum(item.values)
|
|
1673
1814
|
.attr('fill', 'none')
|
|
1674
1815
|
.attr('stroke', 'transparent')
|
|
@@ -1692,7 +1833,8 @@ export function renderSlopeChart(
|
|
|
1692
1833
|
const y = yScale(val);
|
|
1693
1834
|
|
|
1694
1835
|
// Point circle
|
|
1695
|
-
seriesG
|
|
1836
|
+
seriesG
|
|
1837
|
+
.append('circle')
|
|
1696
1838
|
.attr('cx', x)
|
|
1697
1839
|
.attr('cy', y)
|
|
1698
1840
|
.attr('r', 4)
|
|
@@ -1716,7 +1858,8 @@ export function renderSlopeChart(
|
|
|
1716
1858
|
const isLast = i === periods.length - 1;
|
|
1717
1859
|
if (!isLast) {
|
|
1718
1860
|
const adjustedY = leftLabelCollisions.get(i)![idx];
|
|
1719
|
-
seriesG
|
|
1861
|
+
seriesG
|
|
1862
|
+
.append('text')
|
|
1720
1863
|
.attr('x', isFirst ? x - 10 : x)
|
|
1721
1864
|
.attr('y', adjustedY)
|
|
1722
1865
|
.attr('dy', '0.35em')
|
|
@@ -1948,7 +2091,14 @@ export function renderArcDiagram(
|
|
|
1948
2091
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
1949
2092
|
|
|
1950
2093
|
// Title
|
|
1951
|
-
renderChartTitle(
|
|
2094
|
+
renderChartTitle(
|
|
2095
|
+
svg,
|
|
2096
|
+
title,
|
|
2097
|
+
parsed.titleLineNumber,
|
|
2098
|
+
width,
|
|
2099
|
+
textColor,
|
|
2100
|
+
onClickItem
|
|
2101
|
+
);
|
|
1952
2102
|
|
|
1953
2103
|
// Build adjacency map for hover interactions
|
|
1954
2104
|
const neighbors = new Map<string, Set<string>>();
|
|
@@ -2125,13 +2275,18 @@ export function renderArcDiagram(
|
|
|
2125
2275
|
const y = yScale(node)!;
|
|
2126
2276
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2127
2277
|
// Find the first link involving this node (for line number and click target)
|
|
2128
|
-
const nodeLink = links.find(
|
|
2278
|
+
const nodeLink = links.find(
|
|
2279
|
+
(l) => l.source === node || l.target === node
|
|
2280
|
+
);
|
|
2129
2281
|
|
|
2130
2282
|
const nodeG = g
|
|
2131
2283
|
.append('g')
|
|
2132
2284
|
.attr('class', 'arc-node')
|
|
2133
2285
|
.attr('data-node', node)
|
|
2134
|
-
.attr(
|
|
2286
|
+
.attr(
|
|
2287
|
+
'data-line-number',
|
|
2288
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2289
|
+
)
|
|
2135
2290
|
.style('cursor', 'pointer')
|
|
2136
2291
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2137
2292
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2260,13 +2415,18 @@ export function renderArcDiagram(
|
|
|
2260
2415
|
const x = xScale(node)!;
|
|
2261
2416
|
const nodeColor = nodeColorMap.get(node) ?? textColor;
|
|
2262
2417
|
// Find the first link involving this node (for line number and click target)
|
|
2263
|
-
const nodeLink = links.find(
|
|
2418
|
+
const nodeLink = links.find(
|
|
2419
|
+
(l) => l.source === node || l.target === node
|
|
2420
|
+
);
|
|
2264
2421
|
|
|
2265
2422
|
const nodeG = g
|
|
2266
2423
|
.append('g')
|
|
2267
2424
|
.attr('class', 'arc-node')
|
|
2268
2425
|
.attr('data-node', node)
|
|
2269
|
-
.attr(
|
|
2426
|
+
.attr(
|
|
2427
|
+
'data-line-number',
|
|
2428
|
+
nodeLink?.lineNumber ? String(nodeLink.lineNumber) : null
|
|
2429
|
+
)
|
|
2270
2430
|
.style('cursor', 'pointer')
|
|
2271
2431
|
.on('mouseenter', () => handleMouseEnter(node))
|
|
2272
2432
|
.on('mouseleave', handleMouseLeave)
|
|
@@ -2570,6 +2730,26 @@ export function formatDateLabel(dateStr: string): string {
|
|
|
2570
2730
|
return `${month} ${day}, ${year}${timeSuffix}`;
|
|
2571
2731
|
}
|
|
2572
2732
|
|
|
2733
|
+
/**
|
|
2734
|
+
* Formats a boundary label for the time axis.
|
|
2735
|
+
* When both boundaries fall on the same calendar day and have a time component,
|
|
2736
|
+
* returns just the time (e.g. "12:15") to avoid collisions with regular ticks.
|
|
2737
|
+
* Otherwise falls back to the full formatDateLabel.
|
|
2738
|
+
*/
|
|
2739
|
+
function formatBoundaryLabel(dateStr: string, otherDateStr: string): string {
|
|
2740
|
+
const spaceIdx = dateStr.indexOf(' ');
|
|
2741
|
+
const otherSpaceIdx = otherDateStr.indexOf(' ');
|
|
2742
|
+
// Both must have time components and share the same date portion
|
|
2743
|
+
if (spaceIdx !== -1 && otherSpaceIdx !== -1) {
|
|
2744
|
+
const datePart = dateStr.slice(0, spaceIdx);
|
|
2745
|
+
const otherDatePart = otherDateStr.slice(0, otherSpaceIdx);
|
|
2746
|
+
if (datePart === otherDatePart) {
|
|
2747
|
+
return dateStr.slice(spaceIdx + 1); // just "HH:MM"
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return formatDateLabel(dateStr);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2573
2753
|
/**
|
|
2574
2754
|
* Computes adaptive tick marks for a timeline scale.
|
|
2575
2755
|
* - Multi-year spans → year ticks
|
|
@@ -2641,7 +2821,11 @@ export function computeTimeTicks(
|
|
|
2641
2821
|
// Iterate from the start hour boundary
|
|
2642
2822
|
const startDate = fractionalYearToDate(domainMin);
|
|
2643
2823
|
// Round down to nearest step boundary
|
|
2644
|
-
startDate.setMinutes(
|
|
2824
|
+
startDate.setMinutes(
|
|
2825
|
+
Math.floor(startDate.getMinutes() / stepMin) * stepMin,
|
|
2826
|
+
0,
|
|
2827
|
+
0
|
|
2828
|
+
);
|
|
2645
2829
|
|
|
2646
2830
|
while (true) {
|
|
2647
2831
|
const val = dateToFractionalYear(startDate);
|
|
@@ -2662,19 +2846,31 @@ export function computeTimeTicks(
|
|
|
2662
2846
|
else if (spanHours > 24) stepHour = 3;
|
|
2663
2847
|
else if (spanHours > 12) stepHour = 2;
|
|
2664
2848
|
|
|
2849
|
+
// For single-day spans, just show HH:MM without the date prefix
|
|
2850
|
+
const singleDay = spanHours <= 24;
|
|
2851
|
+
|
|
2665
2852
|
const startDate = fractionalYearToDate(domainMin);
|
|
2666
2853
|
// Round down to nearest step boundary
|
|
2667
|
-
startDate.setHours(
|
|
2854
|
+
startDate.setHours(
|
|
2855
|
+
Math.floor(startDate.getHours() / stepHour) * stepHour,
|
|
2856
|
+
0,
|
|
2857
|
+
0,
|
|
2858
|
+
0
|
|
2859
|
+
);
|
|
2668
2860
|
|
|
2669
2861
|
while (true) {
|
|
2670
2862
|
const val = dateToFractionalYear(startDate);
|
|
2671
2863
|
if (val > domainMax) break;
|
|
2672
2864
|
if (val >= domainMin) {
|
|
2673
|
-
const mon = MONTH_ABBR[startDate.getMonth()];
|
|
2674
|
-
const d = startDate.getDate();
|
|
2675
2865
|
const hh = String(startDate.getHours()).padStart(2, '0');
|
|
2676
2866
|
const mm = String(startDate.getMinutes()).padStart(2, '0');
|
|
2677
|
-
|
|
2867
|
+
if (singleDay) {
|
|
2868
|
+
ticks.push({ pos: scale(val), label: `${hh}:${mm}` });
|
|
2869
|
+
} else {
|
|
2870
|
+
const mon = MONTH_ABBR[startDate.getMonth()];
|
|
2871
|
+
const d = startDate.getDate();
|
|
2872
|
+
ticks.push({ pos: scale(val), label: `${mon} ${d} ${hh}:${mm}` });
|
|
2873
|
+
}
|
|
2678
2874
|
}
|
|
2679
2875
|
startDate.setHours(startDate.getHours() + stepHour);
|
|
2680
2876
|
}
|
|
@@ -3073,7 +3269,10 @@ export function renderTimeline(
|
|
|
3073
3269
|
exportDims?: D3ExportDimensions,
|
|
3074
3270
|
activeTagGroup?: string | null,
|
|
3075
3271
|
swimlaneTagGroup?: string | null,
|
|
3076
|
-
onTagStateChange?: (
|
|
3272
|
+
onTagStateChange?: (
|
|
3273
|
+
activeTagGroup: string | null,
|
|
3274
|
+
swimlaneTagGroup: string | null
|
|
3275
|
+
) => void,
|
|
3077
3276
|
viewMode?: boolean
|
|
3078
3277
|
): void {
|
|
3079
3278
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
@@ -3092,7 +3291,11 @@ export function renderTimeline(
|
|
|
3092
3291
|
if (timelineEvents.length === 0) return;
|
|
3093
3292
|
|
|
3094
3293
|
// When sort: tag is set and no explicit swimlane param, use the default
|
|
3095
|
-
if (
|
|
3294
|
+
if (
|
|
3295
|
+
swimlaneTagGroup == null &&
|
|
3296
|
+
timelineSort === 'tag' &&
|
|
3297
|
+
parsed.timelineDefaultSwimlaneTG
|
|
3298
|
+
) {
|
|
3096
3299
|
swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
|
|
3097
3300
|
}
|
|
3098
3301
|
|
|
@@ -3144,12 +3347,8 @@ export function renderTimeline(
|
|
|
3144
3347
|
|
|
3145
3348
|
// Order lanes by earliest event date
|
|
3146
3349
|
const laneEntries = [...buckets.entries()].sort((a, b) => {
|
|
3147
|
-
const aMin = Math.min(
|
|
3148
|
-
|
|
3149
|
-
);
|
|
3150
|
-
const bMin = Math.min(
|
|
3151
|
-
...b[1].map((e) => parseTimelineDate(e.date))
|
|
3152
|
-
);
|
|
3350
|
+
const aMin = Math.min(...a[1].map((e) => parseTimelineDate(e.date)));
|
|
3351
|
+
const bMin = Math.min(...b[1].map((e) => parseTimelineDate(e.date)));
|
|
3153
3352
|
return aMin - bMin;
|
|
3154
3353
|
});
|
|
3155
3354
|
|
|
@@ -3171,7 +3370,11 @@ export function renderTimeline(
|
|
|
3171
3370
|
function eventColor(ev: TimelineEvent): string {
|
|
3172
3371
|
// Tag color takes priority when a tag group is active
|
|
3173
3372
|
if (effectiveColorTG) {
|
|
3174
|
-
const tagColor = resolveTagColor(
|
|
3373
|
+
const tagColor = resolveTagColor(
|
|
3374
|
+
ev.metadata,
|
|
3375
|
+
parsed.timelineTagGroups,
|
|
3376
|
+
effectiveColorTG
|
|
3377
|
+
);
|
|
3175
3378
|
if (tagColor) return tagColor;
|
|
3176
3379
|
}
|
|
3177
3380
|
if (ev.group && groupColorMap.has(ev.group)) {
|
|
@@ -3282,16 +3485,23 @@ export function renderTimeline(
|
|
|
3282
3485
|
el.attr('opacity', val === tagValue ? 1 : FADE_OPACITY);
|
|
3283
3486
|
});
|
|
3284
3487
|
g.selectAll<SVGGElement, unknown>('.tl-legend-item, .tl-lane-header').attr(
|
|
3285
|
-
'opacity',
|
|
3488
|
+
'opacity',
|
|
3489
|
+
FADE_OPACITY
|
|
3490
|
+
);
|
|
3491
|
+
g.selectAll<SVGGElement, unknown>('.tl-marker').attr(
|
|
3492
|
+
'opacity',
|
|
3493
|
+
FADE_OPACITY
|
|
3286
3494
|
);
|
|
3287
|
-
g.selectAll<SVGGElement, unknown>('.tl-marker').attr('opacity', FADE_OPACITY);
|
|
3288
3495
|
// Fade legend entry dots/labels that don't match (keep group pill visible)
|
|
3289
3496
|
g.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry').each(function () {
|
|
3290
3497
|
const el = d3Selection.select(this);
|
|
3291
3498
|
const entryValue = el.attr('data-legend-entry');
|
|
3292
3499
|
if (entryValue === '__group__') return; // keep group pill at full opacity
|
|
3293
3500
|
const entryGroup = el.attr('data-tag-group');
|
|
3294
|
-
el.attr(
|
|
3501
|
+
el.attr(
|
|
3502
|
+
'opacity',
|
|
3503
|
+
entryGroup === tagKey && entryValue === tagValue ? 1 : FADE_OPACITY
|
|
3504
|
+
);
|
|
3295
3505
|
});
|
|
3296
3506
|
}
|
|
3297
3507
|
|
|
@@ -3312,7 +3522,8 @@ export function renderTimeline(
|
|
|
3312
3522
|
// VERTICAL orientation (time flows top→bottom)
|
|
3313
3523
|
// ================================================================
|
|
3314
3524
|
if (isVertical) {
|
|
3315
|
-
const useGroupedVertical =
|
|
3525
|
+
const useGroupedVertical =
|
|
3526
|
+
tagLanes != null ||
|
|
3316
3527
|
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
3317
3528
|
if (useGroupedVertical) {
|
|
3318
3529
|
// === GROUPED: one column/lane per group, vertical ===
|
|
@@ -3371,7 +3582,14 @@ export function renderTimeline(
|
|
|
3371
3582
|
.append('g')
|
|
3372
3583
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3373
3584
|
|
|
3374
|
-
renderChartTitle(
|
|
3585
|
+
renderChartTitle(
|
|
3586
|
+
svg,
|
|
3587
|
+
title,
|
|
3588
|
+
parsed.titleLineNumber,
|
|
3589
|
+
width,
|
|
3590
|
+
textColor,
|
|
3591
|
+
onClickItem
|
|
3592
|
+
);
|
|
3375
3593
|
|
|
3376
3594
|
renderEras(
|
|
3377
3595
|
g,
|
|
@@ -3409,8 +3627,8 @@ export function renderTimeline(
|
|
|
3409
3627
|
textColor,
|
|
3410
3628
|
minDate,
|
|
3411
3629
|
maxDate,
|
|
3412
|
-
|
|
3413
|
-
|
|
3630
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
3631
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3414
3632
|
);
|
|
3415
3633
|
}
|
|
3416
3634
|
|
|
@@ -3621,7 +3839,14 @@ export function renderTimeline(
|
|
|
3621
3839
|
.append('g')
|
|
3622
3840
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3623
3841
|
|
|
3624
|
-
renderChartTitle(
|
|
3842
|
+
renderChartTitle(
|
|
3843
|
+
svg,
|
|
3844
|
+
title,
|
|
3845
|
+
parsed.titleLineNumber,
|
|
3846
|
+
width,
|
|
3847
|
+
textColor,
|
|
3848
|
+
onClickItem
|
|
3849
|
+
);
|
|
3625
3850
|
|
|
3626
3851
|
renderEras(
|
|
3627
3852
|
g,
|
|
@@ -3659,8 +3884,8 @@ export function renderTimeline(
|
|
|
3659
3884
|
textColor,
|
|
3660
3885
|
minDate,
|
|
3661
3886
|
maxDate,
|
|
3662
|
-
|
|
3663
|
-
|
|
3887
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
3888
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3664
3889
|
);
|
|
3665
3890
|
}
|
|
3666
3891
|
|
|
@@ -3747,8 +3972,7 @@ export function renderTimeline(
|
|
|
3747
3972
|
if (ev.uncertain) {
|
|
3748
3973
|
const gradientId = `uncertain-v-${ev.lineNumber}`;
|
|
3749
3974
|
const strokeGradientId = `uncertain-v-s-${ev.lineNumber}`;
|
|
3750
|
-
const defs =
|
|
3751
|
-
svg.select('defs').node() || svg.append('defs').node();
|
|
3975
|
+
const defs = svg.select('defs').node() || svg.append('defs').node();
|
|
3752
3976
|
const defsEl = d3Selection.select(defs as Element);
|
|
3753
3977
|
defsEl
|
|
3754
3978
|
.append('linearGradient')
|
|
@@ -3862,8 +4086,8 @@ export function renderTimeline(
|
|
|
3862
4086
|
const BAR_H = 22; // range bar thickness (tall enough for text inside)
|
|
3863
4087
|
const GROUP_GAP = 12; // vertical gap between group swim-lanes
|
|
3864
4088
|
|
|
3865
|
-
const useGroupedHorizontal =
|
|
3866
|
-
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
4089
|
+
const useGroupedHorizontal =
|
|
4090
|
+
tagLanes != null || (timelineSort === 'group' && timelineGroups.length > 0);
|
|
3867
4091
|
if (useGroupedHorizontal) {
|
|
3868
4092
|
// === GROUPED: swim-lanes stacked vertically, events on own rows ===
|
|
3869
4093
|
let lanes: Lane[];
|
|
@@ -3896,7 +4120,11 @@ export function renderTimeline(
|
|
|
3896
4120
|
// Group-sorted doesn't need legend space (group names shown on left)
|
|
3897
4121
|
const baseTopMargin = title ? 50 : 20;
|
|
3898
4122
|
const margin = {
|
|
3899
|
-
top:
|
|
4123
|
+
top:
|
|
4124
|
+
baseTopMargin +
|
|
4125
|
+
(timelineScale ? 40 : 0) +
|
|
4126
|
+
markerMargin +
|
|
4127
|
+
tagLegendReserve,
|
|
3900
4128
|
right: 40,
|
|
3901
4129
|
bottom: 40 + scaleMargin,
|
|
3902
4130
|
left: dynamicLeftMargin,
|
|
@@ -3922,7 +4150,14 @@ export function renderTimeline(
|
|
|
3922
4150
|
.append('g')
|
|
3923
4151
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
3924
4152
|
|
|
3925
|
-
renderChartTitle(
|
|
4153
|
+
renderChartTitle(
|
|
4154
|
+
svg,
|
|
4155
|
+
title,
|
|
4156
|
+
parsed.titleLineNumber,
|
|
4157
|
+
width,
|
|
4158
|
+
textColor,
|
|
4159
|
+
onClickItem
|
|
4160
|
+
);
|
|
3926
4161
|
|
|
3927
4162
|
renderEras(
|
|
3928
4163
|
g,
|
|
@@ -3960,8 +4195,8 @@ export function renderTimeline(
|
|
|
3960
4195
|
textColor,
|
|
3961
4196
|
minDate,
|
|
3962
4197
|
maxDate,
|
|
3963
|
-
|
|
3964
|
-
|
|
4198
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
4199
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
3965
4200
|
);
|
|
3966
4201
|
}
|
|
3967
4202
|
|
|
@@ -4223,7 +4458,14 @@ export function renderTimeline(
|
|
|
4223
4458
|
.append('g')
|
|
4224
4459
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
|
4225
4460
|
|
|
4226
|
-
renderChartTitle(
|
|
4461
|
+
renderChartTitle(
|
|
4462
|
+
svg,
|
|
4463
|
+
title,
|
|
4464
|
+
parsed.titleLineNumber,
|
|
4465
|
+
width,
|
|
4466
|
+
textColor,
|
|
4467
|
+
onClickItem
|
|
4468
|
+
);
|
|
4227
4469
|
|
|
4228
4470
|
renderEras(
|
|
4229
4471
|
g,
|
|
@@ -4261,8 +4503,8 @@ export function renderTimeline(
|
|
|
4261
4503
|
textColor,
|
|
4262
4504
|
minDate,
|
|
4263
4505
|
maxDate,
|
|
4264
|
-
|
|
4265
|
-
|
|
4506
|
+
formatBoundaryLabel(earliestStartDateStr, latestEndDateStr),
|
|
4507
|
+
formatBoundaryLabel(latestEndDateStr, earliestStartDateStr)
|
|
4266
4508
|
);
|
|
4267
4509
|
}
|
|
4268
4510
|
|
|
@@ -4501,13 +4743,17 @@ export function renderTimeline(
|
|
|
4501
4743
|
expandedWidth: number;
|
|
4502
4744
|
};
|
|
4503
4745
|
const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
|
|
4504
|
-
const pillW =
|
|
4746
|
+
const pillW =
|
|
4747
|
+
measureLegendText(g.name, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4505
4748
|
// Expanded: pill + icon (unless viewMode) + entries
|
|
4506
4749
|
const iconSpace = viewMode ? 8 : LG_ICON_W + 4;
|
|
4507
4750
|
let entryX = LG_CAPSULE_PAD + pillW + iconSpace;
|
|
4508
4751
|
for (const entry of g.entries) {
|
|
4509
4752
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4510
|
-
entryX =
|
|
4753
|
+
entryX =
|
|
4754
|
+
textX +
|
|
4755
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
4756
|
+
LG_ENTRY_TRAIL;
|
|
4511
4757
|
}
|
|
4512
4758
|
return {
|
|
4513
4759
|
group: g,
|
|
@@ -4527,7 +4773,8 @@ export function renderTimeline(
|
|
|
4527
4773
|
y: number,
|
|
4528
4774
|
isSwimActive: boolean
|
|
4529
4775
|
) {
|
|
4530
|
-
const iconG = parent
|
|
4776
|
+
const iconG = parent
|
|
4777
|
+
.append('g')
|
|
4531
4778
|
.attr('class', 'tl-swimlane-icon')
|
|
4532
4779
|
.attr('transform', `translate(${x}, ${y})`)
|
|
4533
4780
|
.style('cursor', 'pointer');
|
|
@@ -4540,7 +4787,8 @@ export function renderTimeline(
|
|
|
4540
4787
|
{ y: 8, w: 6 },
|
|
4541
4788
|
];
|
|
4542
4789
|
for (const bar of bars) {
|
|
4543
|
-
iconG
|
|
4790
|
+
iconG
|
|
4791
|
+
.append('rect')
|
|
4544
4792
|
.attr('x', 0)
|
|
4545
4793
|
.attr('y', bar.y)
|
|
4546
4794
|
.attr('width', bar.w)
|
|
@@ -4555,8 +4803,16 @@ export function renderTimeline(
|
|
|
4555
4803
|
/** Full re-render with updated swimlane state */
|
|
4556
4804
|
function relayout() {
|
|
4557
4805
|
renderTimeline(
|
|
4558
|
-
container,
|
|
4559
|
-
|
|
4806
|
+
container,
|
|
4807
|
+
parsed,
|
|
4808
|
+
palette,
|
|
4809
|
+
isDark,
|
|
4810
|
+
onClickItem,
|
|
4811
|
+
exportDims,
|
|
4812
|
+
currentActiveGroup,
|
|
4813
|
+
currentSwimlaneGroup,
|
|
4814
|
+
onTagStateChange,
|
|
4815
|
+
viewMode
|
|
4560
4816
|
);
|
|
4561
4817
|
}
|
|
4562
4818
|
|
|
@@ -4566,7 +4822,8 @@ export function renderTimeline(
|
|
|
4566
4822
|
mainSvg.selectAll('.tl-tag-legend-container').remove();
|
|
4567
4823
|
|
|
4568
4824
|
// Effective color source: explicit color group > swimlane group
|
|
4569
|
-
const effectiveColorKey =
|
|
4825
|
+
const effectiveColorKey =
|
|
4826
|
+
(currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
|
|
4570
4827
|
|
|
4571
4828
|
// In view mode, only show the color-driving tag group (expanded, non-interactive).
|
|
4572
4829
|
// Skip the swimlane group if it's separate from the color group (lane headers already label it).
|
|
@@ -4581,32 +4838,43 @@ export function renderTimeline(
|
|
|
4581
4838
|
if (visibleGroups.length === 0) return;
|
|
4582
4839
|
|
|
4583
4840
|
// Compute total width and center horizontally in SVG
|
|
4584
|
-
const totalW =
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4841
|
+
const totalW =
|
|
4842
|
+
visibleGroups.reduce((s, lg) => {
|
|
4843
|
+
const isActive =
|
|
4844
|
+
viewMode ||
|
|
4845
|
+
(currentActiveGroup != null &&
|
|
4846
|
+
lg.group.name.toLowerCase() ===
|
|
4847
|
+
currentActiveGroup.toLowerCase());
|
|
4848
|
+
return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
|
|
4849
|
+
}, 0) +
|
|
4850
|
+
(visibleGroups.length - 1) * LG_GROUP_GAP;
|
|
4590
4851
|
|
|
4591
4852
|
let cx = (width - totalW) / 2;
|
|
4592
4853
|
|
|
4593
4854
|
// Legend container for data-legend-active attribute
|
|
4594
|
-
const legendContainer = mainSvg
|
|
4855
|
+
const legendContainer = mainSvg
|
|
4856
|
+
.append('g')
|
|
4595
4857
|
.attr('class', 'tl-tag-legend-container');
|
|
4596
4858
|
if (currentActiveGroup) {
|
|
4597
|
-
legendContainer.attr(
|
|
4859
|
+
legendContainer.attr(
|
|
4860
|
+
'data-legend-active',
|
|
4861
|
+
currentActiveGroup.toLowerCase()
|
|
4862
|
+
);
|
|
4598
4863
|
}
|
|
4599
4864
|
|
|
4600
4865
|
for (const lg of visibleGroups) {
|
|
4601
4866
|
const groupKey = lg.group.name.toLowerCase();
|
|
4602
|
-
const isActive =
|
|
4867
|
+
const isActive =
|
|
4868
|
+
viewMode ||
|
|
4603
4869
|
(currentActiveGroup != null &&
|
|
4604
4870
|
currentActiveGroup.toLowerCase() === groupKey);
|
|
4605
|
-
const isSwimActive =
|
|
4871
|
+
const isSwimActive =
|
|
4872
|
+
currentSwimlaneGroup != null &&
|
|
4606
4873
|
currentSwimlaneGroup.toLowerCase() === groupKey;
|
|
4607
4874
|
|
|
4608
4875
|
const pillLabel = lg.group.name;
|
|
4609
|
-
const pillWidth =
|
|
4876
|
+
const pillWidth =
|
|
4877
|
+
measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
|
|
4610
4878
|
|
|
4611
4879
|
const gEl = legendContainer
|
|
4612
4880
|
.append('g')
|
|
@@ -4617,19 +4885,19 @@ export function renderTimeline(
|
|
|
4617
4885
|
.attr('data-legend-entry', '__group__');
|
|
4618
4886
|
|
|
4619
4887
|
if (!viewMode) {
|
|
4620
|
-
gEl
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
});
|
|
4888
|
+
gEl.style('cursor', 'pointer').on('click', () => {
|
|
4889
|
+
currentActiveGroup =
|
|
4890
|
+
currentActiveGroup === groupKey ? null : groupKey;
|
|
4891
|
+
drawLegend();
|
|
4892
|
+
recolorEvents();
|
|
4893
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4894
|
+
});
|
|
4628
4895
|
}
|
|
4629
4896
|
|
|
4630
4897
|
// Outer capsule background (active only)
|
|
4631
4898
|
if (isActive) {
|
|
4632
|
-
gEl
|
|
4899
|
+
gEl
|
|
4900
|
+
.append('rect')
|
|
4633
4901
|
.attr('width', lg.expandedWidth)
|
|
4634
4902
|
.attr('height', LG_HEIGHT)
|
|
4635
4903
|
.attr('rx', LG_HEIGHT / 2)
|
|
@@ -4641,7 +4909,8 @@ export function renderTimeline(
|
|
|
4641
4909
|
const pillH = LG_HEIGHT - (isActive ? LG_CAPSULE_PAD * 2 : 0);
|
|
4642
4910
|
|
|
4643
4911
|
// Pill background
|
|
4644
|
-
gEl
|
|
4912
|
+
gEl
|
|
4913
|
+
.append('rect')
|
|
4645
4914
|
.attr('x', pillXOff)
|
|
4646
4915
|
.attr('y', pillYOff)
|
|
4647
4916
|
.attr('width', pillWidth)
|
|
@@ -4651,7 +4920,8 @@ export function renderTimeline(
|
|
|
4651
4920
|
|
|
4652
4921
|
// Active pill border
|
|
4653
4922
|
if (isActive) {
|
|
4654
|
-
gEl
|
|
4923
|
+
gEl
|
|
4924
|
+
.append('rect')
|
|
4655
4925
|
.attr('x', pillXOff)
|
|
4656
4926
|
.attr('y', pillYOff)
|
|
4657
4927
|
.attr('width', pillWidth)
|
|
@@ -4663,7 +4933,8 @@ export function renderTimeline(
|
|
|
4663
4933
|
}
|
|
4664
4934
|
|
|
4665
4935
|
// Pill text
|
|
4666
|
-
gEl
|
|
4936
|
+
gEl
|
|
4937
|
+
.append('text')
|
|
4667
4938
|
.attr('x', pillXOff + pillWidth / 2)
|
|
4668
4939
|
.attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
|
|
4669
4940
|
.attr('font-size', LG_PILL_FONT_SIZE)
|
|
@@ -4685,7 +4956,8 @@ export function renderTimeline(
|
|
|
4685
4956
|
.attr('data-swimlane-toggle', groupKey)
|
|
4686
4957
|
.on('click', (event: MouseEvent) => {
|
|
4687
4958
|
event.stopPropagation();
|
|
4688
|
-
currentSwimlaneGroup =
|
|
4959
|
+
currentSwimlaneGroup =
|
|
4960
|
+
currentSwimlaneGroup === groupKey ? null : groupKey;
|
|
4689
4961
|
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4690
4962
|
relayout();
|
|
4691
4963
|
});
|
|
@@ -4698,7 +4970,8 @@ export function renderTimeline(
|
|
|
4698
4970
|
const tagKey = lg.group.name.toLowerCase();
|
|
4699
4971
|
const tagVal = entry.value.toLowerCase();
|
|
4700
4972
|
|
|
4701
|
-
const entryG = gEl
|
|
4973
|
+
const entryG = gEl
|
|
4974
|
+
.append('g')
|
|
4702
4975
|
.attr('class', 'tl-tag-legend-entry')
|
|
4703
4976
|
.attr('data-tag-group', tagKey)
|
|
4704
4977
|
.attr('data-legend-entry', tagVal);
|
|
@@ -4709,18 +4982,24 @@ export function renderTimeline(
|
|
|
4709
4982
|
.on('mouseenter', (event: MouseEvent) => {
|
|
4710
4983
|
event.stopPropagation();
|
|
4711
4984
|
fadeToTagValue(mainG, tagKey, tagVal);
|
|
4712
|
-
mainSvg
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4985
|
+
mainSvg
|
|
4986
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
4987
|
+
.each(function () {
|
|
4988
|
+
const el = d3Selection.select(this);
|
|
4989
|
+
const ev = el.attr('data-legend-entry');
|
|
4990
|
+
if (ev === '__group__') return;
|
|
4991
|
+
const eg = el.attr('data-tag-group');
|
|
4992
|
+
el.attr(
|
|
4993
|
+
'opacity',
|
|
4994
|
+
eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
|
|
4995
|
+
);
|
|
4996
|
+
});
|
|
4719
4997
|
})
|
|
4720
4998
|
.on('mouseleave', (event: MouseEvent) => {
|
|
4721
4999
|
event.stopPropagation();
|
|
4722
5000
|
fadeReset(mainG);
|
|
4723
|
-
mainSvg
|
|
5001
|
+
mainSvg
|
|
5002
|
+
.selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
|
|
4724
5003
|
.attr('opacity', 1);
|
|
4725
5004
|
})
|
|
4726
5005
|
.on('click', (event: MouseEvent) => {
|
|
@@ -4728,14 +5007,16 @@ export function renderTimeline(
|
|
|
4728
5007
|
});
|
|
4729
5008
|
}
|
|
4730
5009
|
|
|
4731
|
-
entryG
|
|
5010
|
+
entryG
|
|
5011
|
+
.append('circle')
|
|
4732
5012
|
.attr('cx', entryX + LG_DOT_R)
|
|
4733
5013
|
.attr('cy', LG_HEIGHT / 2)
|
|
4734
5014
|
.attr('r', LG_DOT_R)
|
|
4735
5015
|
.attr('fill', entry.color);
|
|
4736
5016
|
|
|
4737
5017
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4738
|
-
entryG
|
|
5018
|
+
entryG
|
|
5019
|
+
.append('text')
|
|
4739
5020
|
.attr('x', textX)
|
|
4740
5021
|
.attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
|
|
4741
5022
|
.attr('font-size', LG_ENTRY_FONT_SIZE)
|
|
@@ -4743,7 +5024,10 @@ export function renderTimeline(
|
|
|
4743
5024
|
.attr('fill', palette.textMuted)
|
|
4744
5025
|
.text(entry.value);
|
|
4745
5026
|
|
|
4746
|
-
entryX =
|
|
5027
|
+
entryX =
|
|
5028
|
+
textX +
|
|
5029
|
+
measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
|
|
5030
|
+
LG_ENTRY_TRAIL;
|
|
4747
5031
|
}
|
|
4748
5032
|
}
|
|
4749
5033
|
|
|
@@ -4768,16 +5052,27 @@ export function renderTimeline(
|
|
|
4768
5052
|
let color: string;
|
|
4769
5053
|
if (colorTG) {
|
|
4770
5054
|
const tagColor = resolveTagColor(
|
|
4771
|
-
ev.metadata,
|
|
5055
|
+
ev.metadata,
|
|
5056
|
+
parsed.timelineTagGroups,
|
|
5057
|
+
colorTG
|
|
4772
5058
|
);
|
|
4773
|
-
color =
|
|
4774
|
-
|
|
5059
|
+
color =
|
|
5060
|
+
tagColor ??
|
|
5061
|
+
(ev.group && groupColorMap.has(ev.group)
|
|
5062
|
+
? groupColorMap.get(ev.group)!
|
|
5063
|
+
: textColor);
|
|
4775
5064
|
} else {
|
|
4776
|
-
color =
|
|
4777
|
-
|
|
5065
|
+
color =
|
|
5066
|
+
ev.group && groupColorMap.has(ev.group)
|
|
5067
|
+
? groupColorMap.get(ev.group)!
|
|
5068
|
+
: textColor;
|
|
4778
5069
|
}
|
|
4779
|
-
el.selectAll('rect')
|
|
4780
|
-
|
|
5070
|
+
el.selectAll('rect')
|
|
5071
|
+
.attr('fill', mix(color, bg, 30))
|
|
5072
|
+
.attr('stroke', color);
|
|
5073
|
+
el.selectAll('circle:not(.tl-event-point-outline)')
|
|
5074
|
+
.attr('fill', mix(color, bg, 30))
|
|
5075
|
+
.attr('stroke', color);
|
|
4781
5076
|
});
|
|
4782
5077
|
}
|
|
4783
5078
|
|
|
@@ -4834,7 +5129,14 @@ export function renderWordCloud(
|
|
|
4834
5129
|
|
|
4835
5130
|
const rotateFn = getRotateFn(cloudOptions.rotate);
|
|
4836
5131
|
|
|
4837
|
-
renderChartTitle(
|
|
5132
|
+
renderChartTitle(
|
|
5133
|
+
svg,
|
|
5134
|
+
title,
|
|
5135
|
+
parsed.titleLineNumber,
|
|
5136
|
+
width,
|
|
5137
|
+
textColor,
|
|
5138
|
+
onClickItem
|
|
5139
|
+
);
|
|
4838
5140
|
|
|
4839
5141
|
const g = svg
|
|
4840
5142
|
.append('g')
|
|
@@ -5141,7 +5443,8 @@ export function renderVenn(
|
|
|
5141
5443
|
const labelTextPad = 4;
|
|
5142
5444
|
|
|
5143
5445
|
for (let i = 0; i < n; i++) {
|
|
5144
|
-
const estimatedWidth =
|
|
5446
|
+
const estimatedWidth =
|
|
5447
|
+
vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
|
|
5145
5448
|
const dx = rawCircles[i].x - clusterCx;
|
|
5146
5449
|
const dy = rawCircles[i].y - clusterCy;
|
|
5147
5450
|
if (Math.abs(dx) >= Math.abs(dy)) {
|
|
@@ -5168,13 +5471,27 @@ export function renderVenn(
|
|
|
5168
5471
|
const scaledR = circles[0].r;
|
|
5169
5472
|
|
|
5170
5473
|
// Suppress WebKit focus ring on interactive SVG elements
|
|
5171
|
-
svg
|
|
5474
|
+
svg
|
|
5475
|
+
.append('style')
|
|
5476
|
+
.text('circle:focus, circle:focus-visible { outline: none !important; }');
|
|
5172
5477
|
|
|
5173
5478
|
// Title
|
|
5174
|
-
renderChartTitle(
|
|
5479
|
+
renderChartTitle(
|
|
5480
|
+
svg,
|
|
5481
|
+
title,
|
|
5482
|
+
parsed.titleLineNumber,
|
|
5483
|
+
width,
|
|
5484
|
+
textColor,
|
|
5485
|
+
onClickItem
|
|
5486
|
+
);
|
|
5175
5487
|
|
|
5176
5488
|
// ── Semi-transparent filled circles (non-interactive) ──
|
|
5177
|
-
const circleEls: d3Selection.Selection<
|
|
5489
|
+
const circleEls: d3Selection.Selection<
|
|
5490
|
+
SVGCircleElement,
|
|
5491
|
+
unknown,
|
|
5492
|
+
null,
|
|
5493
|
+
undefined
|
|
5494
|
+
>[] = [];
|
|
5178
5495
|
const circleGroup = svg.append('g');
|
|
5179
5496
|
circles.forEach((c, i) => {
|
|
5180
5497
|
const el = circleGroup
|
|
@@ -5201,10 +5518,13 @@ export function renderVenn(
|
|
|
5201
5518
|
|
|
5202
5519
|
// Individual circle clipPaths
|
|
5203
5520
|
circles.forEach((c, i) => {
|
|
5204
|
-
defs
|
|
5521
|
+
defs
|
|
5522
|
+
.append('clipPath')
|
|
5205
5523
|
.attr('id', `vcp-${i}`)
|
|
5206
5524
|
.append('circle')
|
|
5207
|
-
.attr('cx', c.x)
|
|
5525
|
+
.attr('cx', c.x)
|
|
5526
|
+
.attr('cy', c.y)
|
|
5527
|
+
.attr('r', c.r);
|
|
5208
5528
|
});
|
|
5209
5529
|
|
|
5210
5530
|
// All region index-sets: exclusive then intersection subsets
|
|
@@ -5216,57 +5536,79 @@ export function renderVenn(
|
|
|
5216
5536
|
}
|
|
5217
5537
|
|
|
5218
5538
|
const overlayGroup = svg.append('g').style('pointer-events', 'none');
|
|
5219
|
-
const overlayEls = new Map<
|
|
5539
|
+
const overlayEls = new Map<
|
|
5540
|
+
string,
|
|
5541
|
+
d3Selection.Selection<SVGRectElement, unknown, null, undefined>
|
|
5542
|
+
>();
|
|
5220
5543
|
|
|
5221
5544
|
for (const idxs of regionIdxSets) {
|
|
5222
5545
|
const key = idxs.join('-');
|
|
5223
|
-
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5546
|
+
const excluded = Array.from({ length: n }, (_, j) => j).filter(
|
|
5547
|
+
(j) => !idxs.includes(j)
|
|
5548
|
+
);
|
|
5224
5549
|
|
|
5225
5550
|
// Build nested clipPath for intersection of all idxs
|
|
5226
5551
|
let clipId = `vcp-${idxs[0]}`;
|
|
5227
5552
|
for (let k = 1; k < idxs.length; k++) {
|
|
5228
5553
|
const nestedId = `vcp-n-${idxs.slice(0, k + 1).join('-')}`;
|
|
5229
5554
|
const ci = idxs[k];
|
|
5230
|
-
defs
|
|
5555
|
+
defs
|
|
5556
|
+
.append('clipPath')
|
|
5231
5557
|
.attr('id', nestedId)
|
|
5232
5558
|
.append('circle')
|
|
5233
|
-
.attr('cx', circles[ci].x)
|
|
5559
|
+
.attr('cx', circles[ci].x)
|
|
5560
|
+
.attr('cy', circles[ci].y)
|
|
5561
|
+
.attr('r', circles[ci].r)
|
|
5234
5562
|
.attr('clip-path', `url(#${clipId})`);
|
|
5235
5563
|
clipId = nestedId;
|
|
5236
5564
|
}
|
|
5237
5565
|
|
|
5238
5566
|
// Determine line number for this region (for editor sync)
|
|
5239
|
-
let regionLineNumber: number | null = null;
|
|
5567
|
+
let regionLineNumber: number | null = null; // eslint-disable-line no-useless-assignment
|
|
5240
5568
|
if (idxs.length === 1) {
|
|
5241
5569
|
regionLineNumber = vennSets[idxs[0]].lineNumber;
|
|
5242
5570
|
} else {
|
|
5243
|
-
const sortedNames = idxs.map(i => vennSets[i].name).sort();
|
|
5571
|
+
const sortedNames = idxs.map((i) => vennSets[i].name).sort();
|
|
5244
5572
|
const ov = vennOverlaps.find(
|
|
5245
|
-
(o) =>
|
|
5573
|
+
(o) =>
|
|
5574
|
+
o.sets.length === sortedNames.length &&
|
|
5575
|
+
o.sets.every((s, k) => s === sortedNames[k])
|
|
5246
5576
|
);
|
|
5247
5577
|
regionLineNumber = ov?.lineNumber ?? null;
|
|
5248
5578
|
}
|
|
5249
5579
|
|
|
5250
|
-
const el = overlayGroup
|
|
5251
|
-
.
|
|
5252
|
-
.attr('
|
|
5580
|
+
const el = overlayGroup
|
|
5581
|
+
.append('rect')
|
|
5582
|
+
.attr('x', 0)
|
|
5583
|
+
.attr('y', 0)
|
|
5584
|
+
.attr('width', width)
|
|
5585
|
+
.attr('height', height)
|
|
5253
5586
|
.attr('fill', 'white')
|
|
5254
5587
|
.attr('fill-opacity', 0)
|
|
5255
5588
|
.attr('class', 'venn-region-overlay')
|
|
5256
|
-
.attr(
|
|
5589
|
+
.attr(
|
|
5590
|
+
'data-line-number',
|
|
5591
|
+
regionLineNumber != null ? String(regionLineNumber) : '0'
|
|
5592
|
+
)
|
|
5257
5593
|
.attr('clip-path', `url(#${clipId})`);
|
|
5258
5594
|
|
|
5259
5595
|
if (excluded.length > 0) {
|
|
5260
5596
|
// Mask subtracts excluded circles so only the exact region shape highlights
|
|
5261
5597
|
const maskId = `vvm-${key}`;
|
|
5262
5598
|
const mask = defs.append('mask').attr('id', maskId);
|
|
5263
|
-
mask
|
|
5264
|
-
.
|
|
5265
|
-
.attr('
|
|
5599
|
+
mask
|
|
5600
|
+
.append('rect')
|
|
5601
|
+
.attr('x', 0)
|
|
5602
|
+
.attr('y', 0)
|
|
5603
|
+
.attr('width', width)
|
|
5604
|
+
.attr('height', height)
|
|
5266
5605
|
.attr('fill', 'white');
|
|
5267
5606
|
for (const j of excluded) {
|
|
5268
|
-
mask
|
|
5269
|
-
.
|
|
5607
|
+
mask
|
|
5608
|
+
.append('circle')
|
|
5609
|
+
.attr('cx', circles[j].x)
|
|
5610
|
+
.attr('cy', circles[j].y)
|
|
5611
|
+
.attr('r', circles[j].r)
|
|
5270
5612
|
.attr('fill', 'black');
|
|
5271
5613
|
}
|
|
5272
5614
|
el.attr('mask', `url(#${maskId})`);
|
|
@@ -5277,10 +5619,12 @@ export function renderVenn(
|
|
|
5277
5619
|
|
|
5278
5620
|
const showRegionOverlay = (idxs: number[]) => {
|
|
5279
5621
|
const key = [...idxs].sort((a, b) => a - b).join('-');
|
|
5280
|
-
overlayEls.forEach((el, k) =>
|
|
5622
|
+
overlayEls.forEach((el, k) =>
|
|
5623
|
+
el.attr('fill-opacity', k === key ? 0 : 0.55)
|
|
5624
|
+
);
|
|
5281
5625
|
};
|
|
5282
5626
|
const hideAllOverlays = () => {
|
|
5283
|
-
overlayEls.forEach(el => el.attr('fill-opacity', 0));
|
|
5627
|
+
overlayEls.forEach((el) => el.attr('fill-opacity', 0));
|
|
5284
5628
|
};
|
|
5285
5629
|
|
|
5286
5630
|
// ── Labels ──
|
|
@@ -5289,7 +5633,9 @@ export function renderVenn(
|
|
|
5289
5633
|
|
|
5290
5634
|
function exclusiveHSpan(px: number, py: number, ci: number): number {
|
|
5291
5635
|
const dy = py - circles[ci].y;
|
|
5292
|
-
const halfChord = Math.sqrt(
|
|
5636
|
+
const halfChord = Math.sqrt(
|
|
5637
|
+
Math.max(0, circles[ci].r * circles[ci].r - dy * dy)
|
|
5638
|
+
);
|
|
5293
5639
|
let left = circles[ci].x - halfChord;
|
|
5294
5640
|
let right = circles[ci].x + halfChord;
|
|
5295
5641
|
for (let j = 0; j < n; j++) {
|
|
@@ -5320,11 +5666,14 @@ export function renderVenn(
|
|
|
5320
5666
|
const centroid = regionCentroid(circles, inside);
|
|
5321
5667
|
|
|
5322
5668
|
const availW = exclusiveHSpan(centroid.x, centroid.y, i);
|
|
5323
|
-
const fitFont = Math.min(
|
|
5324
|
-
|
|
5669
|
+
const fitFont = Math.min(
|
|
5670
|
+
MAX_FONT,
|
|
5671
|
+
Math.max(MIN_FONT, (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO))
|
|
5672
|
+
);
|
|
5325
5673
|
const estTextW = text.length * CH_RATIO * fitFont;
|
|
5326
5674
|
|
|
5327
|
-
const fitsInside =
|
|
5675
|
+
const fitsInside =
|
|
5676
|
+
estTextW + INTERNAL_PAD * 2 < availW &&
|
|
5328
5677
|
pointInCircle({ x: centroid.x, y: centroid.y - fitFont / 2 }, c) &&
|
|
5329
5678
|
pointInCircle({ x: centroid.x, y: centroid.y + fitFont / 2 }, c);
|
|
5330
5679
|
|
|
@@ -5343,7 +5692,13 @@ export function renderVenn(
|
|
|
5343
5692
|
let dx = c.x - gcx;
|
|
5344
5693
|
let dy = c.y - gcy;
|
|
5345
5694
|
const mag = Math.sqrt(dx * dx + dy * dy);
|
|
5346
|
-
if (mag < 1e-6) {
|
|
5695
|
+
if (mag < 1e-6) {
|
|
5696
|
+
dx = 1;
|
|
5697
|
+
dy = 0;
|
|
5698
|
+
} else {
|
|
5699
|
+
dx /= mag;
|
|
5700
|
+
dy /= mag;
|
|
5701
|
+
}
|
|
5347
5702
|
|
|
5348
5703
|
const exitX = c.x + dx * c.r;
|
|
5349
5704
|
const exitY = c.y + dy * c.r;
|
|
@@ -5354,8 +5709,10 @@ export function renderVenn(
|
|
|
5354
5709
|
|
|
5355
5710
|
labelGroup
|
|
5356
5711
|
.append('line')
|
|
5357
|
-
.attr('x1', edgeX)
|
|
5358
|
-
.attr('
|
|
5712
|
+
.attr('x1', edgeX)
|
|
5713
|
+
.attr('y1', edgeY)
|
|
5714
|
+
.attr('x2', stubEndX)
|
|
5715
|
+
.attr('y2', stubEndY)
|
|
5359
5716
|
.attr('stroke', textColor)
|
|
5360
5717
|
.attr('stroke-width', 1);
|
|
5361
5718
|
|
|
@@ -5382,7 +5739,8 @@ export function renderVenn(
|
|
|
5382
5739
|
|
|
5383
5740
|
// ── Overlap labels (inline at region centroid) ──
|
|
5384
5741
|
function overlapHSpan(py: number, idxs: number[]): number {
|
|
5385
|
-
let left = -Infinity,
|
|
5742
|
+
let left = -Infinity,
|
|
5743
|
+
right = Infinity;
|
|
5386
5744
|
for (const ci of idxs) {
|
|
5387
5745
|
const dy = py - circles[ci].y;
|
|
5388
5746
|
if (Math.abs(dy) >= circles[ci].r) return 0;
|
|
@@ -5412,8 +5770,13 @@ export function renderVenn(
|
|
|
5412
5770
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5413
5771
|
const centroid = regionCentroid(circles, inside);
|
|
5414
5772
|
const availW = overlapHSpan(centroid.y, idxs);
|
|
5415
|
-
const fitFont = Math.min(
|
|
5416
|
-
|
|
5773
|
+
const fitFont = Math.min(
|
|
5774
|
+
MAX_FONT,
|
|
5775
|
+
Math.max(
|
|
5776
|
+
MIN_FONT,
|
|
5777
|
+
(availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
|
|
5778
|
+
)
|
|
5779
|
+
);
|
|
5417
5780
|
labelGroup
|
|
5418
5781
|
.append('text')
|
|
5419
5782
|
.attr('x', centroid.x)
|
|
@@ -5442,11 +5805,16 @@ export function renderVenn(
|
|
|
5442
5805
|
.attr('data-line-number', String(vennSets[i].lineNumber))
|
|
5443
5806
|
.style('cursor', onClickItem ? 'pointer' : 'default')
|
|
5444
5807
|
.style('outline', 'none')
|
|
5445
|
-
.on('mouseenter', () => {
|
|
5446
|
-
|
|
5808
|
+
.on('mouseenter', () => {
|
|
5809
|
+
showRegionOverlay([i]);
|
|
5810
|
+
})
|
|
5811
|
+
.on('mouseleave', () => {
|
|
5812
|
+
hideAllOverlays();
|
|
5813
|
+
})
|
|
5447
5814
|
.on('click', function () {
|
|
5448
5815
|
(this as SVGElement).blur?.();
|
|
5449
|
-
if (onClickItem && vennSets[i].lineNumber)
|
|
5816
|
+
if (onClickItem && vennSets[i].lineNumber)
|
|
5817
|
+
onClickItem(vennSets[i].lineNumber);
|
|
5450
5818
|
});
|
|
5451
5819
|
});
|
|
5452
5820
|
|
|
@@ -5455,14 +5823,23 @@ export function renderVenn(
|
|
|
5455
5823
|
|
|
5456
5824
|
const subsets: { idxs: number[]; sets: string[] }[] = [];
|
|
5457
5825
|
if (n === 2) {
|
|
5458
|
-
subsets.push({
|
|
5826
|
+
subsets.push({
|
|
5827
|
+
idxs: [0, 1],
|
|
5828
|
+
sets: [vennSets[0].name, vennSets[1].name].sort(),
|
|
5829
|
+
});
|
|
5459
5830
|
} else {
|
|
5460
5831
|
for (let a = 0; a < n; a++) {
|
|
5461
5832
|
for (let b = a + 1; b < n; b++) {
|
|
5462
|
-
subsets.push({
|
|
5833
|
+
subsets.push({
|
|
5834
|
+
idxs: [a, b],
|
|
5835
|
+
sets: [vennSets[a].name, vennSets[b].name].sort(),
|
|
5836
|
+
});
|
|
5463
5837
|
}
|
|
5464
5838
|
}
|
|
5465
|
-
subsets.push({
|
|
5839
|
+
subsets.push({
|
|
5840
|
+
idxs: [0, 1, 2],
|
|
5841
|
+
sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort(),
|
|
5842
|
+
});
|
|
5466
5843
|
}
|
|
5467
5844
|
|
|
5468
5845
|
for (const subset of subsets) {
|
|
@@ -5470,7 +5847,8 @@ export function renderVenn(
|
|
|
5470
5847
|
const inside = circles.map((_, j) => idxs.includes(j));
|
|
5471
5848
|
const centroid = regionCentroid(circles, inside);
|
|
5472
5849
|
const declaredOv = vennOverlaps.find(
|
|
5473
|
-
(ov) =>
|
|
5850
|
+
(ov) =>
|
|
5851
|
+
ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
|
|
5474
5852
|
);
|
|
5475
5853
|
hoverGroup
|
|
5476
5854
|
.append('circle')
|
|
@@ -5483,8 +5861,12 @@ export function renderVenn(
|
|
|
5483
5861
|
.attr('data-line-number', declaredOv ? String(declaredOv.lineNumber) : '')
|
|
5484
5862
|
.style('cursor', onClickItem && declaredOv ? 'pointer' : 'default')
|
|
5485
5863
|
.style('outline', 'none')
|
|
5486
|
-
.on('mouseenter', () => {
|
|
5487
|
-
|
|
5864
|
+
.on('mouseenter', () => {
|
|
5865
|
+
showRegionOverlay(idxs);
|
|
5866
|
+
})
|
|
5867
|
+
.on('mouseleave', () => {
|
|
5868
|
+
hideAllOverlays();
|
|
5869
|
+
})
|
|
5488
5870
|
.on('click', function () {
|
|
5489
5871
|
(this as SVGElement).blur?.();
|
|
5490
5872
|
if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
|
|
@@ -5543,7 +5925,12 @@ export function renderQuadrant(
|
|
|
5543
5925
|
// Margins
|
|
5544
5926
|
const hasXAxis = !!quadrantXAxis;
|
|
5545
5927
|
const hasYAxis = !!quadrantYAxis;
|
|
5546
|
-
const margin = {
|
|
5928
|
+
const margin = {
|
|
5929
|
+
top: title ? 60 : 30,
|
|
5930
|
+
right: 30,
|
|
5931
|
+
bottom: hasXAxis ? 70 : 40,
|
|
5932
|
+
left: hasYAxis ? 80 : 40,
|
|
5933
|
+
};
|
|
5547
5934
|
const chartWidth = width - margin.left - margin.right;
|
|
5548
5935
|
const chartHeight = height - margin.top - margin.bottom;
|
|
5549
5936
|
|
|
@@ -5555,7 +5942,14 @@ export function renderQuadrant(
|
|
|
5555
5942
|
const tooltip = createTooltip(container, palette, isDark);
|
|
5556
5943
|
|
|
5557
5944
|
// Title
|
|
5558
|
-
renderChartTitle(
|
|
5945
|
+
renderChartTitle(
|
|
5946
|
+
svg,
|
|
5947
|
+
title,
|
|
5948
|
+
quadrantTitleLineNumber,
|
|
5949
|
+
width,
|
|
5950
|
+
textColor,
|
|
5951
|
+
onClickItem
|
|
5952
|
+
);
|
|
5559
5953
|
|
|
5560
5954
|
// Chart group (translated by margins)
|
|
5561
5955
|
const chartG = svg
|
|
@@ -5566,12 +5960,21 @@ export function renderQuadrant(
|
|
|
5566
5960
|
const mixHex = (a: string, b: string, pct: number): string => {
|
|
5567
5961
|
const parse = (h: string) => {
|
|
5568
5962
|
const r = h.replace('#', '');
|
|
5569
|
-
const f = r.length === 3 ? r[0]+r[0]+r[1]+r[1]+r[2]+r[2] : r;
|
|
5570
|
-
return [
|
|
5963
|
+
const f = r.length === 3 ? r[0] + r[0] + r[1] + r[1] + r[2] + r[2] : r;
|
|
5964
|
+
return [
|
|
5965
|
+
parseInt(f.substring(0, 2), 16),
|
|
5966
|
+
parseInt(f.substring(2, 4), 16),
|
|
5967
|
+
parseInt(f.substring(4, 6), 16),
|
|
5968
|
+
];
|
|
5571
5969
|
};
|
|
5572
|
-
const [ar,ag,ab] = parse(a),
|
|
5573
|
-
|
|
5574
|
-
|
|
5970
|
+
const [ar, ag, ab] = parse(a),
|
|
5971
|
+
[br, bg, bb] = parse(b),
|
|
5972
|
+
t = pct / 100;
|
|
5973
|
+
const c = (x: number, y: number) =>
|
|
5974
|
+
Math.round(x * t + y * (1 - t))
|
|
5975
|
+
.toString(16)
|
|
5976
|
+
.padStart(2, '0');
|
|
5977
|
+
return `#${c(ar, br)}${c(ag, bg)}${c(ab, bb)}`;
|
|
5575
5978
|
};
|
|
5576
5979
|
|
|
5577
5980
|
const bg = isDark ? palette.surface : palette.bg;
|
|
@@ -5688,7 +6091,11 @@ export function renderQuadrant(
|
|
|
5688
6091
|
fontSize: number;
|
|
5689
6092
|
}
|
|
5690
6093
|
|
|
5691
|
-
const quadrantLabelLayout = (
|
|
6094
|
+
const quadrantLabelLayout = (
|
|
6095
|
+
text: string,
|
|
6096
|
+
qw: number,
|
|
6097
|
+
qh: number
|
|
6098
|
+
): QuadrantLabelLayout => {
|
|
5692
6099
|
const availW = qw - LABEL_PAD;
|
|
5693
6100
|
const availH = qh - LABEL_PAD;
|
|
5694
6101
|
const words = text.split(/\s+/);
|
|
@@ -5696,7 +6103,10 @@ export function renderQuadrant(
|
|
|
5696
6103
|
// Try single line first
|
|
5697
6104
|
if (estTextWidth(text, LABEL_MAX_FONT) <= availW) {
|
|
5698
6105
|
const fs = Math.min(LABEL_MAX_FONT, availH);
|
|
5699
|
-
return {
|
|
6106
|
+
return {
|
|
6107
|
+
lines: [text],
|
|
6108
|
+
fontSize: Math.max(LABEL_MIN_FONT, Math.round(fs)),
|
|
6109
|
+
};
|
|
5700
6110
|
}
|
|
5701
6111
|
|
|
5702
6112
|
// Try wrapping into 2+ lines: greedily pack words so each line fits availW
|
|
@@ -5743,7 +6153,10 @@ export function renderQuadrant(
|
|
|
5743
6153
|
const qh = chartHeight / 2;
|
|
5744
6154
|
const quadrantDefsWithLabel = quadrantDefs.filter((d) => d.label !== null);
|
|
5745
6155
|
const labelLayouts = new Map(
|
|
5746
|
-
quadrantDefsWithLabel.map((d) => [
|
|
6156
|
+
quadrantDefsWithLabel.map((d) => [
|
|
6157
|
+
d.label!.text,
|
|
6158
|
+
quadrantLabelLayout(d.label!.text, qw, qh),
|
|
6159
|
+
])
|
|
5747
6160
|
);
|
|
5748
6161
|
|
|
5749
6162
|
const quadrantLabelTexts = chartG
|
|
@@ -5808,7 +6221,10 @@ export function renderQuadrant(
|
|
|
5808
6221
|
.attr('text-anchor', 'middle')
|
|
5809
6222
|
.attr('fill', textColor)
|
|
5810
6223
|
.attr('font-size', '18px')
|
|
5811
|
-
.attr(
|
|
6224
|
+
.attr(
|
|
6225
|
+
'data-line-number',
|
|
6226
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6227
|
+
)
|
|
5812
6228
|
.style(
|
|
5813
6229
|
'cursor',
|
|
5814
6230
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5824,7 +6240,10 @@ export function renderQuadrant(
|
|
|
5824
6240
|
.attr('text-anchor', 'middle')
|
|
5825
6241
|
.attr('fill', textColor)
|
|
5826
6242
|
.attr('font-size', '18px')
|
|
5827
|
-
.attr(
|
|
6243
|
+
.attr(
|
|
6244
|
+
'data-line-number',
|
|
6245
|
+
quadrantXAxisLineNumber ? String(quadrantXAxisLineNumber) : null
|
|
6246
|
+
)
|
|
5828
6247
|
.style(
|
|
5829
6248
|
'cursor',
|
|
5830
6249
|
onClickItem && quadrantXAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5860,7 +6279,10 @@ export function renderQuadrant(
|
|
|
5860
6279
|
.attr('fill', textColor)
|
|
5861
6280
|
.attr('font-size', '18px')
|
|
5862
6281
|
.attr('transform', `rotate(-90, 22, ${yMidBottom})`)
|
|
5863
|
-
.attr(
|
|
6282
|
+
.attr(
|
|
6283
|
+
'data-line-number',
|
|
6284
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6285
|
+
)
|
|
5864
6286
|
.style(
|
|
5865
6287
|
'cursor',
|
|
5866
6288
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5877,7 +6299,10 @@ export function renderQuadrant(
|
|
|
5877
6299
|
.attr('fill', textColor)
|
|
5878
6300
|
.attr('font-size', '18px')
|
|
5879
6301
|
.attr('transform', `rotate(-90, 22, ${yMidTop})`)
|
|
5880
|
-
.attr(
|
|
6302
|
+
.attr(
|
|
6303
|
+
'data-line-number',
|
|
6304
|
+
quadrantYAxisLineNumber ? String(quadrantYAxisLineNumber) : null
|
|
6305
|
+
)
|
|
5881
6306
|
.style(
|
|
5882
6307
|
'cursor',
|
|
5883
6308
|
onClickItem && quadrantYAxisLineNumber ? 'pointer' : 'default'
|
|
@@ -5936,7 +6361,9 @@ export function renderQuadrant(
|
|
|
5936
6361
|
const pointColor =
|
|
5937
6362
|
quadDef?.label?.color ?? defaultColors[quadDef?.colorIdx ?? 0];
|
|
5938
6363
|
|
|
5939
|
-
const pointG = pointsG
|
|
6364
|
+
const pointG = pointsG
|
|
6365
|
+
.append('g')
|
|
6366
|
+
.attr('class', 'point-group')
|
|
5940
6367
|
.attr('data-line-number', String(point.lineNumber));
|
|
5941
6368
|
|
|
5942
6369
|
// Circle with white fill and colored border for visibility on opaque quadrants
|
|
@@ -6025,7 +6452,10 @@ const EXPORT_HEIGHT = 800;
|
|
|
6025
6452
|
/**
|
|
6026
6453
|
* Resolves the palette for export, falling back to Nord light/dark.
|
|
6027
6454
|
*/
|
|
6028
|
-
async function resolveExportPalette(
|
|
6455
|
+
async function resolveExportPalette(
|
|
6456
|
+
theme: string,
|
|
6457
|
+
palette?: PaletteColors
|
|
6458
|
+
): Promise<PaletteColors> {
|
|
6029
6459
|
if (palette) return palette;
|
|
6030
6460
|
const { getPalette } = await import('./palettes');
|
|
6031
6461
|
return theme === 'dark' ? getPalette('nord').dark : getPalette('nord').light;
|
|
@@ -6087,7 +6517,13 @@ export async function renderForExport(
|
|
|
6087
6517
|
hiddenAttributes?: Set<string>;
|
|
6088
6518
|
swimlaneTagGroup?: string | null;
|
|
6089
6519
|
},
|
|
6090
|
-
options?: {
|
|
6520
|
+
options?: {
|
|
6521
|
+
branding?: boolean;
|
|
6522
|
+
c4Level?: 'context' | 'containers' | 'components' | 'deployment';
|
|
6523
|
+
c4System?: string;
|
|
6524
|
+
c4Container?: string;
|
|
6525
|
+
tagGroup?: string;
|
|
6526
|
+
}
|
|
6091
6527
|
): Promise<string> {
|
|
6092
6528
|
// Flowchart and org chart use their own parser pipelines — intercept before parseVisualization()
|
|
6093
6529
|
const { parseDgmoChartType } = await import('./dgmo-router');
|
|
@@ -6107,7 +6543,8 @@ export async function renderForExport(
|
|
|
6107
6543
|
|
|
6108
6544
|
// Apply interactive collapse state when provided
|
|
6109
6545
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6110
|
-
const activeTagGroup =
|
|
6546
|
+
const activeTagGroup =
|
|
6547
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6111
6548
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6112
6549
|
|
|
6113
6550
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6129,7 +6566,17 @@ export async function renderForExport(
|
|
|
6129
6566
|
const exportHeight = orgLayout.height + PADDING * 2 + titleOffset;
|
|
6130
6567
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6131
6568
|
|
|
6132
|
-
renderOrg(
|
|
6569
|
+
renderOrg(
|
|
6570
|
+
container,
|
|
6571
|
+
effectiveParsed,
|
|
6572
|
+
orgLayout,
|
|
6573
|
+
effectivePalette,
|
|
6574
|
+
isDark,
|
|
6575
|
+
undefined,
|
|
6576
|
+
{ width: exportWidth, height: exportHeight },
|
|
6577
|
+
activeTagGroup,
|
|
6578
|
+
hiddenAttributes
|
|
6579
|
+
);
|
|
6133
6580
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6134
6581
|
}
|
|
6135
6582
|
|
|
@@ -6147,7 +6594,8 @@ export async function renderForExport(
|
|
|
6147
6594
|
|
|
6148
6595
|
// Apply interactive collapse state when provided
|
|
6149
6596
|
const collapsedNodes = orgExportState?.collapsedNodes;
|
|
6150
|
-
const activeTagGroup =
|
|
6597
|
+
const activeTagGroup =
|
|
6598
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup ?? null;
|
|
6151
6599
|
const hiddenAttributes = orgExportState?.hiddenAttributes;
|
|
6152
6600
|
|
|
6153
6601
|
const { parsed: effectiveParsed, hiddenCounts } =
|
|
@@ -6160,7 +6608,7 @@ export async function renderForExport(
|
|
|
6160
6608
|
hiddenCounts.size > 0 ? hiddenCounts : undefined,
|
|
6161
6609
|
activeTagGroup,
|
|
6162
6610
|
hiddenAttributes,
|
|
6163
|
-
true
|
|
6611
|
+
true
|
|
6164
6612
|
);
|
|
6165
6613
|
|
|
6166
6614
|
const PADDING = 20;
|
|
@@ -6169,7 +6617,17 @@ export async function renderForExport(
|
|
|
6169
6617
|
const exportHeight = sitemapLayout.height + PADDING * 2 + titleOffset;
|
|
6170
6618
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6171
6619
|
|
|
6172
|
-
renderSitemap(
|
|
6620
|
+
renderSitemap(
|
|
6621
|
+
container,
|
|
6622
|
+
effectiveParsed,
|
|
6623
|
+
sitemapLayout,
|
|
6624
|
+
effectivePalette,
|
|
6625
|
+
isDark,
|
|
6626
|
+
undefined,
|
|
6627
|
+
{ width: exportWidth, height: exportHeight },
|
|
6628
|
+
activeTagGroup,
|
|
6629
|
+
hiddenAttributes
|
|
6630
|
+
);
|
|
6173
6631
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6174
6632
|
}
|
|
6175
6633
|
|
|
@@ -6187,7 +6645,15 @@ export async function renderForExport(
|
|
|
6187
6645
|
container.style.left = '-9999px';
|
|
6188
6646
|
document.body.appendChild(container);
|
|
6189
6647
|
|
|
6190
|
-
renderKanban(
|
|
6648
|
+
renderKanban(
|
|
6649
|
+
container,
|
|
6650
|
+
kanbanParsed,
|
|
6651
|
+
effectivePalette,
|
|
6652
|
+
theme === 'dark',
|
|
6653
|
+
undefined,
|
|
6654
|
+
undefined,
|
|
6655
|
+
options?.tagGroup
|
|
6656
|
+
);
|
|
6191
6657
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6192
6658
|
}
|
|
6193
6659
|
|
|
@@ -6207,7 +6673,15 @@ export async function renderForExport(
|
|
|
6207
6673
|
const exportHeight = classLayout.height + PADDING * 2 + titleOffset;
|
|
6208
6674
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6209
6675
|
|
|
6210
|
-
renderClassDiagram(
|
|
6676
|
+
renderClassDiagram(
|
|
6677
|
+
container,
|
|
6678
|
+
classParsed,
|
|
6679
|
+
classLayout,
|
|
6680
|
+
effectivePalette,
|
|
6681
|
+
theme === 'dark',
|
|
6682
|
+
undefined,
|
|
6683
|
+
{ width: exportWidth, height: exportHeight }
|
|
6684
|
+
);
|
|
6211
6685
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6212
6686
|
}
|
|
6213
6687
|
|
|
@@ -6227,14 +6701,26 @@ export async function renderForExport(
|
|
|
6227
6701
|
const exportHeight = erLayout.height + PADDING * 2 + titleOffset;
|
|
6228
6702
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6229
6703
|
|
|
6230
|
-
renderERDiagram(
|
|
6704
|
+
renderERDiagram(
|
|
6705
|
+
container,
|
|
6706
|
+
erParsed,
|
|
6707
|
+
erLayout,
|
|
6708
|
+
effectivePalette,
|
|
6709
|
+
theme === 'dark',
|
|
6710
|
+
undefined,
|
|
6711
|
+
{ width: exportWidth, height: exportHeight },
|
|
6712
|
+
options?.tagGroup
|
|
6713
|
+
);
|
|
6231
6714
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6232
6715
|
}
|
|
6233
6716
|
|
|
6234
6717
|
if (detectedType === 'initiative-status') {
|
|
6235
|
-
const { parseInitiativeStatus } =
|
|
6236
|
-
|
|
6237
|
-
const {
|
|
6718
|
+
const { parseInitiativeStatus } =
|
|
6719
|
+
await import('./initiative-status/parser');
|
|
6720
|
+
const { layoutInitiativeStatus } =
|
|
6721
|
+
await import('./initiative-status/layout');
|
|
6722
|
+
const { renderInitiativeStatus } =
|
|
6723
|
+
await import('./initiative-status/renderer');
|
|
6238
6724
|
|
|
6239
6725
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6240
6726
|
const isParsed = parseInitiativeStatus(content);
|
|
@@ -6247,14 +6733,27 @@ export async function renderForExport(
|
|
|
6247
6733
|
const exportHeight = isLayout.height + PADDING * 2 + titleOffset;
|
|
6248
6734
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6249
6735
|
|
|
6250
|
-
renderInitiativeStatus(
|
|
6736
|
+
renderInitiativeStatus(
|
|
6737
|
+
container,
|
|
6738
|
+
isParsed,
|
|
6739
|
+
isLayout,
|
|
6740
|
+
effectivePalette,
|
|
6741
|
+
theme === 'dark',
|
|
6742
|
+
{ exportDims: { width: exportWidth, height: exportHeight } }
|
|
6743
|
+
);
|
|
6251
6744
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6252
6745
|
}
|
|
6253
6746
|
|
|
6254
6747
|
if (detectedType === 'c4') {
|
|
6255
6748
|
const { parseC4 } = await import('./c4/parser');
|
|
6256
|
-
const {
|
|
6257
|
-
|
|
6749
|
+
const {
|
|
6750
|
+
layoutC4Context,
|
|
6751
|
+
layoutC4Containers,
|
|
6752
|
+
layoutC4Components,
|
|
6753
|
+
layoutC4Deployment,
|
|
6754
|
+
} = await import('./c4/layout');
|
|
6755
|
+
const { renderC4Context, renderC4Containers } =
|
|
6756
|
+
await import('./c4/renderer');
|
|
6258
6757
|
|
|
6259
6758
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6260
6759
|
const c4Parsed = parseC4(content, effectivePalette);
|
|
@@ -6265,13 +6764,14 @@ export async function renderForExport(
|
|
|
6265
6764
|
const c4System = options?.c4System;
|
|
6266
6765
|
const c4Container = options?.c4Container;
|
|
6267
6766
|
|
|
6268
|
-
const c4Layout =
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6767
|
+
const c4Layout =
|
|
6768
|
+
c4Level === 'deployment'
|
|
6769
|
+
? layoutC4Deployment(c4Parsed)
|
|
6770
|
+
: c4Level === 'components' && c4System && c4Container
|
|
6771
|
+
? layoutC4Components(c4Parsed, c4System, c4Container)
|
|
6772
|
+
: c4Level === 'containers' && c4System
|
|
6773
|
+
? layoutC4Containers(c4Parsed, c4System)
|
|
6774
|
+
: layoutC4Context(c4Parsed);
|
|
6275
6775
|
|
|
6276
6776
|
if (c4Layout.nodes.length === 0) return '';
|
|
6277
6777
|
|
|
@@ -6281,11 +6781,23 @@ export async function renderForExport(
|
|
|
6281
6781
|
const exportHeight = c4Layout.height + PADDING * 2 + titleOffset;
|
|
6282
6782
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6283
6783
|
|
|
6284
|
-
const renderFn =
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6784
|
+
const renderFn =
|
|
6785
|
+
c4Level === 'deployment' ||
|
|
6786
|
+
(c4Level === 'components' && c4System && c4Container) ||
|
|
6787
|
+
(c4Level === 'containers' && c4System)
|
|
6788
|
+
? renderC4Containers
|
|
6789
|
+
: renderC4Context;
|
|
6790
|
+
|
|
6791
|
+
renderFn(
|
|
6792
|
+
container,
|
|
6793
|
+
c4Parsed,
|
|
6794
|
+
c4Layout,
|
|
6795
|
+
effectivePalette,
|
|
6796
|
+
theme === 'dark',
|
|
6797
|
+
undefined,
|
|
6798
|
+
{ width: exportWidth, height: exportHeight },
|
|
6799
|
+
options?.tagGroup
|
|
6800
|
+
);
|
|
6289
6801
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6290
6802
|
}
|
|
6291
6803
|
|
|
@@ -6301,7 +6813,15 @@ export async function renderForExport(
|
|
|
6301
6813
|
const layout = layoutGraph(fcParsed);
|
|
6302
6814
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6303
6815
|
|
|
6304
|
-
renderFlowchart(
|
|
6816
|
+
renderFlowchart(
|
|
6817
|
+
container,
|
|
6818
|
+
fcParsed,
|
|
6819
|
+
layout,
|
|
6820
|
+
effectivePalette,
|
|
6821
|
+
theme === 'dark',
|
|
6822
|
+
undefined,
|
|
6823
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
6824
|
+
);
|
|
6305
6825
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6306
6826
|
}
|
|
6307
6827
|
|
|
@@ -6309,7 +6829,8 @@ export async function renderForExport(
|
|
|
6309
6829
|
const { parseInfra } = await import('./infra/parser');
|
|
6310
6830
|
const { computeInfra } = await import('./infra/compute');
|
|
6311
6831
|
const { layoutInfra } = await import('./infra/layout');
|
|
6312
|
-
const { renderInfra, computeInfraLegendGroups } =
|
|
6832
|
+
const { renderInfra, computeInfraLegendGroups } =
|
|
6833
|
+
await import('./infra/renderer');
|
|
6313
6834
|
|
|
6314
6835
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6315
6836
|
const infraParsed = parseInfra(content);
|
|
@@ -6320,13 +6841,30 @@ export async function renderForExport(
|
|
|
6320
6841
|
const activeTagGroup = options?.tagGroup ?? null;
|
|
6321
6842
|
|
|
6322
6843
|
const titleOffset = infraParsed.title ? 40 : 0;
|
|
6323
|
-
const legendGroups = computeInfraLegendGroups(
|
|
6844
|
+
const legendGroups = computeInfraLegendGroups(
|
|
6845
|
+
infraLayout.nodes,
|
|
6846
|
+
infraParsed.tagGroups,
|
|
6847
|
+
effectivePalette
|
|
6848
|
+
);
|
|
6324
6849
|
const legendOffset = legendGroups.length > 0 ? 28 : 0;
|
|
6325
6850
|
const exportWidth = infraLayout.width;
|
|
6326
6851
|
const exportHeight = infraLayout.height + titleOffset + legendOffset;
|
|
6327
6852
|
const container = createExportContainer(exportWidth, exportHeight);
|
|
6328
6853
|
|
|
6329
|
-
renderInfra(
|
|
6854
|
+
renderInfra(
|
|
6855
|
+
container,
|
|
6856
|
+
infraLayout,
|
|
6857
|
+
effectivePalette,
|
|
6858
|
+
theme === 'dark',
|
|
6859
|
+
infraParsed.title,
|
|
6860
|
+
infraParsed.titleLineNumber,
|
|
6861
|
+
infraParsed.tagGroups,
|
|
6862
|
+
activeTagGroup,
|
|
6863
|
+
false,
|
|
6864
|
+
null,
|
|
6865
|
+
null,
|
|
6866
|
+
true
|
|
6867
|
+
);
|
|
6330
6868
|
// Restore explicit pixel dimensions for resvg (renderer uses 100%/viewBox for app scaling)
|
|
6331
6869
|
const infraSvg = container.querySelector('svg');
|
|
6332
6870
|
if (infraSvg) {
|
|
@@ -6350,7 +6888,14 @@ export async function renderForExport(
|
|
|
6350
6888
|
const EXPORT_H = 800;
|
|
6351
6889
|
const container = createExportContainer(EXPORT_W, EXPORT_H);
|
|
6352
6890
|
|
|
6353
|
-
renderGantt(
|
|
6891
|
+
renderGantt(
|
|
6892
|
+
container,
|
|
6893
|
+
resolved,
|
|
6894
|
+
effectivePalette,
|
|
6895
|
+
theme === 'dark',
|
|
6896
|
+
undefined,
|
|
6897
|
+
{ width: EXPORT_W, height: EXPORT_H }
|
|
6898
|
+
);
|
|
6354
6899
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6355
6900
|
}
|
|
6356
6901
|
|
|
@@ -6366,7 +6911,15 @@ export async function renderForExport(
|
|
|
6366
6911
|
const layout = layoutGraph(stateParsed);
|
|
6367
6912
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6368
6913
|
|
|
6369
|
-
renderState(
|
|
6914
|
+
renderState(
|
|
6915
|
+
container,
|
|
6916
|
+
stateParsed,
|
|
6917
|
+
layout,
|
|
6918
|
+
effectivePalette,
|
|
6919
|
+
theme === 'dark',
|
|
6920
|
+
undefined,
|
|
6921
|
+
{ width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
|
|
6922
|
+
);
|
|
6370
6923
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|
|
6371
6924
|
}
|
|
6372
6925
|
|
|
@@ -6392,30 +6945,75 @@ export async function renderForExport(
|
|
|
6392
6945
|
const effectivePalette = await resolveExportPalette(theme, palette);
|
|
6393
6946
|
const isDark = theme === 'dark';
|
|
6394
6947
|
const container = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
|
|
6395
|
-
const dims: D3ExportDimensions = {
|
|
6948
|
+
const dims: D3ExportDimensions = {
|
|
6949
|
+
width: EXPORT_WIDTH,
|
|
6950
|
+
height: EXPORT_HEIGHT,
|
|
6951
|
+
};
|
|
6396
6952
|
|
|
6397
6953
|
if (parsed.type === 'sequence') {
|
|
6398
6954
|
const { parseSequenceDgmo } = await import('./sequence/parser');
|
|
6399
6955
|
const { renderSequenceDiagram } = await import('./sequence/renderer');
|
|
6400
6956
|
const seqParsed = parseSequenceDgmo(content);
|
|
6401
6957
|
if (seqParsed.error || seqParsed.participants.length === 0) return '';
|
|
6402
|
-
renderSequenceDiagram(
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6958
|
+
renderSequenceDiagram(
|
|
6959
|
+
container,
|
|
6960
|
+
seqParsed,
|
|
6961
|
+
effectivePalette,
|
|
6962
|
+
isDark,
|
|
6963
|
+
undefined,
|
|
6964
|
+
{
|
|
6965
|
+
exportWidth: EXPORT_WIDTH,
|
|
6966
|
+
activeTagGroup: options?.tagGroup,
|
|
6967
|
+
}
|
|
6968
|
+
);
|
|
6406
6969
|
} else if (parsed.type === 'wordcloud') {
|
|
6407
|
-
await renderWordCloudAsync(
|
|
6970
|
+
await renderWordCloudAsync(
|
|
6971
|
+
container,
|
|
6972
|
+
parsed,
|
|
6973
|
+
effectivePalette,
|
|
6974
|
+
isDark,
|
|
6975
|
+
dims
|
|
6976
|
+
);
|
|
6408
6977
|
} else if (parsed.type === 'arc') {
|
|
6409
|
-
renderArcDiagram(
|
|
6978
|
+
renderArcDiagram(
|
|
6979
|
+
container,
|
|
6980
|
+
parsed,
|
|
6981
|
+
effectivePalette,
|
|
6982
|
+
isDark,
|
|
6983
|
+
undefined,
|
|
6984
|
+
dims
|
|
6985
|
+
);
|
|
6410
6986
|
} else if (parsed.type === 'timeline') {
|
|
6411
|
-
renderTimeline(
|
|
6412
|
-
|
|
6987
|
+
renderTimeline(
|
|
6988
|
+
container,
|
|
6989
|
+
parsed,
|
|
6990
|
+
effectivePalette,
|
|
6991
|
+
isDark,
|
|
6992
|
+
undefined,
|
|
6993
|
+
dims,
|
|
6994
|
+
orgExportState?.activeTagGroup ?? options?.tagGroup,
|
|
6995
|
+
orgExportState?.swimlaneTagGroup
|
|
6996
|
+
);
|
|
6413
6997
|
} else if (parsed.type === 'venn') {
|
|
6414
6998
|
renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
|
|
6415
6999
|
} else if (parsed.type === 'quadrant') {
|
|
6416
|
-
renderQuadrant(
|
|
7000
|
+
renderQuadrant(
|
|
7001
|
+
container,
|
|
7002
|
+
parsed,
|
|
7003
|
+
effectivePalette,
|
|
7004
|
+
isDark,
|
|
7005
|
+
undefined,
|
|
7006
|
+
dims
|
|
7007
|
+
);
|
|
6417
7008
|
} else {
|
|
6418
|
-
renderSlopeChart(
|
|
7009
|
+
renderSlopeChart(
|
|
7010
|
+
container,
|
|
7011
|
+
parsed,
|
|
7012
|
+
effectivePalette,
|
|
7013
|
+
isDark,
|
|
7014
|
+
undefined,
|
|
7015
|
+
dims
|
|
7016
|
+
);
|
|
6419
7017
|
}
|
|
6420
7018
|
|
|
6421
7019
|
return finalizeSvgExport(container, theme, effectivePalette, options);
|