@diagrammo/dgmo 0.5.0 → 0.5.1
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/dist/cli.cjs +146 -146
- package/dist/index.cjs +195 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +195 -43
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/d3.ts +251 -58
- package/src/sharing.ts +8 -0
package/package.json
CHANGED
package/src/d3.ts
CHANGED
|
@@ -65,7 +65,7 @@ export interface ArcNodeGroup {
|
|
|
65
65
|
lineNumber: number;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export type TimelineSort = 'time' | 'group';
|
|
68
|
+
export type TimelineSort = 'time' | 'group' | 'tag';
|
|
69
69
|
|
|
70
70
|
export interface TimelineEvent {
|
|
71
71
|
date: string;
|
|
@@ -156,6 +156,7 @@ export interface ParsedD3 {
|
|
|
156
156
|
timelineMarkers: TimelineMarker[];
|
|
157
157
|
timelineTagGroups: TagGroup[];
|
|
158
158
|
timelineSort: TimelineSort;
|
|
159
|
+
timelineDefaultSwimlaneTG?: string;
|
|
159
160
|
timelineScale: boolean;
|
|
160
161
|
timelineSwimlanes: boolean;
|
|
161
162
|
vennSets: VennSet[];
|
|
@@ -815,10 +816,19 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
815
816
|
if (key === 'sort') {
|
|
816
817
|
const v = line
|
|
817
818
|
.substring(colonIndex + 1)
|
|
818
|
-
.trim()
|
|
819
|
-
|
|
820
|
-
if (
|
|
821
|
-
result.timelineSort =
|
|
819
|
+
.trim();
|
|
820
|
+
const vLower = v.toLowerCase();
|
|
821
|
+
if (vLower === 'time' || vLower === 'group') {
|
|
822
|
+
result.timelineSort = vLower;
|
|
823
|
+
} else if (vLower === 'tag' || vLower.startsWith('tag:')) {
|
|
824
|
+
result.timelineSort = 'tag';
|
|
825
|
+
if (vLower.startsWith('tag:')) {
|
|
826
|
+
// Extract group name (preserving original case for display)
|
|
827
|
+
const groupRef = v.substring(4).trim();
|
|
828
|
+
if (groupRef) {
|
|
829
|
+
result.timelineDefaultSwimlaneTG = groupRef;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
822
832
|
}
|
|
823
833
|
continue;
|
|
824
834
|
}
|
|
@@ -1026,6 +1036,30 @@ export function parseD3(content: string, palette?: PaletteColors): ParsedD3 {
|
|
|
1026
1036
|
}
|
|
1027
1037
|
}
|
|
1028
1038
|
}
|
|
1039
|
+
|
|
1040
|
+
// Resolve sort: tag default swimlane group
|
|
1041
|
+
if (result.timelineSort === 'tag') {
|
|
1042
|
+
if (result.timelineTagGroups.length === 0) {
|
|
1043
|
+
warn(1, '"sort: tag" requires at least one tag group definition');
|
|
1044
|
+
result.timelineSort = 'time';
|
|
1045
|
+
} else if (result.timelineDefaultSwimlaneTG) {
|
|
1046
|
+
// Resolve alias → full group name
|
|
1047
|
+
const ref = result.timelineDefaultSwimlaneTG.toLowerCase();
|
|
1048
|
+
const match = result.timelineTagGroups.find(
|
|
1049
|
+
(g) => g.name.toLowerCase() === ref || g.alias?.toLowerCase() === ref
|
|
1050
|
+
);
|
|
1051
|
+
if (match) {
|
|
1052
|
+
result.timelineDefaultSwimlaneTG = match.name;
|
|
1053
|
+
} else {
|
|
1054
|
+
warn(1, `"sort: tag:${result.timelineDefaultSwimlaneTG}" — no tag group matches "${result.timelineDefaultSwimlaneTG}"`);
|
|
1055
|
+
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
// Default to first tag group
|
|
1059
|
+
result.timelineDefaultSwimlaneTG = result.timelineTagGroups[0].name;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1029
1063
|
return result;
|
|
1030
1064
|
}
|
|
1031
1065
|
|
|
@@ -2781,7 +2815,9 @@ export function renderTimeline(
|
|
|
2781
2815
|
isDark: boolean,
|
|
2782
2816
|
onClickItem?: (lineNumber: number) => void,
|
|
2783
2817
|
exportDims?: D3ExportDimensions,
|
|
2784
|
-
activeTagGroup?: string | null
|
|
2818
|
+
activeTagGroup?: string | null,
|
|
2819
|
+
swimlaneTagGroup?: string | null,
|
|
2820
|
+
onTagStateChange?: (activeTagGroup: string | null, swimlaneTagGroup: string | null) => void
|
|
2785
2821
|
): void {
|
|
2786
2822
|
d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
|
|
2787
2823
|
|
|
@@ -2798,6 +2834,11 @@ export function renderTimeline(
|
|
|
2798
2834
|
} = parsed;
|
|
2799
2835
|
if (timelineEvents.length === 0) return;
|
|
2800
2836
|
|
|
2837
|
+
// When sort: tag is set and no explicit swimlane param, use the default
|
|
2838
|
+
if (swimlaneTagGroup == null && timelineSort === 'tag' && parsed.timelineDefaultSwimlaneTG) {
|
|
2839
|
+
swimlaneTagGroup = parsed.timelineDefaultSwimlaneTG;
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2801
2842
|
const tooltip = createTooltip(container, palette, isDark);
|
|
2802
2843
|
|
|
2803
2844
|
const width = exportDims?.width ?? container.clientWidth;
|
|
@@ -2818,10 +2859,61 @@ export function renderTimeline(
|
|
|
2818
2859
|
groupColorMap.set(grp.name, grp.color ?? colors[i % colors.length]);
|
|
2819
2860
|
});
|
|
2820
2861
|
|
|
2862
|
+
// When tag-based swimlanes are active, compute lanes from tag values
|
|
2863
|
+
// and populate groupColorMap with tag entry colors for lane headers.
|
|
2864
|
+
type Lane = { name: string; events: TimelineEvent[] };
|
|
2865
|
+
let tagLanes: Lane[] | null = null;
|
|
2866
|
+
|
|
2867
|
+
if (swimlaneTagGroup) {
|
|
2868
|
+
const tagKey = swimlaneTagGroup.toLowerCase();
|
|
2869
|
+
const tagGroup = parsed.timelineTagGroups.find(
|
|
2870
|
+
(g) => g.name.toLowerCase() === tagKey
|
|
2871
|
+
);
|
|
2872
|
+
if (tagGroup) {
|
|
2873
|
+
// Collect events per tag value
|
|
2874
|
+
const buckets = new Map<string, TimelineEvent[]>();
|
|
2875
|
+
const otherEvents: TimelineEvent[] = [];
|
|
2876
|
+
for (const ev of timelineEvents) {
|
|
2877
|
+
const val = ev.metadata[tagKey];
|
|
2878
|
+
if (val) {
|
|
2879
|
+
const list = buckets.get(val) ?? [];
|
|
2880
|
+
list.push(ev);
|
|
2881
|
+
buckets.set(val, list);
|
|
2882
|
+
} else {
|
|
2883
|
+
otherEvents.push(ev);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Order lanes by earliest event date
|
|
2888
|
+
const laneEntries = [...buckets.entries()].sort((a, b) => {
|
|
2889
|
+
const aMin = Math.min(
|
|
2890
|
+
...a[1].map((e) => parseTimelineDate(e.date))
|
|
2891
|
+
);
|
|
2892
|
+
const bMin = Math.min(
|
|
2893
|
+
...b[1].map((e) => parseTimelineDate(e.date))
|
|
2894
|
+
);
|
|
2895
|
+
return aMin - bMin;
|
|
2896
|
+
});
|
|
2897
|
+
|
|
2898
|
+
tagLanes = laneEntries.map(([name, events]) => ({ name, events }));
|
|
2899
|
+
if (otherEvents.length > 0) {
|
|
2900
|
+
tagLanes.push({ name: '(Other)', events: otherEvents });
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
// Populate groupColorMap from tag entry colors
|
|
2904
|
+
for (const entry of tagGroup.entries) {
|
|
2905
|
+
groupColorMap.set(entry.value, entry.color);
|
|
2906
|
+
}
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
// Determine effective color source: explicit colorTG > swimlaneTG > group
|
|
2911
|
+
const effectiveColorTG = activeTagGroup ?? swimlaneTagGroup ?? null;
|
|
2912
|
+
|
|
2821
2913
|
function eventColor(ev: TimelineEvent): string {
|
|
2822
2914
|
// Tag color takes priority when a tag group is active
|
|
2823
|
-
if (
|
|
2824
|
-
const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups,
|
|
2915
|
+
if (effectiveColorTG) {
|
|
2916
|
+
const tagColor = resolveTagColor(ev.metadata, parsed.timelineTagGroups, effectiveColorTG);
|
|
2825
2917
|
if (tagColor) return tagColor;
|
|
2826
2918
|
}
|
|
2827
2919
|
if (ev.group && groupColorMap.has(ev.group)) {
|
|
@@ -2962,14 +3054,34 @@ export function renderTimeline(
|
|
|
2962
3054
|
// VERTICAL orientation (time flows top→bottom)
|
|
2963
3055
|
// ================================================================
|
|
2964
3056
|
if (isVertical) {
|
|
2965
|
-
|
|
3057
|
+
const useGroupedVertical = tagLanes != null ||
|
|
3058
|
+
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
3059
|
+
if (useGroupedVertical) {
|
|
2966
3060
|
// === GROUPED: one column/lane per group, vertical ===
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
)
|
|
2971
|
-
|
|
2972
|
-
|
|
3061
|
+
let laneNames: string[];
|
|
3062
|
+
let laneEventsByName: Map<string, TimelineEvent[]>;
|
|
3063
|
+
|
|
3064
|
+
if (tagLanes) {
|
|
3065
|
+
laneNames = tagLanes.map((l) => l.name);
|
|
3066
|
+
laneEventsByName = new Map(tagLanes.map((l) => [l.name, l.events]));
|
|
3067
|
+
} else {
|
|
3068
|
+
const groupNames = timelineGroups.map((gr) => gr.name);
|
|
3069
|
+
const ungroupedEvents = timelineEvents.filter(
|
|
3070
|
+
(ev) => ev.group === null || !groupNames.includes(ev.group)
|
|
3071
|
+
);
|
|
3072
|
+
laneNames =
|
|
3073
|
+
ungroupedEvents.length > 0 ? [...groupNames, '(Other)'] : groupNames;
|
|
3074
|
+
laneEventsByName = new Map(
|
|
3075
|
+
laneNames.map((name) => [
|
|
3076
|
+
name,
|
|
3077
|
+
timelineEvents.filter((ev) =>
|
|
3078
|
+
name === '(Other)'
|
|
3079
|
+
? ev.group === null || !groupNames.includes(ev.group)
|
|
3080
|
+
: ev.group === name
|
|
3081
|
+
),
|
|
3082
|
+
])
|
|
3083
|
+
);
|
|
3084
|
+
}
|
|
2973
3085
|
|
|
2974
3086
|
const laneCount = laneNames.length;
|
|
2975
3087
|
const scaleMargin = timelineScale ? 40 : 0;
|
|
@@ -3043,6 +3155,23 @@ export function renderTimeline(
|
|
|
3043
3155
|
);
|
|
3044
3156
|
}
|
|
3045
3157
|
|
|
3158
|
+
// Render swimlane backgrounds for vertical lanes
|
|
3159
|
+
if (timelineSwimlanes || tagLanes) {
|
|
3160
|
+
laneNames.forEach((laneName, laneIdx) => {
|
|
3161
|
+
const laneX = laneIdx * laneWidth;
|
|
3162
|
+
const fillColor = laneIdx % 2 === 0 ? textColor : 'transparent';
|
|
3163
|
+
g.append('rect')
|
|
3164
|
+
.attr('class', 'tl-swimlane')
|
|
3165
|
+
.attr('data-group', laneName)
|
|
3166
|
+
.attr('x', laneX)
|
|
3167
|
+
.attr('y', 0)
|
|
3168
|
+
.attr('width', laneWidth)
|
|
3169
|
+
.attr('height', innerHeight)
|
|
3170
|
+
.attr('fill', fillColor)
|
|
3171
|
+
.attr('opacity', 0.06);
|
|
3172
|
+
});
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3046
3175
|
laneNames.forEach((laneName, laneIdx) => {
|
|
3047
3176
|
const laneX = laneIdx * laneWidth;
|
|
3048
3177
|
const laneColor = groupColorMap.get(laneName) ?? textColor;
|
|
@@ -3075,11 +3204,7 @@ export function renderTimeline(
|
|
|
3075
3204
|
.attr('stroke-width', 1)
|
|
3076
3205
|
.attr('stroke-dasharray', '4,4');
|
|
3077
3206
|
|
|
3078
|
-
const laneEvents =
|
|
3079
|
-
laneName === '(Other)'
|
|
3080
|
-
? ev.group === null || !groupNames.includes(ev.group)
|
|
3081
|
-
: ev.group === laneName
|
|
3082
|
-
);
|
|
3207
|
+
const laneEvents = laneEventsByName.get(laneName) ?? [];
|
|
3083
3208
|
|
|
3084
3209
|
for (const ev of laneEvents) {
|
|
3085
3210
|
const y = yScale(parseTimelineDate(ev.date));
|
|
@@ -3110,11 +3235,13 @@ export function renderTimeline(
|
|
|
3110
3235
|
});
|
|
3111
3236
|
setTagAttrs(evG, ev);
|
|
3112
3237
|
|
|
3238
|
+
const evColor = eventColor(ev);
|
|
3239
|
+
|
|
3113
3240
|
if (ev.endDate) {
|
|
3114
3241
|
const y2 = yScale(parseTimelineDate(ev.endDate));
|
|
3115
3242
|
const rectH = Math.max(y2 - y, 4);
|
|
3116
3243
|
|
|
3117
|
-
let fill: string =
|
|
3244
|
+
let fill: string = evColor;
|
|
3118
3245
|
if (ev.uncertain) {
|
|
3119
3246
|
const gradientId = `uncertain-vg-${ev.lineNumber}`;
|
|
3120
3247
|
const defs =
|
|
@@ -3163,7 +3290,7 @@ export function renderTimeline(
|
|
|
3163
3290
|
.attr('cx', laneCenter)
|
|
3164
3291
|
.attr('cy', y)
|
|
3165
3292
|
.attr('r', 4)
|
|
3166
|
-
.attr('fill',
|
|
3293
|
+
.attr('fill', evColor)
|
|
3167
3294
|
.attr('stroke', bgColor)
|
|
3168
3295
|
.attr('stroke-width', 1.5);
|
|
3169
3296
|
evG
|
|
@@ -3429,24 +3556,30 @@ export function renderTimeline(
|
|
|
3429
3556
|
const BAR_H = 22; // range bar thickness (tall enough for text inside)
|
|
3430
3557
|
const GROUP_GAP = 12; // vertical gap between group swim-lanes
|
|
3431
3558
|
|
|
3432
|
-
|
|
3559
|
+
const useGroupedHorizontal = tagLanes != null ||
|
|
3560
|
+
(timelineSort === 'group' && timelineGroups.length > 0);
|
|
3561
|
+
if (useGroupedHorizontal) {
|
|
3433
3562
|
// === GROUPED: swim-lanes stacked vertically, events on own rows ===
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3563
|
+
let lanes: Lane[];
|
|
3564
|
+
|
|
3565
|
+
if (tagLanes) {
|
|
3566
|
+
lanes = tagLanes;
|
|
3567
|
+
} else {
|
|
3568
|
+
const groupNames = timelineGroups.map((gr) => gr.name);
|
|
3569
|
+
const ungroupedEvents = timelineEvents.filter(
|
|
3570
|
+
(ev) => ev.group === null || !groupNames.includes(ev.group)
|
|
3571
|
+
);
|
|
3572
|
+
const laneNames =
|
|
3573
|
+
ungroupedEvents.length > 0 ? [...groupNames, '(Other)'] : groupNames;
|
|
3574
|
+
lanes = laneNames.map((name) => ({
|
|
3575
|
+
name,
|
|
3576
|
+
events: timelineEvents.filter((ev) =>
|
|
3577
|
+
name === '(Other)'
|
|
3578
|
+
? ev.group === null || !groupNames.includes(ev.group)
|
|
3579
|
+
: ev.group === name
|
|
3580
|
+
),
|
|
3581
|
+
}));
|
|
3582
|
+
}
|
|
3450
3583
|
|
|
3451
3584
|
const totalEventRows = lanes.reduce((s, l) => s + l.events.length, 0);
|
|
3452
3585
|
const scaleMargin = timelineScale ? 24 : 0;
|
|
@@ -3531,7 +3664,7 @@ export function renderTimeline(
|
|
|
3531
3664
|
|
|
3532
3665
|
// Render swimlane backgrounds first (so they appear behind events)
|
|
3533
3666
|
// Extend into left margin to include group names
|
|
3534
|
-
if (timelineSwimlanes) {
|
|
3667
|
+
if (timelineSwimlanes || tagLanes) {
|
|
3535
3668
|
let swimY = markerMargin;
|
|
3536
3669
|
lanes.forEach((lane, idx) => {
|
|
3537
3670
|
const laneSpan = lane.events.length * rowH;
|
|
@@ -3626,6 +3759,8 @@ export function renderTimeline(
|
|
|
3626
3759
|
});
|
|
3627
3760
|
setTagAttrs(evG, ev);
|
|
3628
3761
|
|
|
3762
|
+
const evColor = eventColor(ev);
|
|
3763
|
+
|
|
3629
3764
|
if (ev.endDate) {
|
|
3630
3765
|
const x2 = xScale(parseTimelineDate(ev.endDate));
|
|
3631
3766
|
const rectW = Math.max(x2 - x, 4);
|
|
@@ -3633,7 +3768,7 @@ export function renderTimeline(
|
|
|
3633
3768
|
const estLabelWidth = ev.label.length * 7 + 16;
|
|
3634
3769
|
const labelFitsInside = rectW >= estLabelWidth;
|
|
3635
3770
|
|
|
3636
|
-
let fill: string =
|
|
3771
|
+
let fill: string = evColor;
|
|
3637
3772
|
if (ev.uncertain) {
|
|
3638
3773
|
// Create gradient for uncertain end - fades last 20%
|
|
3639
3774
|
const gradientId = `uncertain-${ev.lineNumber}`;
|
|
@@ -3655,7 +3790,7 @@ export function renderTimeline(
|
|
|
3655
3790
|
.enter()
|
|
3656
3791
|
.append('stop')
|
|
3657
3792
|
.attr('offset', (d) => d.offset)
|
|
3658
|
-
.attr('stop-color',
|
|
3793
|
+
.attr('stop-color', evColor)
|
|
3659
3794
|
.attr('stop-opacity', (d) => d.opacity);
|
|
3660
3795
|
fill = `url(#${gradientId})`;
|
|
3661
3796
|
}
|
|
@@ -3708,7 +3843,7 @@ export function renderTimeline(
|
|
|
3708
3843
|
.attr('cx', x)
|
|
3709
3844
|
.attr('cy', y)
|
|
3710
3845
|
.attr('r', 5)
|
|
3711
|
-
.attr('fill',
|
|
3846
|
+
.attr('fill', evColor)
|
|
3712
3847
|
.attr('stroke', bgColor)
|
|
3713
3848
|
.attr('stroke-width', 1.5);
|
|
3714
3849
|
evG
|
|
@@ -3997,14 +4132,11 @@ export function renderTimeline(
|
|
|
3997
4132
|
const LG_ENTRY_DOT_GAP = 4;
|
|
3998
4133
|
const LG_ENTRY_TRAIL = 8;
|
|
3999
4134
|
const LG_GROUP_GAP = 12;
|
|
4135
|
+
const LG_ICON_W = 20; // swimlane icon area (icon + surrounding space)
|
|
4000
4136
|
|
|
4001
4137
|
const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
|
|
4002
4138
|
const mainG = mainSvg.select<SVGGElement>('g');
|
|
4003
4139
|
if (!mainSvg.empty() && !mainG.empty()) {
|
|
4004
|
-
// Legend goes in the reserved space between title and chart content.
|
|
4005
|
-
// Title is at y=30 in SVG coords; we place the legend centered in the
|
|
4006
|
-
// tagLegendReserve gap just above where g starts (margin.top).
|
|
4007
|
-
// Render legend directly on SVG (not inside g) for clean centering.
|
|
4008
4140
|
const legendY = title ? 50 : 10;
|
|
4009
4141
|
|
|
4010
4142
|
const groupBg = isDark
|
|
@@ -4019,7 +4151,8 @@ export function renderTimeline(
|
|
|
4019
4151
|
};
|
|
4020
4152
|
const legendGroups: LegendGroup[] = parsed.timelineTagGroups.map((g) => {
|
|
4021
4153
|
const pillW = g.name.length * LG_PILL_FONT_W + LG_PILL_PAD;
|
|
4022
|
-
|
|
4154
|
+
// Expanded: pill + icon + entries
|
|
4155
|
+
let entryX = LG_CAPSULE_PAD + pillW + LG_ICON_W + 4;
|
|
4023
4156
|
for (const entry of g.entries) {
|
|
4024
4157
|
const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
|
|
4025
4158
|
entryX = textX + entry.value.length * LG_ENTRY_FONT_W + LG_ENTRY_TRAIL;
|
|
@@ -4031,8 +4164,49 @@ export function renderTimeline(
|
|
|
4031
4164
|
};
|
|
4032
4165
|
});
|
|
4033
4166
|
|
|
4034
|
-
//
|
|
4167
|
+
// Two independent state axes: swimlane source + color source
|
|
4035
4168
|
let currentActiveGroup: string | null = activeTagGroup ?? null;
|
|
4169
|
+
let currentSwimlaneGroup: string | null = swimlaneTagGroup ?? null;
|
|
4170
|
+
|
|
4171
|
+
/** Render the swimlane icon (3 horizontal bars of varying width) */
|
|
4172
|
+
function drawSwimlaneIcon(
|
|
4173
|
+
parent: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
|
|
4174
|
+
x: number,
|
|
4175
|
+
y: number,
|
|
4176
|
+
isSwimActive: boolean
|
|
4177
|
+
) {
|
|
4178
|
+
const iconG = parent.append('g')
|
|
4179
|
+
.attr('class', 'tl-swimlane-icon')
|
|
4180
|
+
.attr('transform', `translate(${x}, ${y})`)
|
|
4181
|
+
.style('cursor', 'pointer');
|
|
4182
|
+
|
|
4183
|
+
const barColor = isSwimActive ? palette.primary : palette.textMuted;
|
|
4184
|
+
const barOpacity = isSwimActive ? 1 : 0.35;
|
|
4185
|
+
const bars = [
|
|
4186
|
+
{ y: 0, w: 8 },
|
|
4187
|
+
{ y: 4, w: 12 },
|
|
4188
|
+
{ y: 8, w: 6 },
|
|
4189
|
+
];
|
|
4190
|
+
for (const bar of bars) {
|
|
4191
|
+
iconG.append('rect')
|
|
4192
|
+
.attr('x', 0)
|
|
4193
|
+
.attr('y', bar.y)
|
|
4194
|
+
.attr('width', bar.w)
|
|
4195
|
+
.attr('height', 2)
|
|
4196
|
+
.attr('rx', 1)
|
|
4197
|
+
.attr('fill', barColor)
|
|
4198
|
+
.attr('opacity', barOpacity);
|
|
4199
|
+
}
|
|
4200
|
+
return iconG;
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
/** Full re-render with updated swimlane state */
|
|
4204
|
+
function relayout() {
|
|
4205
|
+
renderTimeline(
|
|
4206
|
+
container, parsed, palette, isDark, onClickItem, exportDims,
|
|
4207
|
+
currentActiveGroup, currentSwimlaneGroup, onTagStateChange
|
|
4208
|
+
);
|
|
4209
|
+
}
|
|
4036
4210
|
|
|
4037
4211
|
function drawLegend() {
|
|
4038
4212
|
// Remove previous legend
|
|
@@ -4048,8 +4222,11 @@ export function renderTimeline(
|
|
|
4048
4222
|
let cx = (width - totalW) / 2;
|
|
4049
4223
|
|
|
4050
4224
|
for (const lg of legendGroups) {
|
|
4225
|
+
const groupKey = lg.group.name.toLowerCase();
|
|
4051
4226
|
const isActive = currentActiveGroup != null &&
|
|
4052
|
-
|
|
4227
|
+
currentActiveGroup.toLowerCase() === groupKey;
|
|
4228
|
+
const isSwimActive = currentSwimlaneGroup != null &&
|
|
4229
|
+
currentSwimlaneGroup.toLowerCase() === groupKey;
|
|
4053
4230
|
|
|
4054
4231
|
const pillLabel = lg.group.name;
|
|
4055
4232
|
const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
|
|
@@ -4058,15 +4235,15 @@ export function renderTimeline(
|
|
|
4058
4235
|
.append('g')
|
|
4059
4236
|
.attr('transform', `translate(${cx}, ${legendY})`)
|
|
4060
4237
|
.attr('class', 'tl-tag-legend-group tl-tag-legend-entry')
|
|
4061
|
-
.attr('data-legend-group',
|
|
4062
|
-
.attr('data-tag-group',
|
|
4238
|
+
.attr('data-legend-group', groupKey)
|
|
4239
|
+
.attr('data-tag-group', groupKey)
|
|
4063
4240
|
.attr('data-legend-entry', '__group__')
|
|
4064
4241
|
.style('cursor', 'pointer')
|
|
4065
4242
|
.on('click', () => {
|
|
4066
|
-
const groupKey = lg.group.name.toLowerCase();
|
|
4067
4243
|
currentActiveGroup = currentActiveGroup === groupKey ? null : groupKey;
|
|
4068
4244
|
drawLegend();
|
|
4069
4245
|
recolorEvents();
|
|
4246
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4070
4247
|
});
|
|
4071
4248
|
|
|
4072
4249
|
// Outer capsule background (active only)
|
|
@@ -4115,9 +4292,22 @@ export function renderTimeline(
|
|
|
4115
4292
|
.attr('text-anchor', 'middle')
|
|
4116
4293
|
.text(pillLabel);
|
|
4117
4294
|
|
|
4118
|
-
// Entries inside capsule (active only)
|
|
4295
|
+
// Entries + swimlane icon inside capsule (active only)
|
|
4119
4296
|
if (isActive) {
|
|
4120
|
-
|
|
4297
|
+
// Swimlane icon right after the pill label, with breathing room
|
|
4298
|
+
const iconX = pillXOff + pillWidth + 5;
|
|
4299
|
+
const iconY = (LG_HEIGHT - 10) / 2; // vertically centered
|
|
4300
|
+
const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimActive);
|
|
4301
|
+
iconEl
|
|
4302
|
+
.attr('data-swimlane-toggle', groupKey)
|
|
4303
|
+
.on('click', (event: MouseEvent) => {
|
|
4304
|
+
event.stopPropagation();
|
|
4305
|
+
currentSwimlaneGroup = currentSwimlaneGroup === groupKey ? null : groupKey;
|
|
4306
|
+
onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
|
|
4307
|
+
relayout();
|
|
4308
|
+
});
|
|
4309
|
+
|
|
4310
|
+
let entryX = pillXOff + pillWidth + LG_ICON_W + 4;
|
|
4121
4311
|
for (const entry of lg.group.entries) {
|
|
4122
4312
|
const tagKey = lg.group.name.toLowerCase();
|
|
4123
4313
|
const tagVal = entry.value.toLowerCase();
|
|
@@ -4179,6 +4369,7 @@ export function renderTimeline(
|
|
|
4179
4369
|
}
|
|
4180
4370
|
|
|
4181
4371
|
function recolorEvents() {
|
|
4372
|
+
const colorTG = currentActiveGroup ?? swimlaneTagGroup ?? null;
|
|
4182
4373
|
mainG.selectAll<SVGGElement, unknown>('.tl-event').each(function () {
|
|
4183
4374
|
const el = d3Selection.select(this);
|
|
4184
4375
|
const lineNum = el.attr('data-line-number');
|
|
@@ -4186,9 +4377,9 @@ export function renderTimeline(
|
|
|
4186
4377
|
if (!ev) return;
|
|
4187
4378
|
|
|
4188
4379
|
let color: string;
|
|
4189
|
-
if (
|
|
4380
|
+
if (colorTG) {
|
|
4190
4381
|
const tagColor = resolveTagColor(
|
|
4191
|
-
ev.metadata, parsed.timelineTagGroups,
|
|
4382
|
+
ev.metadata, parsed.timelineTagGroups, colorTG
|
|
4192
4383
|
);
|
|
4193
4384
|
color = tagColor ?? (ev.group && groupColorMap.has(ev.group)
|
|
4194
4385
|
? groupColorMap.get(ev.group)! : textColor);
|
|
@@ -5525,6 +5716,7 @@ export async function renderD3ForExport(
|
|
|
5525
5716
|
collapsedNodes?: Set<string>;
|
|
5526
5717
|
activeTagGroup?: string | null;
|
|
5527
5718
|
hiddenAttributes?: Set<string>;
|
|
5719
|
+
swimlaneTagGroup?: string | null;
|
|
5528
5720
|
},
|
|
5529
5721
|
options?: { branding?: boolean; c4Level?: 'context' | 'containers' | 'components' | 'deployment'; c4System?: string; c4Container?: string; scenario?: string }
|
|
5530
5722
|
): Promise<string> {
|
|
@@ -5830,7 +6022,8 @@ export async function renderD3ForExport(
|
|
|
5830
6022
|
} else if (parsed.type === 'arc') {
|
|
5831
6023
|
renderArcDiagram(container, parsed, effectivePalette, isDark, undefined, dims);
|
|
5832
6024
|
} else if (parsed.type === 'timeline') {
|
|
5833
|
-
renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims
|
|
6025
|
+
renderTimeline(container, parsed, effectivePalette, isDark, undefined, dims,
|
|
6026
|
+
orgExportState?.activeTagGroup, orgExportState?.swimlaneTagGroup);
|
|
5834
6027
|
} else if (parsed.type === 'venn') {
|
|
5835
6028
|
renderVenn(container, parsed, effectivePalette, isDark, undefined, dims);
|
|
5836
6029
|
} else if (parsed.type === 'quadrant') {
|
package/src/sharing.ts
CHANGED
|
@@ -9,6 +9,7 @@ const COMPRESSED_SIZE_LIMIT = 8192; // 8 KB
|
|
|
9
9
|
export interface DiagramViewState {
|
|
10
10
|
activeTagGroup?: string;
|
|
11
11
|
collapsedGroups?: string[];
|
|
12
|
+
swimlaneTagGroup?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface DecodedDiagramUrl {
|
|
@@ -52,6 +53,10 @@ export function encodeDiagramUrl(
|
|
|
52
53
|
hash += `&cg=${encodeURIComponent(options.viewState.collapsedGroups.join(','))}`;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
if (options?.viewState?.swimlaneTagGroup) {
|
|
57
|
+
hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
// Encode in both query param AND hash fragment — some share mechanisms
|
|
56
61
|
// strip one or the other (iOS share sheet strips #, AirDrop strips ?)
|
|
57
62
|
return { url: `${baseUrl}?${hash}#${hash}` };
|
|
@@ -97,6 +102,9 @@ export function decodeDiagramUrl(hash: string): DecodedDiagramUrl {
|
|
|
97
102
|
if (key === 'cg' && val) {
|
|
98
103
|
viewState.collapsedGroups = val.split(',').filter(Boolean);
|
|
99
104
|
}
|
|
105
|
+
if (key === 'swim' && val) {
|
|
106
|
+
viewState.swimlaneTagGroup = val;
|
|
107
|
+
}
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
// Strip 'dgmo=' prefix
|