@diagrammo/dgmo 0.8.9 → 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.
@@ -22,6 +22,12 @@ import {
22
22
  LEGEND_ICON_W,
23
23
  measureLegendText,
24
24
  } from '../utils/legend-constants';
25
+ import { renderLegendD3 } from '../utils/legend-d3';
26
+ import type {
27
+ LegendConfig,
28
+ LegendState,
29
+ LegendCallbacks,
30
+ } from '../utils/legend-types';
25
31
  import {
26
32
  TITLE_FONT_SIZE,
27
33
  TITLE_FONT_WEIGHT,
@@ -1893,7 +1899,7 @@ function renderTagLegend(
1893
1899
  const isSwimlane =
1894
1900
  currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
1895
1901
  const showIcon = !legendViewMode && tagGroups.length > 0;
1896
- const iconReserve = showIcon ? LEGEND_ICON_W : 0;
1902
+ const iconReserve = showIcon && isActive ? LEGEND_ICON_W : 0;
1897
1903
  const pillW =
1898
1904
  measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
1899
1905
  LEGEND_PILL_PAD +
@@ -1940,196 +1946,148 @@ function renderTagLegend(
1940
1946
 
1941
1947
  let cursorX = 0;
1942
1948
 
1943
- for (let i = 0; i < visibleGroups.length; i++) {
1944
- const group = visibleGroups[i];
1945
- const isActive =
1946
- activeGroupName?.toLowerCase() === group.name.toLowerCase();
1947
- const isSwimlane =
1948
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase();
1949
+ // Render tag groups via centralized legend system
1950
+ if (visibleGroups.length > 0) {
1949
1951
  const showIcon = !legendViewMode && tagGroups.length > 0;
1950
1952
  const iconReserve = showIcon ? LEGEND_ICON_W : 0;
1951
- const pillW =
1952
- measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) +
1953
- LEGEND_PILL_PAD +
1954
- iconReserve;
1955
- const pillH = LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2;
1956
- const groupW = groupWidths[i];
1957
-
1958
- const gEl = legendRow
1959
- .append('g')
1960
- .attr('transform', `translate(${cursorX}, 0)`)
1961
- .attr('class', 'gantt-tag-legend-group')
1962
- .attr('data-tag-group', group.name)
1963
- .attr('data-line-number', String(group.lineNumber))
1964
- .style('cursor', 'pointer')
1965
- .on('click', () => {
1966
- if (onToggle) onToggle(group.name);
1967
- });
1968
-
1969
- if (isActive) {
1970
- // Outer capsule background
1971
- gEl
1972
- .append('rect')
1973
- .attr('width', groupW)
1974
- .attr('height', LEGEND_HEIGHT)
1975
- .attr('rx', LEGEND_HEIGHT / 2)
1976
- .attr('fill', groupBg);
1977
- }
1978
1953
 
1979
- const pillXOff = isActive ? LEGEND_CAPSULE_PAD : 0;
1980
- const pillYOff = LEGEND_CAPSULE_PAD;
1981
-
1982
- // Pill background
1983
- gEl
1984
- .append('rect')
1985
- .attr('x', pillXOff)
1986
- .attr('y', pillYOff)
1987
- .attr('width', pillW)
1988
- .attr('height', pillH)
1989
- .attr('rx', pillH / 2)
1990
- .attr('fill', isActive ? palette.bg : groupBg);
1991
-
1992
- // Active pill border
1993
- if (isActive) {
1994
- gEl
1995
- .append('rect')
1996
- .attr('x', pillXOff)
1997
- .attr('y', pillYOff)
1998
- .attr('width', pillW)
1999
- .attr('height', pillH)
2000
- .attr('rx', pillH / 2)
2001
- .attr('fill', 'none')
2002
- .attr('stroke', mix(palette.textMuted, palette.bg, 50))
2003
- .attr('stroke-width', 0.75);
2004
- }
1954
+ // Build groups with filtered entries
1955
+ const legendGroups = visibleGroups.map((g) => {
1956
+ const key = g.name.toLowerCase();
1957
+ const entries = filteredEntries.get(key) ?? g.entries;
1958
+ return {
1959
+ name: g.name,
1960
+ entries: entries.map((e) => ({ value: e.value, color: e.color })),
1961
+ };
1962
+ });
2005
1963
 
2006
- // Pill text (offset to leave room for icon on right)
2007
- const textW =
2008
- measureLegendText(group.name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
2009
- gEl
2010
- .append('text')
2011
- .attr('x', pillXOff + textW / 2)
2012
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2)
2013
- .attr('text-anchor', 'middle')
2014
- .attr('font-size', `${LEGEND_PILL_FONT_SIZE}px`)
2015
- .attr('font-weight', '500')
2016
- .attr('fill', isActive || isSwimlane ? palette.text : palette.textMuted)
2017
- .text(group.name);
2018
-
2019
- // swimlane icon (after pill name)
2020
- if (showIcon) {
2021
- const iconX = pillXOff + textW + 3;
2022
- const iconY = (LEGEND_HEIGHT - 10) / 2;
2023
- const iconEl = drawSwimlaneIcon(gEl, iconX, iconY, isSwimlane, palette);
2024
- iconEl.append('title').text(`Group by ${group.name}`);
2025
- iconEl.style('cursor', 'pointer').on('click', (event: Event) => {
2026
- event.stopPropagation();
2027
- if (onSwimlaneChange) {
2028
- onSwimlaneChange(
2029
- currentSwimlaneGroup?.toLowerCase() === group.name.toLowerCase()
2030
- ? null
2031
- : group.name
2032
- );
1964
+ const legendConfig: LegendConfig = {
1965
+ groups: legendGroups,
1966
+ position: {
1967
+ placement: 'top-center' as const,
1968
+ titleRelation: 'below-title' as const,
1969
+ },
1970
+ mode: 'fixed' as const,
1971
+ capsulePillAddonWidth: iconReserve,
1972
+ };
1973
+ const legendState: LegendState = { activeGroup: activeGroupName };
1974
+
1975
+ const tagGroupsW =
1976
+ visibleGroups.reduce((s, _, i) => s + groupWidths[i], 0) +
1977
+ Math.max(0, (visibleGroups.length - 1) * LEGEND_GROUP_GAP);
1978
+ const tagGroupG = legendRow.append('g');
1979
+
1980
+ const legendCallbacks: LegendCallbacks = {
1981
+ onGroupToggle: onToggle,
1982
+ onEntryHover: (groupName, entryValue) => {
1983
+ const tagKey = groupName.toLowerCase();
1984
+ if (entryValue) {
1985
+ const ev = entryValue.toLowerCase();
1986
+ chartG
1987
+ .selectAll<SVGGElement, unknown>('.gantt-task')
1988
+ .each(function () {
1989
+ const el = d3Selection.select(this);
1990
+ el.attr(
1991
+ 'opacity',
1992
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
1993
+ );
1994
+ });
1995
+ chartG
1996
+ .selectAll<SVGElement, unknown>('.gantt-milestone')
1997
+ .attr('opacity', FADE_OPACITY);
1998
+ chartG
1999
+ .selectAll<
2000
+ SVGElement,
2001
+ unknown
2002
+ >('.gantt-group-bar, .gantt-group-summary')
2003
+ .attr('opacity', FADE_OPACITY);
2004
+ svg
2005
+ .selectAll<SVGTextElement, unknown>('.gantt-task-label')
2006
+ .each(function () {
2007
+ const el = d3Selection.select(this);
2008
+ el.attr(
2009
+ 'opacity',
2010
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
2011
+ );
2012
+ });
2013
+ svg
2014
+ .selectAll<SVGGElement, unknown>('.gantt-group-label')
2015
+ .attr('opacity', FADE_OPACITY);
2016
+ svg
2017
+ .selectAll<SVGGElement, unknown>('.gantt-lane-header')
2018
+ .each(function () {
2019
+ const el = d3Selection.select(this);
2020
+ el.attr(
2021
+ 'opacity',
2022
+ el.attr(`data-tag-${tagKey}`) === ev ? 1 : FADE_OPACITY
2023
+ );
2024
+ });
2025
+ chartG
2026
+ .selectAll<
2027
+ SVGElement,
2028
+ unknown
2029
+ >('.gantt-lane-band, .gantt-lane-accent')
2030
+ .attr('opacity', FADE_OPACITY);
2031
+ } else {
2032
+ if (criticalPathActive) {
2033
+ applyCriticalPathHighlight(svg, chartG);
2034
+ } else {
2035
+ resetHighlightAll(svg, chartG);
2036
+ }
2033
2037
  }
2034
- });
2035
- }
2036
-
2037
- // Entries (when active expanded color group, only used values)
2038
- if (isActive) {
2039
- const tagKey = group.name.toLowerCase();
2040
- const entries = filteredEntries.get(tagKey) ?? group.entries;
2041
- let ex = pillXOff + pillW + LEGEND_CAPSULE_PAD + 4;
2042
- for (const entry of entries) {
2043
- const entryValue = entry.value.toLowerCase();
2044
-
2045
- // Wrap dot + label in a <g> for hover targeting
2046
- const entryG = gEl
2047
- .append('g')
2048
- .attr('class', 'gantt-legend-entry')
2049
- .attr('data-line-number', String(entry.lineNumber))
2050
- .style('cursor', 'pointer');
2051
-
2052
- // Dot
2053
- entryG
2054
- .append('circle')
2055
- .attr('cx', ex + LEGEND_DOT_R)
2056
- .attr('cy', LEGEND_HEIGHT / 2)
2057
- .attr('r', LEGEND_DOT_R)
2058
- .attr('fill', entry.color);
2059
-
2060
- // Label
2061
- entryG
2062
- .append('text')
2063
- .attr('x', ex + LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP)
2064
- .attr('y', LEGEND_HEIGHT / 2 + LEGEND_ENTRY_FONT_SIZE / 2 - 2)
2065
- .attr('text-anchor', 'start')
2066
- .attr('font-size', `${LEGEND_ENTRY_FONT_SIZE}px`)
2067
- .attr('fill', palette.textMuted)
2068
- .text(entry.value);
2069
-
2070
- // Hover: highlight matching tasks + labels + lane headers, fade others
2071
- entryG
2072
- .on('mouseenter', () => {
2073
- chartG
2074
- .selectAll<SVGGElement, unknown>('.gantt-task')
2075
- .each(function () {
2076
- const el = d3Selection.select(this);
2077
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
2078
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
2079
- });
2080
- chartG
2081
- .selectAll<SVGElement, unknown>('.gantt-milestone')
2082
- .attr('opacity', FADE_OPACITY);
2083
- chartG
2084
- .selectAll<
2085
- SVGElement,
2086
- unknown
2087
- >('.gantt-group-bar, .gantt-group-summary')
2088
- .attr('opacity', FADE_OPACITY);
2089
- // Fade left-side task labels
2090
- svg
2091
- .selectAll<SVGTextElement, unknown>('.gantt-task-label')
2092
- .each(function () {
2093
- const el = d3Selection.select(this);
2094
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
2095
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
2096
- });
2097
- // Fade group labels
2098
- svg
2099
- .selectAll<SVGGElement, unknown>('.gantt-group-label')
2100
- .attr('opacity', FADE_OPACITY);
2101
- // Fade non-matching lane headers + bands + accents
2102
- svg
2103
- .selectAll<SVGGElement, unknown>('.gantt-lane-header')
2104
- .each(function () {
2105
- const el = d3Selection.select(this);
2106
- const matches = el.attr(`data-tag-${tagKey}`) === entryValue;
2107
- el.attr('opacity', matches ? 1 : FADE_OPACITY);
2108
- });
2109
- chartG
2110
- .selectAll<
2111
- SVGElement,
2112
- unknown
2113
- >('.gantt-lane-band, .gantt-lane-accent')
2114
- .attr('opacity', FADE_OPACITY);
2115
- })
2116
- .on('mouseleave', () => {
2117
- if (criticalPathActive) {
2118
- applyCriticalPathHighlight(svg, chartG);
2119
- } else {
2120
- resetHighlightAll(svg, chartG);
2038
+ },
2039
+ onGroupRendered: (groupName, groupEl, _isActive) => {
2040
+ // Add swimlane icon and data attributes
2041
+ const group = visibleGroups.find((g) => g.name === groupName);
2042
+ if (group) {
2043
+ groupEl
2044
+ .attr('data-tag-group', group.name)
2045
+ .attr('data-line-number', String(group.lineNumber));
2046
+ }
2047
+ if (showIcon && _isActive) {
2048
+ const isSwimlane =
2049
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase();
2050
+ const textW =
2051
+ measureLegendText(groupName, LEGEND_PILL_FONT_SIZE) +
2052
+ LEGEND_PILL_PAD;
2053
+ const pillXOff = LEGEND_CAPSULE_PAD;
2054
+ const iconX = pillXOff + textW + 3;
2055
+ const iconY = (LEGEND_HEIGHT - 10) / 2;
2056
+ const iconEl = drawSwimlaneIcon(
2057
+ groupEl,
2058
+ iconX,
2059
+ iconY,
2060
+ isSwimlane,
2061
+ palette
2062
+ );
2063
+ iconEl.append('title').text(`Group by ${groupName}`);
2064
+ iconEl.style('cursor', 'pointer').on('click', (event: Event) => {
2065
+ event.stopPropagation();
2066
+ if (onSwimlaneChange) {
2067
+ onSwimlaneChange(
2068
+ currentSwimlaneGroup?.toLowerCase() === groupName.toLowerCase()
2069
+ ? null
2070
+ : groupName
2071
+ );
2121
2072
  }
2122
2073
  });
2074
+ }
2075
+ },
2076
+ };
2123
2077
 
2124
- ex +=
2125
- LEGEND_DOT_R * 2 +
2126
- LEGEND_ENTRY_DOT_GAP +
2127
- measureLegendText(entry.value, LEGEND_ENTRY_FONT_SIZE) +
2128
- LEGEND_ENTRY_TRAIL;
2129
- }
2130
- }
2078
+ renderLegendD3(
2079
+ tagGroupG,
2080
+ legendConfig,
2081
+ legendState,
2082
+ palette,
2083
+ isDark,
2084
+ legendCallbacks,
2085
+ tagGroupsW
2086
+ );
2131
2087
 
2132
- cursorX += groupW + LEGEND_GROUP_GAP;
2088
+ for (let i = 0; i < visibleGroups.length; i++) {
2089
+ cursorX += groupWidths[i] + LEGEND_GROUP_GAP;
2090
+ }
2133
2091
  }
2134
2092
 
2135
2093
  // Critical Path pill
package/src/index.ts CHANGED
@@ -356,8 +356,28 @@ export {
356
356
  computeScatterLabelGraphics,
357
357
  } from './echarts';
358
358
  export type { ScatterLabelPoint } from './echarts';
359
- export { renderLegendSvg, type LegendGroupData } from './utils/legend-svg';
359
+ export {
360
+ renderLegendSvg,
361
+ renderLegendSvgFromConfig,
362
+ type LegendGroupData,
363
+ } from './utils/legend-svg';
360
364
  export { LEGEND_HEIGHT } from './utils/legend-constants';
365
+ export { renderLegendD3 } from './utils/legend-d3';
366
+ export {
367
+ computeLegendLayout,
368
+ getLegendReservedHeight,
369
+ } from './utils/legend-layout';
370
+ export type {
371
+ LegendConfig,
372
+ LegendState,
373
+ LegendCallbacks,
374
+ LegendPosition,
375
+ LegendMode,
376
+ LegendControl,
377
+ LegendLayout,
378
+ LegendHandle,
379
+ LegendPalette,
380
+ } from './utils/legend-types';
361
381
  export { buildMermaidQuadrant } from './dgmo-mermaid';
362
382
 
363
383
  // ============================================================