@diagrammo/dgmo 0.8.8 → 0.8.10

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/src/d3.ts CHANGED
@@ -205,9 +205,14 @@ import {
205
205
  LEGEND_ENTRY_FONT_SIZE as TL_LEGEND_ENTRY_FONT_SIZE,
206
206
  LEGEND_ENTRY_DOT_GAP as TL_LEGEND_ENTRY_DOT_GAP,
207
207
  LEGEND_ENTRY_TRAIL as TL_LEGEND_ENTRY_TRAIL,
208
- LEGEND_GROUP_GAP as TL_LEGEND_GROUP_GAP,
209
208
  measureLegendText,
210
209
  } from './utils/legend-constants';
210
+ import { renderLegendD3 } from './utils/legend-d3';
211
+ import type {
212
+ LegendConfig,
213
+ LegendState,
214
+ LegendCallbacks,
215
+ } from './utils/legend-types';
211
216
  import {
212
217
  TITLE_FONT_SIZE,
213
218
  TITLE_FONT_WEIGHT,
@@ -4866,7 +4871,7 @@ export function renderTimeline(
4866
4871
  const LG_ENTRY_FONT_SIZE = TL_LEGEND_ENTRY_FONT_SIZE;
4867
4872
  const LG_ENTRY_DOT_GAP = TL_LEGEND_ENTRY_DOT_GAP;
4868
4873
  const LG_ENTRY_TRAIL = TL_LEGEND_ENTRY_TRAIL;
4869
- const LG_GROUP_GAP = TL_LEGEND_GROUP_GAP;
4874
+ // LG_GROUP_GAP no longer needed — centralized legend handles spacing
4870
4875
  const LG_ICON_W = 20; // swimlane icon area (icon + surrounding space) — local
4871
4876
 
4872
4877
  const mainSvg = d3Selection.select(container).select<SVGSVGElement>('svg');
@@ -4875,10 +4880,6 @@ export function renderTimeline(
4875
4880
  // Position legend at top, below title
4876
4881
  const legendY = title ? 50 : 10;
4877
4882
 
4878
- const groupBg = isDark
4879
- ? mix(palette.surface, palette.bg, 50)
4880
- : mix(palette.surface, palette.bg, 30);
4881
-
4882
4883
  // Pre-compute group widths (minified and expanded)
4883
4884
  type LegendGroup = {
4884
4885
  group: TagGroup;
@@ -4980,20 +4981,6 @@ export function renderTimeline(
4980
4981
 
4981
4982
  if (visibleGroups.length === 0) return;
4982
4983
 
4983
- // Compute total width and center horizontally in SVG
4984
- const totalW =
4985
- visibleGroups.reduce((s, lg) => {
4986
- const isActive =
4987
- viewMode ||
4988
- (currentActiveGroup != null &&
4989
- lg.group.name.toLowerCase() ===
4990
- currentActiveGroup.toLowerCase());
4991
- return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
4992
- }, 0) +
4993
- (visibleGroups.length - 1) * LG_GROUP_GAP;
4994
-
4995
- let cx = (width - totalW) / 2;
4996
-
4997
4984
  // Legend container for data-legend-active attribute
4998
4985
  const legendContainer = mainSvg
4999
4986
  .append('g')
@@ -5005,177 +4992,113 @@ export function renderTimeline(
5005
4992
  );
5006
4993
  }
5007
4994
 
5008
- for (const lg of visibleGroups) {
5009
- const groupKey = lg.group.name.toLowerCase();
5010
- const isActive =
5011
- viewMode ||
5012
- (currentActiveGroup != null &&
5013
- currentActiveGroup.toLowerCase() === groupKey);
5014
- const isSwimActive =
5015
- currentSwimlaneGroup != null &&
5016
- currentSwimlaneGroup.toLowerCase() === groupKey;
5017
-
5018
- const pillLabel = lg.group.name;
5019
- const pillWidth =
5020
- measureLegendText(pillLabel, LG_PILL_FONT_SIZE) + LG_PILL_PAD;
5021
-
5022
- const gEl = legendContainer
5023
- .append('g')
5024
- .attr('transform', `translate(${cx}, ${legendY})`)
5025
- .attr('class', 'tl-tag-legend-group tl-tag-legend-entry')
5026
- .attr('data-legend-group', groupKey)
5027
- .attr('data-tag-group', groupKey)
5028
- .attr('data-legend-entry', '__group__');
5029
-
5030
- if (!viewMode) {
5031
- gEl.style('cursor', 'pointer').on('click', () => {
5032
- currentActiveGroup =
5033
- currentActiveGroup === groupKey ? null : groupKey;
5034
- drawLegend();
5035
- recolorEvents();
5036
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
5037
- });
5038
- }
5039
-
5040
- // Outer capsule background (active only)
5041
- if (isActive) {
5042
- gEl
5043
- .append('rect')
5044
- .attr('width', lg.expandedWidth)
5045
- .attr('height', LG_HEIGHT)
5046
- .attr('rx', LG_HEIGHT / 2)
5047
- .attr('fill', groupBg);
5048
- }
5049
-
5050
- const pillXOff = isActive ? LG_CAPSULE_PAD : 0;
5051
- const pillYOff = LG_CAPSULE_PAD;
5052
- const pillH = LG_HEIGHT - LG_CAPSULE_PAD * 2;
5053
-
5054
- // Pill background
5055
- gEl
5056
- .append('rect')
5057
- .attr('x', pillXOff)
5058
- .attr('y', pillYOff)
5059
- .attr('width', pillWidth)
5060
- .attr('height', pillH)
5061
- .attr('rx', pillH / 2)
5062
- .attr('fill', isActive ? palette.bg : groupBg);
5063
-
5064
- // Active pill border
5065
- if (isActive) {
5066
- gEl
5067
- .append('rect')
5068
- .attr('x', pillXOff)
5069
- .attr('y', pillYOff)
5070
- .attr('width', pillWidth)
5071
- .attr('height', pillH)
5072
- .attr('rx', pillH / 2)
5073
- .attr('fill', 'none')
5074
- .attr('stroke', mix(palette.textMuted, palette.bg, 50))
5075
- .attr('stroke-width', 0.75);
5076
- }
5077
-
5078
- // Pill text
5079
- gEl
5080
- .append('text')
5081
- .attr('x', pillXOff + pillWidth / 2)
5082
- .attr('y', LG_HEIGHT / 2 + LG_PILL_FONT_SIZE / 2 - 2)
5083
- .attr('font-size', LG_PILL_FONT_SIZE)
5084
- .attr('font-weight', '500')
5085
- .attr('font-family', FONT_FAMILY)
5086
- .attr('fill', isActive ? palette.text : palette.textMuted)
5087
- .attr('text-anchor', 'middle')
5088
- .text(pillLabel);
5089
-
5090
- // Entries + swimlane icon inside capsule (active only)
5091
- if (isActive) {
5092
- // Swimlane icon (skip in view mode — non-interactive)
5093
- let entryX: number;
5094
- if (!viewMode) {
5095
- const iconX = pillXOff + pillWidth + 5;
5096
- const iconY = (LG_HEIGHT - 10) / 2; // vertically centered
5097
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimActive);
5098
- iconEl
5099
- .attr('data-swimlane-toggle', groupKey)
5100
- .on('click', (event: MouseEvent) => {
5101
- event.stopPropagation();
5102
- currentSwimlaneGroup =
5103
- currentSwimlaneGroup === groupKey ? null : groupKey;
5104
- onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
5105
- relayout();
5106
- });
5107
- entryX = pillXOff + pillWidth + LG_ICON_W + 4;
5108
- } else {
5109
- entryX = pillXOff + pillWidth + 8;
5110
- }
5111
-
5112
- for (const entry of lg.group.entries) {
5113
- const tagKey = lg.group.name.toLowerCase();
5114
- const tagVal = entry.value.toLowerCase();
5115
-
5116
- const entryG = gEl
5117
- .append('g')
5118
- .attr('class', 'tl-tag-legend-entry')
5119
- .attr('data-tag-group', tagKey)
5120
- .attr('data-legend-entry', tagVal);
5121
-
5122
- if (!viewMode) {
5123
- entryG
5124
- .style('cursor', 'pointer')
5125
- .on('mouseenter', (event: MouseEvent) => {
5126
- event.stopPropagation();
5127
- fadeToTagValue(mainG, tagKey, tagVal);
5128
- mainSvg
5129
- .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5130
- .each(function () {
5131
- const el = d3Selection.select(this);
5132
- const ev = el.attr('data-legend-entry');
5133
- if (ev === '__group__') return;
5134
- const eg = el.attr('data-tag-group');
5135
- el.attr(
5136
- 'opacity',
5137
- eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
5138
- );
5139
- });
5140
- })
5141
- .on('mouseleave', (event: MouseEvent) => {
5142
- event.stopPropagation();
5143
- fadeReset(mainG);
5144
- mainSvg
5145
- .selectAll<SVGGElement, unknown>('.tl-tag-legend-entry')
5146
- .attr('opacity', 1);
5147
- })
5148
- .on('click', (event: MouseEvent) => {
5149
- event.stopPropagation();
5150
- });
5151
- }
5152
-
5153
- entryG
5154
- .append('circle')
5155
- .attr('cx', entryX + LG_DOT_R)
5156
- .attr('cy', LG_HEIGHT / 2)
5157
- .attr('r', LG_DOT_R)
5158
- .attr('fill', entry.color);
5159
-
5160
- const textX = entryX + LG_DOT_R * 2 + LG_ENTRY_DOT_GAP;
5161
- entryG
5162
- .append('text')
5163
- .attr('x', textX)
5164
- .attr('y', LG_HEIGHT / 2 + LG_ENTRY_FONT_SIZE / 2 - 1)
5165
- .attr('font-size', LG_ENTRY_FONT_SIZE)
5166
- .attr('font-family', FONT_FAMILY)
5167
- .attr('fill', palette.textMuted)
5168
- .text(entry.value);
5169
-
5170
- entryX =
5171
- textX +
5172
- measureLegendText(entry.value, LG_ENTRY_FONT_SIZE) +
5173
- LG_ENTRY_TRAIL;
5174
- }
5175
- }
5176
-
5177
- cx += (isActive ? lg.expandedWidth : lg.minifiedWidth) + LG_GROUP_GAP;
5178
- }
4995
+ // Render tag groups via centralized legend system
4996
+ const iconAddon = viewMode ? 0 : LG_ICON_W;
4997
+ const centralGroups = visibleGroups.map((lg) => ({
4998
+ name: lg.group.name,
4999
+ entries: lg.group.entries.map((e) => ({
5000
+ value: e.value,
5001
+ color: e.color,
5002
+ })),
5003
+ }));
5004
+
5005
+ // Determine effective active group for centralized renderer
5006
+ const centralActive = viewMode ? effectiveColorKey : currentActiveGroup;
5007
+
5008
+ const centralConfig: LegendConfig = {
5009
+ groups: centralGroups,
5010
+ position: { placement: 'top-center', titleRelation: 'below-title' },
5011
+ mode: 'fixed',
5012
+ capsulePillAddonWidth: iconAddon,
5013
+ };
5014
+ const centralState: LegendState = { activeGroup: centralActive };
5015
+
5016
+ const centralCallbacks: LegendCallbacks = viewMode
5017
+ ? {}
5018
+ : {
5019
+ onGroupToggle: (groupName) => {
5020
+ currentActiveGroup =
5021
+ currentActiveGroup === groupName.toLowerCase()
5022
+ ? null
5023
+ : groupName.toLowerCase();
5024
+ drawLegend();
5025
+ recolorEvents();
5026
+ onTagStateChange?.(currentActiveGroup, currentSwimlaneGroup);
5027
+ },
5028
+ onEntryHover: (groupName, entryValue) => {
5029
+ const tagKey = groupName.toLowerCase();
5030
+ if (entryValue) {
5031
+ const tagVal = entryValue.toLowerCase();
5032
+ fadeToTagValue(mainG, tagKey, tagVal);
5033
+ mainSvg
5034
+ .selectAll<SVGGElement, unknown>('[data-legend-entry]')
5035
+ .each(function () {
5036
+ const el = d3Selection.select(this);
5037
+ const ev = el.attr('data-legend-entry');
5038
+ const eg =
5039
+ el.attr('data-tag-group') ??
5040
+ (el.node() as Element)
5041
+ ?.closest?.('[data-tag-group]')
5042
+ ?.getAttribute('data-tag-group');
5043
+ el.attr(
5044
+ 'opacity',
5045
+ eg === tagKey && ev === tagVal ? 1 : FADE_OPACITY
5046
+ );
5047
+ });
5048
+ } else {
5049
+ fadeReset(mainG);
5050
+ mainSvg
5051
+ .selectAll<SVGGElement, unknown>('[data-legend-entry]')
5052
+ .attr('opacity', 1);
5053
+ }
5054
+ },
5055
+ onGroupRendered: (groupName, groupEl, isActive) => {
5056
+ const groupKey = groupName.toLowerCase();
5057
+ groupEl.attr('data-tag-group', groupKey);
5058
+ if (isActive && !viewMode) {
5059
+ const isSwimActive =
5060
+ currentSwimlaneGroup != null &&
5061
+ currentSwimlaneGroup.toLowerCase() === groupKey;
5062
+ const pillWidth =
5063
+ measureLegendText(groupName, LG_PILL_FONT_SIZE) +
5064
+ LG_PILL_PAD;
5065
+ const pillXOff = LG_CAPSULE_PAD;
5066
+ const iconX = pillXOff + pillWidth + 5;
5067
+ const iconY = (LG_HEIGHT - 10) / 2;
5068
+ const iconEl = drawSwimlaneIcon(
5069
+ groupEl,
5070
+ iconX,
5071
+ iconY,
5072
+ isSwimActive
5073
+ );
5074
+ iconEl
5075
+ .attr('data-swimlane-toggle', groupKey)
5076
+ .on('click', (event: MouseEvent) => {
5077
+ event.stopPropagation();
5078
+ currentSwimlaneGroup =
5079
+ currentSwimlaneGroup === groupKey ? null : groupKey;
5080
+ onTagStateChange?.(
5081
+ currentActiveGroup,
5082
+ currentSwimlaneGroup
5083
+ );
5084
+ relayout();
5085
+ });
5086
+ }
5087
+ },
5088
+ };
5089
+
5090
+ const legendInnerG = legendContainer
5091
+ .append('g')
5092
+ .attr('transform', `translate(0, ${legendY})`);
5093
+ renderLegendD3(
5094
+ legendInnerG,
5095
+ centralConfig,
5096
+ centralState,
5097
+ palette,
5098
+ isDark,
5099
+ centralCallbacks,
5100
+ width
5101
+ );
5179
5102
  }
5180
5103
 
5181
5104
  // Build a quick lineNumber→event lookup