@unovis/ts 1.7.0-Phoenix.0 → 1.7.0-pre.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.
Files changed (100) hide show
  1. package/components/annotations/config.d.ts +2 -0
  2. package/components/annotations/config.js +1 -1
  3. package/components/annotations/config.js.map +1 -1
  4. package/components/annotations/index.d.ts +1 -0
  5. package/components/annotations/index.js +25 -10
  6. package/components/annotations/index.js.map +1 -1
  7. package/components/annotations/style.d.ts +2 -0
  8. package/components/annotations/style.js +8 -1
  9. package/components/annotations/style.js.map +1 -1
  10. package/components/area/config.d.ts +11 -1
  11. package/components/area/config.js +1 -1
  12. package/components/area/config.js.map +1 -1
  13. package/components/area/index.d.ts +6 -0
  14. package/components/area/index.js +80 -7
  15. package/components/area/index.js.map +1 -1
  16. package/components/area/style.d.ts +1 -0
  17. package/components/area/style.js +7 -1
  18. package/components/area/style.js.map +1 -1
  19. package/components/axis/index.d.ts +2 -0
  20. package/components/axis/index.js +45 -7
  21. package/components/axis/index.js.map +1 -1
  22. package/components/bullet-legend/index.d.ts +2 -0
  23. package/components/bullet-legend/index.js +9 -5
  24. package/components/bullet-legend/index.js.map +1 -1
  25. package/components/bullet-legend/modules/shape.js +3 -2
  26. package/components/bullet-legend/modules/shape.js.map +1 -1
  27. package/components/crosshair/config.d.ts +1 -1
  28. package/components/crosshair/config.js.map +1 -1
  29. package/components/crosshair/index.d.ts +1 -1
  30. package/components/crosshair/index.js +3 -2
  31. package/components/crosshair/index.js.map +1 -1
  32. package/components/flow-legend/config.d.ts +10 -0
  33. package/components/flow-legend/config.js +4 -0
  34. package/components/flow-legend/config.js.map +1 -1
  35. package/components/flow-legend/index.d.ts +6 -2
  36. package/components/flow-legend/index.js +34 -16
  37. package/components/flow-legend/index.js.map +1 -1
  38. package/components/flow-legend/style.d.ts +3 -3
  39. package/components/flow-legend/style.js +30 -26
  40. package/components/flow-legend/style.js.map +1 -1
  41. package/components/free-brush/types.js +1 -0
  42. package/components/free-brush/types.js.map +1 -1
  43. package/components/graph/index.d.ts +1 -0
  44. package/components/graph/index.js +35 -14
  45. package/components/graph/index.js.map +1 -1
  46. package/components/graph/modules/link/index.js +2 -2
  47. package/components/graph/modules/link/index.js.map +1 -1
  48. package/components/graph/modules/node/index.js +2 -1
  49. package/components/graph/modules/node/index.js.map +1 -1
  50. package/components/leaflet-map/modules/map.js +2 -2
  51. package/components/leaflet-map/modules/map.js.map +1 -1
  52. package/components/leaflet-map/renderer/mapboxgl-utils.d.ts +0 -1
  53. package/components/leaflet-map/renderer/mapboxgl-utils.js +1 -2
  54. package/components/leaflet-map/renderer/mapboxgl-utils.js.map +1 -1
  55. package/components/sankey/config.d.ts +25 -10
  56. package/components/sankey/config.js +2 -2
  57. package/components/sankey/config.js.map +1 -1
  58. package/components/sankey/index.d.ts +26 -31
  59. package/components/sankey/index.js +340 -115
  60. package/components/sankey/index.js.map +1 -1
  61. package/components/sankey/modules/label.d.ts +8 -5
  62. package/components/sankey/modules/label.js +70 -32
  63. package/components/sankey/modules/label.js.map +1 -1
  64. package/components/sankey/modules/link.d.ts +1 -0
  65. package/components/sankey/modules/link.js +23 -41
  66. package/components/sankey/modules/link.js.map +1 -1
  67. package/components/sankey/modules/node.d.ts +5 -4
  68. package/components/sankey/modules/node.js +65 -16
  69. package/components/sankey/modules/node.js.map +1 -1
  70. package/components/sankey/style.d.ts +67 -1
  71. package/components/sankey/style.js +77 -77
  72. package/components/sankey/style.js.map +1 -1
  73. package/components/sankey/types.d.ts +5 -2
  74. package/components/sankey/types.js +9 -2
  75. package/components/sankey/types.js.map +1 -1
  76. package/components/tooltip/index.js +2 -2
  77. package/components/tooltip/index.js.map +1 -1
  78. package/components/treemap/index.d.ts +5 -2
  79. package/components/treemap/index.js +53 -49
  80. package/components/treemap/index.js.map +1 -1
  81. package/containers/single-container/config.d.ts +3 -0
  82. package/containers/single-container/config.js.map +1 -1
  83. package/containers/single-container/index.js +2 -1
  84. package/containers/single-container/index.js.map +1 -1
  85. package/containers/xy-container/config.d.ts +3 -0
  86. package/containers/xy-container/config.js.map +1 -1
  87. package/containers/xy-container/index.d.ts +1 -0
  88. package/containers/xy-container/index.js +13 -9
  89. package/containers/xy-container/index.js.map +1 -1
  90. package/index.js +1 -1
  91. package/package.json +2 -4
  92. package/types.js +1 -1
  93. package/utils/misc.js +13 -2
  94. package/utils/misc.js.map +1 -1
  95. package/utils/text.d.ts +1 -1
  96. package/utils/text.js +13 -15
  97. package/utils/text.js.map +1 -1
  98. package/utils/to-px.d.ts +1 -0
  99. package/utils/to-px.js +110 -0
  100. package/utils/to-px.js.map +1 -0
@@ -1,6 +1,7 @@
1
- import { select } from 'd3-selection';
1
+ import { pointer, select } from 'd3-selection';
2
+ import { zoom, zoomIdentity } from 'd3-zoom';
2
3
  import { sankey } from 'd3-sankey';
3
- import { max, extent, sum } from 'd3-array';
4
+ import { max, min, extent, sum } from 'd3-array';
4
5
  import { scaleLinear } from 'd3-scale';
5
6
  import { ComponentCore } from '../../core/component/index.js';
6
7
  import { GraphDataModel } from '../../data-models/graph.js';
@@ -8,28 +9,37 @@ import { Sizing } from '../../types/component.js';
8
9
  import { Position } from '../../types/position.js';
9
10
  import { VerticalAlign } from '../../types/text.js';
10
11
  import { smartTransition } from '../../utils/d3.js';
11
- import { getString, isNumber, getNumber, groupBy } from '../../utils/data.js';
12
- import { getCSSVariableValueInPixels } from '../../utils/misc.js';
12
+ import { getString, isNumber, clamp, getNumber, groupBy } from '../../utils/data.js';
13
13
  import { SankeyDefaultConfig } from './config.js';
14
14
  import * as style from './style.js';
15
15
  import { background, links, nodes, link, nodeGroup, nodeExit } from './style.js';
16
- import { SankeyLayout } from './types.js';
16
+ import { SankeyLayout, SankeyZoomMode } from './types.js';
17
17
  import { removeLinks, createLinks, updateLinks } from './modules/link.js';
18
- import { removeNodes, createNodes, updateNodes, onNodeMouseOver, onNodeMouseOut } from './modules/node.js';
19
- import { requiredLabelSpace, getLabelOrientation } from './modules/label.js';
18
+ import { NODE_SELECTION_RECT_DELTA, removeNodes, createNodes, updateNodes, onNodeMouseOver, onNodeMouseOut } from './modules/node.js';
19
+ import { getLabelFontSize, getSubLabelFontSize, SANKEY_LABEL_BLOCK_PADDING, SANKEY_LABEL_SPACING, getLabelOrientation, estimateRequiredLabelWidth } from './modules/label.js';
20
20
 
21
21
  class Sankey extends ComponentCore {
22
22
  constructor(config) {
23
+ var _a;
23
24
  super();
24
25
  this._defaultConfig = SankeyDefaultConfig;
25
26
  this.config = this._defaultConfig;
26
27
  this.datamodel = new GraphDataModel();
28
+ this._prevWidth = undefined;
27
29
  this._extendedWidth = undefined;
28
30
  this._extendedHeight = undefined;
29
31
  this._extendedHeightIncreased = undefined;
32
+ this._extendedWidthIncreased = undefined;
30
33
  this._sankey = sankey();
31
34
  this._highlightTimeoutId = null;
32
35
  this._highlightActive = false;
36
+ // Zoom / Pan
37
+ this._zoomScale = [1, 1];
38
+ this._pan = [0, 0];
39
+ this._prevZoomTransform = { x: 0, y: 0, k: 1 };
40
+ this._animationFrameId = null;
41
+ this._bleedCached = null;
42
+ // Events
33
43
  this.events = {
34
44
  [Sankey.selectors.nodeGroup]: {
35
45
  mouseenter: this._onNodeMouseOver.bind(this),
@@ -38,7 +48,6 @@ class Sankey extends ComponentCore {
38
48
  [Sankey.selectors.node]: {
39
49
  mouseenter: this._onNodeRectMouseOver.bind(this),
40
50
  mouseleave: this._onNodeRectMouseOut.bind(this),
41
- click: this._onNodeClick.bind(this),
42
51
  },
43
52
  [Sankey.selectors.link]: {
44
53
  mouseenter: this._onLinkMouseOver.bind(this),
@@ -47,54 +56,97 @@ class Sankey extends ComponentCore {
47
56
  };
48
57
  if (config)
49
58
  this.setConfig(config);
50
- this._backgroundRect = this.g.append('rect').attr('class', background);
59
+ // eslint-disable-next-line @typescript-eslint/naming-convention
60
+ this._gNode = this.g.node();
61
+ this._backgroundRect = this.g.append('rect').attr('class', background).style('pointer-events', 'all');
51
62
  this._linksGroup = this.g.append('g').attr('class', links);
52
63
  this._nodesGroup = this.g.append('g').attr('class', nodes);
64
+ // Initialize scale values from config
65
+ this._zoomScale = (_a = this.config.zoomScale) !== null && _a !== void 0 ? _a : [1, 1];
66
+ // Set up d3-zoom to handle wheel/pinch/drag smoothly
67
+ this._zoomBehavior = zoom()
68
+ .scaleExtent(this.config.zoomExtent)
69
+ .on('zoom', (event) => this._onZoom(event));
70
+ if (this.config.enableZoom)
71
+ this.g.call(this._zoomBehavior);
53
72
  }
54
73
  get bleed() {
55
- var _a;
56
74
  const { config, datamodel: { nodes, links } } = this;
57
- const labelFontSize = (_a = config.labelFontSize) !== null && _a !== void 0 ? _a : getCSSVariableValueInPixels('var(--vis-sankey-label-font-size)', this.element);
58
- const labelSize = requiredLabelSpace(config.labelMaxWidth, labelFontSize);
59
- let left = 0;
60
- let right = 0;
61
- // We pre-calculate sankey layout to get information about node labels placement and calculate bleed properly
62
- // Potentially it can be a performance bottleneck for large layouts, but generally rendering of such layouts is much more computationally heavy
75
+ let bleed = { top: 0, bottom: 0, left: 0, right: 0 };
63
76
  if (nodes.length) {
77
+ const labelFontSize = getLabelFontSize(config, this.element);
78
+ const subLabelFontSize = getSubLabelFontSize(config, this.element);
79
+ // We pre-calculate sankey layout to get information about node labels placement and calculate bleed properly
80
+ // Potentially it can be a performance bottleneck for large layouts, but generally rendering of such layouts is much more computationally heavy
64
81
  const sankeyProbeSize = 1000;
65
82
  this._populateLinkAndNodeValues();
66
83
  this._sankey.size([sankeyProbeSize, sankeyProbeSize]);
67
84
  this._sankey({ nodes, links });
68
- const maxDepth = max(nodes, d => d.depth);
69
- const zeroDepthNodes = nodes.filter(d => d.depth === 0);
70
- const maxDepthNodes = nodes.filter(d => d.depth === maxDepth);
71
- left = zeroDepthNodes.some(d => getLabelOrientation(d, sankeyProbeSize, config.labelPosition) === Position.Left) ? labelSize.width : 0;
72
- right = maxDepthNodes.some(d => getLabelOrientation(d, sankeyProbeSize, config.labelPosition) === Position.Right) ? labelSize.width : 0;
85
+ const requiredLabelHeight = labelFontSize * 2.5 + 2 * SANKEY_LABEL_BLOCK_PADDING; // Assuming 2.5 lines per label
86
+ const maxLayer = max(nodes, d => d.layer);
87
+ const zeroLayerNodes = nodes.filter(d => d.layer === 0);
88
+ const maxLayerNodes = nodes.filter(d => d.layer === maxLayer);
89
+ const layerSpacing = this._getLayerSpacing(nodes);
90
+ let left = 0;
91
+ const fallbackLabelMaxWidth = Math.min(this._width / 6, layerSpacing);
92
+ const labelMaxWidth = config.labelMaxWidth || fallbackLabelMaxWidth;
93
+ const labelHorizontalPadding = 2 * SANKEY_LABEL_SPACING + 2 * SANKEY_LABEL_BLOCK_PADDING;
94
+ const hasLabelsOnTheLeft = zeroLayerNodes.some(d => getLabelOrientation(d, sankeyProbeSize, config.labelPosition) === Position.Left);
95
+ if (hasLabelsOnTheLeft) {
96
+ const maxLeftLabelWidth = max(zeroLayerNodes, d => estimateRequiredLabelWidth(d, config, labelFontSize, subLabelFontSize));
97
+ left = min([labelMaxWidth, maxLeftLabelWidth]) + labelHorizontalPadding;
98
+ }
99
+ let right = 0;
100
+ const hasLabelsOnTheRight = maxLayerNodes.some(d => getLabelOrientation(d, sankeyProbeSize, config.labelPosition) === Position.Right);
101
+ if (hasLabelsOnTheRight) {
102
+ const maxRightLabelWidth = max(maxLayerNodes, d => estimateRequiredLabelWidth(d, config, labelFontSize, subLabelFontSize));
103
+ right = min([labelMaxWidth, maxRightLabelWidth]) + labelHorizontalPadding;
104
+ }
105
+ const top = config.labelVerticalAlign === VerticalAlign.Top ? 0
106
+ : config.labelVerticalAlign === VerticalAlign.Bottom ? requiredLabelHeight
107
+ : requiredLabelHeight / 3;
108
+ const bottom = config.labelVerticalAlign === VerticalAlign.Top ? requiredLabelHeight
109
+ : config.labelVerticalAlign === VerticalAlign.Bottom ? 0
110
+ : requiredLabelHeight / 3;
111
+ const nodeSelectionRectBleed = config.selectedNodeIds ? 1 + NODE_SELECTION_RECT_DELTA : 0;
112
+ bleed = {
113
+ top: nodeSelectionRectBleed + top,
114
+ bottom: nodeSelectionRectBleed + bottom,
115
+ left: left + (hasLabelsOnTheLeft ? 0 : nodeSelectionRectBleed),
116
+ right: right + (hasLabelsOnTheRight ? 0 : nodeSelectionRectBleed),
117
+ };
73
118
  }
74
- const top = config.labelVerticalAlign === VerticalAlign.Top ? 0
75
- : config.labelVerticalAlign === VerticalAlign.Bottom ? labelSize.height
76
- : labelSize.height / 2;
77
- const bottom = config.labelVerticalAlign === VerticalAlign.Top ? labelSize.height
78
- : config.labelVerticalAlign === VerticalAlign.Bottom ? 0
79
- : labelSize.height / 2;
80
- return { top, bottom, left, right };
119
+ // Cache bleed for onZoom
120
+ this._bleedCached = bleed;
121
+ return bleed;
81
122
  }
82
123
  setData(data) {
83
124
  super.setData(data);
84
- // Pre-collapse nodes based on disabledField
85
- this._applyInitialCollapseState();
86
125
  // Pre-calculate component size for Sizing.EXTEND
87
126
  if ((this.sizing !== Sizing.Fit) || !this._hasLinks())
88
127
  this._preCalculateComponentSize();
128
+ this._bleedCached = null;
89
129
  }
90
130
  setConfig(config) {
91
131
  super.setConfig(config);
92
- // Apply initial collapse state if disabledField is set
93
- this._applyInitialCollapseState();
132
+ // Update zoom scale from config
133
+ if (this.config.zoomScale !== undefined) {
134
+ this._zoomScale = this.config.zoomScale;
135
+ }
136
+ else if (this.prevConfig.zoomScale !== undefined) {
137
+ this._zoomScale = [1, 1];
138
+ this._pan = [0, 0];
139
+ }
140
+ // Update zoom pan from config
141
+ if (this.config.zoomPan !== undefined) {
142
+ this._pan = this.config.zoomPan;
143
+ }
144
+ else if (this.prevConfig.zoomPan !== undefined) {
145
+ this._pan = [0, 0];
146
+ }
94
147
  // Pre-calculate component size for Sizing.EXTEND
95
148
  if ((this.sizing !== Sizing.Fit) || !this._hasLinks())
96
149
  this._preCalculateComponentSize();
97
- // Using "as any" because typings are not full ("@types/d3-sankey": "^0.11.2")
98
150
  const nodeId = ((d, i) => getString(d, this.config.id, i));
99
151
  this._sankey.linkSort(this.config.linkSort);
100
152
  this._sankey
@@ -104,9 +156,21 @@ class Sankey extends ComponentCore {
104
156
  .nodeAlign(SankeyLayout[this.config.nodeAlign])
105
157
  .nodeSort(this.config.nodeSort)
106
158
  .iterations(this.config.iterations);
159
+ // Update zoom behavior if already initialized
160
+ if (this._zoomBehavior) {
161
+ this._zoomBehavior.scaleExtent(this.config.zoomExtent);
162
+ if (this.config.enableZoom)
163
+ this.g.call(this._zoomBehavior);
164
+ else
165
+ this.g.on('.zoom', null);
166
+ }
167
+ this._bleedCached = null;
107
168
  }
108
169
  _render(customDuration) {
109
- const { config, bleed, datamodel: { nodes, links } } = this;
170
+ const { config, datamodel: { nodes, links } } = this;
171
+ const wasResized = this._prevWidth !== this._width;
172
+ this._prevWidth = this._width;
173
+ const bleed = wasResized || !this._bleedCached ? this.bleed : this._bleedCached;
110
174
  const duration = isNumber(customDuration) ? customDuration : config.duration;
111
175
  if ((nodes.length === 0) ||
112
176
  (nodes.length === 1 && links.length > 0) ||
@@ -118,29 +182,162 @@ class Sankey extends ComponentCore {
118
182
  // Prepare Layout
119
183
  this._prepareLayout();
120
184
  // Links
121
- smartTransition(this._linksGroup, duration).attr('transform', `translate(${bleed.left},${bleed.top})`);
122
185
  const linkSelection = this._linksGroup.selectAll(`.${link}`)
123
- .data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : i; });
186
+ .data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : `${d.source.id}-${d.target.id}`; });
124
187
  const linkSelectionEnter = linkSelection.enter().append('g').attr('class', link);
125
188
  linkSelectionEnter.call(createLinks);
126
189
  linkSelection.merge(linkSelectionEnter).call(updateLinks, config, duration);
127
190
  linkSelection.exit().call(removeLinks);
128
191
  // Nodes
129
- smartTransition(this._nodesGroup, duration).attr('transform', `translate(${bleed.left},${bleed.top})`);
192
+ // Sort nodes by x0 for optimize label rendering performance (see `getXDistanceToNextNode` in `modules/node.ts`)
193
+ nodes.sort((a, b) => a.x0 - b.x0);
194
+ const nodeSpacing = this._getLayerSpacing(nodes);
130
195
  const nodeSelection = this._nodesGroup.selectAll(`.${nodeGroup}`)
131
196
  .data(nodes, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : i; });
132
197
  const nodeSelectionEnter = nodeSelection.enter().append('g').attr('class', nodeGroup);
133
- nodeSelectionEnter.call(createNodes, this.config, this._width, bleed);
134
- nodeSelection.merge(nodeSelectionEnter).call(updateNodes, config, this._width, bleed, this._hasLinks(), duration);
198
+ const sankeyWidth = (this.sizing === Sizing.Fit ? this._width : this._extendedWidth) * this._zoomScale[0];
199
+ nodeSelectionEnter.call(createNodes, this.config, sankeyWidth, bleed);
200
+ nodeSelection.merge(nodeSelectionEnter).call(updateNodes, config, sankeyWidth, bleed, this._hasLinks(), duration, nodeSpacing);
135
201
  nodeSelection.exit()
136
202
  .attr('class', nodeExit)
137
203
  .call(removeNodes, config, duration);
204
+ // Pan
205
+ this._applyPanTransform(duration, bleed);
138
206
  // Background
139
207
  this._backgroundRect
140
208
  .attr('width', this.getWidth())
141
209
  .attr('height', this.getHeight())
142
210
  .attr('opacity', 0);
143
211
  }
212
+ _applyPanTransform(duration, bleed) {
213
+ var _a;
214
+ const pan = (_a = this.config.zoomPan) !== null && _a !== void 0 ? _a : this._pan;
215
+ const tx = bleed.left + pan[0];
216
+ const ty = bleed.top + pan[1];
217
+ smartTransition(this._linksGroup, duration).attr('transform', `translate(${tx},${ty})`);
218
+ smartTransition(this._nodesGroup, duration).attr('transform', `translate(${tx},${ty})`);
219
+ }
220
+ _scheduleRender(duration) {
221
+ if (this._animationFrameId != null)
222
+ return;
223
+ this._animationFrameId = requestAnimationFrame(() => {
224
+ this._render(duration);
225
+ this._animationFrameId = null;
226
+ });
227
+ }
228
+ setZoomScale(horizontalScale, verticalScale, duration = this.config.duration) {
229
+ var _a, _b;
230
+ // If zoomScale is controlled by config, do nothing
231
+ if (this.config.zoomScale !== undefined)
232
+ return;
233
+ const [min, max] = this.config.zoomExtent;
234
+ if (isNumber(horizontalScale))
235
+ this._zoomScale[0] = Math.min(max, Math.max(min, horizontalScale));
236
+ if (isNumber(verticalScale))
237
+ this._zoomScale[1] = Math.min(max, Math.max(min, verticalScale));
238
+ // Sync D3's zoom transform to match our scale
239
+ // Use the geometric mean as a reasonable approximation for D3's single scale
240
+ const effectiveScale = Math.sqrt(((_a = this._zoomScale[0]) !== null && _a !== void 0 ? _a : 1) * ((_b = this._zoomScale[1]) !== null && _b !== void 0 ? _b : 1));
241
+ const currentTransform = zoomIdentity.scale(effectiveScale);
242
+ this._gNode.__zoom = currentTransform;
243
+ this._prevZoomTransform.k = effectiveScale;
244
+ this._render(duration);
245
+ }
246
+ getZoomScale() {
247
+ // If zoomPan is controlled by config, do nothing
248
+ if (this.config.zoomPan !== undefined)
249
+ return;
250
+ return [this._zoomScale[0] || 1, this._zoomScale[1] || 1];
251
+ }
252
+ setPan(x, y, duration = this.config.duration) {
253
+ this._pan = [x !== null && x !== void 0 ? x : 0, y !== null && y !== void 0 ? y : 0];
254
+ this._prevZoomTransform.x = 0;
255
+ this._prevZoomTransform.y = 0;
256
+ this._scheduleRender(duration);
257
+ }
258
+ getPan() {
259
+ return this._pan;
260
+ }
261
+ fitView(duration = this.config.duration) {
262
+ var _a, _b;
263
+ this._zoomScale = (_a = this.config.zoomScale) !== null && _a !== void 0 ? _a : [1, 1];
264
+ this._pan = (_b = this.config.zoomPan) !== null && _b !== void 0 ? _b : [0, 0];
265
+ // Sync D3 zoom transform with our scales
266
+ const effectiveScale = Math.sqrt(this._zoomScale[0] * this._zoomScale[1]);
267
+ const currentTransform = zoomIdentity.scale(effectiveScale);
268
+ this._gNode.__zoom = currentTransform;
269
+ this._prevZoomTransform.k = effectiveScale;
270
+ this._prevZoomTransform.x = 0;
271
+ this._prevZoomTransform.y = 0;
272
+ this._render(duration);
273
+ }
274
+ _onZoom(event) {
275
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
276
+ const { datamodel, config } = this;
277
+ if (config.zoomScale || config.zoomPan)
278
+ return;
279
+ const nodes = datamodel.nodes;
280
+ const transform = event.transform;
281
+ const sourceEvent = event.sourceEvent;
282
+ const zoomMode = config.zoomMode || SankeyZoomMode.XY;
283
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
284
+ // Zoom pivots
285
+ const minX = (_b = min(nodes, d => d.x0)) !== null && _b !== void 0 ? _b : 0;
286
+ const minY = (_c = min(nodes, d => d.y0)) !== null && _c !== void 0 ? _c : 0;
287
+ // Determine whether this is a zoom (wheel/pinch) or a pan (drag)
288
+ const isZoomEvent = Math.abs(transform.k - this._prevZoomTransform.k) > 1e-6;
289
+ if (isZoomEvent) { // Zoom and Pan
290
+ // Compute delta factor from transform.k.
291
+ // If Cmd (metaKey) is pressed, only change horizontal scale.
292
+ // If Alt/Option (altKey) is pressed, only change vertical scale.
293
+ const deltaK = transform.k / this._prevZoomTransform.k;
294
+ const isHorizontalOnlyKey = Boolean(sourceEvent === null || sourceEvent === void 0 ? void 0 : sourceEvent.metaKey);
295
+ const isVerticalOnlyKey = !isHorizontalOnlyKey && Boolean(sourceEvent === null || sourceEvent === void 0 ? void 0 : sourceEvent.altKey);
296
+ const isHorizontalOnly = isHorizontalOnlyKey || zoomMode === SankeyZoomMode.X;
297
+ const isVerticalOnly = isVerticalOnlyKey || zoomMode === SankeyZoomMode.Y;
298
+ // Use our scale state as the source of truth (not config, not D3's k)
299
+ const [hCurrent, vCurrent] = this._zoomScale;
300
+ const hNext = isVerticalOnly ? hCurrent : clamp(hCurrent * deltaK, config.zoomExtent[0], config.zoomExtent[1]);
301
+ const vNext = isHorizontalOnly ? vCurrent : clamp(vCurrent * deltaK, config.zoomExtent[0], config.zoomExtent[1]);
302
+ this._zoomScale = [hNext, vNext];
303
+ // Pointer-centric compensation: keep the point under cursor fixed
304
+ const pos = sourceEvent ? pointer(sourceEvent, this.g.node()) : [this._width / 2, this._height / 2];
305
+ // Invert current mapping to get layout coordinates under pointer
306
+ const panX = (_e = (_d = config.zoomPan) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : this._pan[0];
307
+ const panY = (_g = (_f = config.zoomPan) === null || _f === void 0 ? void 0 : _f[1]) !== null && _g !== void 0 ? _g : this._pan[1];
308
+ const layoutX = minX + (pos[0] - bleed.left - panX - minX) / hCurrent;
309
+ const layoutY = minY + (pos[1] - bleed.top - panY - minY) / vCurrent;
310
+ // Solve for new pan to keep pointer fixed after new scales
311
+ if (!isVerticalOnly && !isFinite((_h = config.zoomPan) === null || _h === void 0 ? void 0 : _h[0]) && zoomMode !== SankeyZoomMode.Y) {
312
+ this._pan[0] = pos[0] - bleed.left - (minX + (layoutX - minX) * hNext);
313
+ }
314
+ if (!isHorizontalOnly && !isFinite((_j = config.zoomPan) === null || _j === void 0 ? void 0 : _j[1]) && zoomMode !== SankeyZoomMode.X) {
315
+ this._pan[1] = pos[1] - bleed.top - (minY + (layoutY - minY) * vNext);
316
+ }
317
+ }
318
+ else { // Just Pan: apply translation delta directly
319
+ const dx = transform.x - this._prevZoomTransform.x;
320
+ const dy = transform.y - this._prevZoomTransform.y;
321
+ if (zoomMode !== SankeyZoomMode.Y)
322
+ this._pan[0] += dx;
323
+ if (zoomMode !== SankeyZoomMode.X)
324
+ this._pan[1] += dy;
325
+ }
326
+ // Horizontal Pan Constraint
327
+ const maxX = (_k = max(nodes, d => d.x1)) !== null && _k !== void 0 ? _k : 0;
328
+ const viewportWidth = this.getWidth() - bleed.left - bleed.right;
329
+ this._pan[0] = clamp(this._pan[0], viewportWidth - maxX, minX);
330
+ // Vertical Pan Constraint
331
+ const maxY = (_l = max(nodes, d => d.y1)) !== null && _l !== void 0 ? _l : 0;
332
+ const viewportHeight = this.getHeight() - bleed.top - bleed.bottom;
333
+ this._pan[1] = clamp(this._pan[1], viewportHeight - maxY, minY);
334
+ // Update last zoom state
335
+ this._prevZoomTransform.k = transform.k;
336
+ this._prevZoomTransform.x = transform.x;
337
+ this._prevZoomTransform.y = transform.y;
338
+ (_m = config.onZoom) === null || _m === void 0 ? void 0 : _m.call(config, this._zoomScale[0], this._zoomScale[1], this._pan[0], this._pan[1], config.zoomExtent, event);
339
+ this._scheduleRender(0);
340
+ }
144
341
  _populateLinkAndNodeValues() {
145
342
  const { config, datamodel } = this;
146
343
  const nodes = datamodel.nodes;
@@ -173,8 +370,9 @@ class Sankey extends ComponentCore {
173
370
  this._extendedWidth = Math.max(0, (config.nodeWidth + config.nodeHorizontalSpacing) * Object.keys(groupedByColumn).length - config.nodeHorizontalSpacing + bleed.left + bleed.right);
174
371
  }
175
372
  _prepareLayout() {
176
- var _a, _b;
177
- const { config, bleed, datamodel } = this;
373
+ var _a, _b, _c;
374
+ const { config, datamodel } = this;
375
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
178
376
  const isExtendedSize = this.sizing === Sizing.Extend;
179
377
  const sankeyHeight = this.sizing === Sizing.Fit ? this._height : this._extendedHeight;
180
378
  const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
@@ -201,7 +399,18 @@ class Sankey extends ComponentCore {
201
399
  node.layer = 0;
202
400
  y = node.y1 + config.nodePadding;
203
401
  }
204
- this._extendedHeightIncreased = undefined;
402
+ // Apply scaling for manual layout as well
403
+ this._applyLayoutScaling();
404
+ if (isExtendedSize) {
405
+ const height = max(nodes, d => d.y1) || 0;
406
+ const width = max(nodes, d => d.x1) || 0;
407
+ this._extendedHeightIncreased = height + bleed.top + bleed.bottom;
408
+ this._extendedWidthIncreased = width + bleed.left + bleed.right;
409
+ }
410
+ else {
411
+ this._extendedHeightIncreased = undefined;
412
+ this._extendedWidthIncreased = undefined;
413
+ }
205
414
  return;
206
415
  }
207
416
  // Calculate sankey
@@ -211,30 +420,81 @@ class Sankey extends ComponentCore {
211
420
  // Default: 1px
212
421
  // Extended size nodes that have no links: config.nodeMinHeight
213
422
  for (const node of nodes) {
214
- const singleExtendedSize = isExtendedSize && !((_a = node.sourceLinks) === null || _a === void 0 ? void 0 : _a.length) && !((_b = node.targetLinks) === null || _b === void 0 ? void 0 : _b.length);
423
+ const singleExtendedSize = isExtendedSize && !((_b = node.sourceLinks) === null || _b === void 0 ? void 0 : _b.length) && !((_c = node.targetLinks) === null || _c === void 0 ? void 0 : _c.length);
215
424
  const h = Math.max(singleExtendedSize ? config.nodeMinHeight : 1, node.y1 - node.y0);
216
425
  const y = (node.y0 + node.y1) / 2;
217
426
  node.y0 = y - h / 2;
218
427
  node.y1 = y + h / 2;
219
428
  }
429
+ // Apply layout scaling (affects spacing only, not node width/height)
430
+ this._applyLayoutScaling();
220
431
  if (isExtendedSize) {
221
- const height = max(nodes, d => d.y1);
432
+ const height = max(nodes, d => d.y1) || 0;
433
+ const width = max(nodes, d => d.x1) || 0;
222
434
  this._extendedHeightIncreased = height + bleed.top + bleed.bottom;
435
+ this._extendedWidthIncreased = width + bleed.left + bleed.right;
436
+ }
437
+ }
438
+ _applyLayoutScaling() {
439
+ var _a, _b;
440
+ const { datamodel } = this;
441
+ const nodes = datamodel.nodes;
442
+ const links = datamodel.links;
443
+ // Use our scale state as the single source of truth
444
+ const [hScale, vScale] = this._zoomScale;
445
+ if ((hScale === 1 || !isFinite(hScale)) && (vScale === 1 || !isFinite(vScale)))
446
+ return;
447
+ const minX = (_a = min(nodes, d => d.x0)) !== null && _a !== void 0 ? _a : 0;
448
+ // Preserve original node positions to realign link anchors after scaling
449
+ const prevNodePos = new Map(nodes.map(n => [n, { x0: n.x0, x1: n.x1, y0: n.y0, y1: n.y1 }]));
450
+ const prevLinkY = new Map(links.map(l => [l, { y0: l.y0, y1: l.y1 }]));
451
+ // Horizontal spacing: scale relative to leftmost x
452
+ if (isFinite(hScale) && hScale !== 1) {
453
+ for (const n of nodes) {
454
+ const nodeWidth = n.width || (n.x1 - n.x0);
455
+ const relX0 = n.x0 - minX;
456
+ n.x0 = minX + relX0 * hScale;
457
+ n.x1 = n.x0 + nodeWidth;
458
+ }
459
+ }
460
+ // Vertical spacing: scale from the topmost node of the graph
461
+ if (isFinite(vScale) && vScale !== 1) {
462
+ const minY = (_b = min(nodes, d => d.y0)) !== null && _b !== void 0 ? _b : 0;
463
+ for (const n of nodes) {
464
+ const nodeHeight = n.y1 - n.y0;
465
+ const relY0 = n.y0 - minY;
466
+ n.y0 = minY + relY0 * vScale;
467
+ n.y1 = n.y0 + nodeHeight;
468
+ }
469
+ // Re-anchor links by maintaining their offset within the source/target nodes
470
+ for (const l of links) {
471
+ const prev = prevLinkY.get(l);
472
+ const prevSrc = prevNodePos.get(l.source);
473
+ const prevTrg = prevNodePos.get(l.target);
474
+ const deltaSrc = l.source.y0 - prevSrc.y0;
475
+ const deltaTrg = l.target.y0 - prevTrg.y0;
476
+ l.y0 = prev.y0 + deltaSrc;
477
+ l.y1 = prev.y1 + deltaTrg;
478
+ }
223
479
  }
224
480
  }
225
481
  getWidth() {
226
- return this.sizing === Sizing.Fit ? this._width : (this._extendedWidth || 0);
482
+ return this.sizing === Sizing.Fit ? this._width : Math.max(this._extendedWidthIncreased || 0, this._extendedWidth || 0);
227
483
  }
228
484
  getHeight() {
229
485
  return this.sizing === Sizing.Fit ? this._height : Math.max(this._extendedHeightIncreased || 0, this._extendedHeight || 0);
230
486
  }
231
487
  getLayoutWidth() {
232
- return this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
488
+ return this.sizing === Sizing.Fit ? this._width : (this._extendedWidthIncreased || this._extendedWidth);
233
489
  }
234
490
  getLayoutHeight() {
235
491
  return this.sizing === Sizing.Fit ? this._height : (this._extendedHeightIncreased || this._extendedHeight);
236
492
  }
493
+ /** @deprecated Use getLayerXCenters instead */
237
494
  getColumnCenters() {
495
+ return this.getLayerXCenters();
496
+ }
497
+ getLayerXCenters() {
238
498
  const { datamodel } = this;
239
499
  const nodes = datamodel.nodes;
240
500
  const centers = nodes.reduce((pos, node) => {
@@ -246,6 +506,18 @@ class Sankey extends ComponentCore {
246
506
  }, []);
247
507
  return centers;
248
508
  }
509
+ getLayerYCenters() {
510
+ const { datamodel } = this;
511
+ const nodes = datamodel.nodes;
512
+ const nodesByLayer = groupBy(nodes, d => d.layer);
513
+ const layerYCenters = [];
514
+ Object.values(nodesByLayer).forEach((layerNodes, idx) => {
515
+ const minY = Math.min(...layerNodes.map(n => n.y0));
516
+ const maxY = Math.max(...layerNodes.map(n => n.y1));
517
+ layerYCenters[idx] = (minY + maxY) / 2;
518
+ });
519
+ return layerYCenters;
520
+ }
249
521
  highlightSubtree(node) {
250
522
  const { config, datamodel } = this;
251
523
  clearTimeout(this._highlightTimeoutId);
@@ -279,97 +551,50 @@ class Sankey extends ComponentCore {
279
551
  this._render(config.highlightDuration);
280
552
  }
281
553
  }
282
- /**
283
- * Collapses a node by hiding only the links directly connected to it.
284
- * All other nodes (including children and descendants) remain visible in their original positions.
285
- * Only the immediate incoming and outgoing links of the collapsed node are hidden.
286
- */
287
- collapseNode(node) {
288
- const { config } = this;
289
- // Clear any active highlights before collapsing
290
- this.disableHighlight();
291
- node._state = node._state || {};
292
- node._state.collapsed = true;
293
- this._render(config.collapseAnimationDuration);
294
- }
295
- /**
296
- * Expands a previously collapsed node by showing its directly connected links.
297
- */
298
- expandNode(node) {
299
- const { config } = this;
300
- // Clear any active highlights before expanding
301
- this.disableHighlight();
302
- node._state = node._state || {};
303
- node._state.collapsed = false;
304
- this._render(config.collapseAnimationDuration);
305
- }
306
- /**
307
- * Toggles the collapse state of a node.
308
- *
309
- * @param node The node to toggle
310
- */
311
- toggleNodeCollapse(node) {
312
- var _a;
313
- if ((_a = node._state) === null || _a === void 0 ? void 0 : _a.collapsed) {
314
- this.expandNode(node);
315
- }
316
- else {
317
- this.collapseNode(node);
318
- }
319
- }
320
554
  _hasLinks() {
321
555
  const { datamodel } = this;
322
556
  return datamodel.links.length > 0;
323
557
  }
324
- /**
325
- * Applies initial collapse state to nodes based on the disabledField configuration.
326
- * If disabledField is set (e.g., "disabled"), nodes with that field set to true
327
- * will be pre-collapsed when the component loads.
328
- */
329
- _applyInitialCollapseState() {
330
- const { config, datamodel } = this;
331
- if (!config.disabledField)
332
- return;
333
- // Check each node for the disabled field and set initial collapse state
334
- for (const node of datamodel.nodes) {
335
- const inputData = node;
336
- const isDisabled = inputData && typeof inputData === 'object' &&
337
- config.disabledField in inputData &&
338
- inputData[config.disabledField] === true;
339
- if (isDisabled) {
340
- node._state = node._state || {};
341
- node._state.collapsed = true;
342
- }
343
- }
558
+ _getLayerSpacing(nodes) {
559
+ const { config } = this;
560
+ if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length))
561
+ return 0;
562
+ const firstLayerNode = nodes.find(d => d.layer === 0);
563
+ const nextLayerNode = nodes.find(d => d.layer === firstLayerNode.layer + 1);
564
+ return nextLayerNode ? nextLayerNode.x0 - (firstLayerNode.x0 + config.nodeWidth) : this._width - firstLayerNode.x1;
344
565
  }
345
566
  _onNodeMouseOver(d, event) {
346
- onNodeMouseOver(d, select(event.currentTarget), this.config, this._width);
567
+ var _a;
568
+ const { datamodel } = this;
569
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
570
+ const nodeSelection = select(event.currentTarget);
571
+ const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
572
+ onNodeMouseOver(d, datamodel.nodes, nodeSelection, this.config, sankeyWidth, this._getLayerSpacing(this.datamodel.nodes), bleed);
347
573
  }
348
574
  _onNodeMouseOut(d, event) {
349
- onNodeMouseOut(d, select(event.currentTarget), this.config, this._width);
575
+ var _a;
576
+ const { datamodel } = this;
577
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
578
+ const nodeSelection = select(event.currentTarget);
579
+ const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
580
+ onNodeMouseOut(d, datamodel.nodes, nodeSelection, this.config, sankeyWidth, this._getLayerSpacing(this.datamodel.nodes), bleed);
350
581
  }
351
582
  _onNodeRectMouseOver(d) {
352
583
  const { config } = this;
353
584
  if (config.highlightSubtreeOnHover)
354
585
  this.highlightSubtree(d);
355
586
  }
356
- _onNodeRectMouseOut(d) {
587
+ _onNodeRectMouseOut() {
357
588
  this.disableHighlight();
358
589
  }
359
- _onLinkMouseOver(d, event) {
590
+ _onLinkMouseOver(d) {
360
591
  const { config } = this;
361
592
  if (config.highlightSubtreeOnHover)
362
593
  this.highlightSubtree(d.target);
363
594
  }
364
- _onLinkMouseOut(d, event) {
595
+ _onLinkMouseOut() {
365
596
  this.disableHighlight();
366
597
  }
367
- _onNodeClick(d, event) {
368
- const { config } = this;
369
- if (config.enableNodeCollapse) {
370
- this.toggleNodeCollapse(d);
371
- }
372
- }
373
598
  }
374
599
  Sankey.selectors = style;
375
600