@unovis/ts 1.6.2-pre.8 → 1.6.3

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 (110) 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 +46 -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/leaflet.css.js +2 -2
  51. package/components/leaflet-map/modules/map.js +2 -2
  52. package/components/leaflet-map/modules/map.js.map +1 -1
  53. package/components/leaflet-map/renderer/mapboxgl-utils.d.ts +0 -1
  54. package/components/leaflet-map/renderer/mapboxgl-utils.js +1 -2
  55. package/components/leaflet-map/renderer/mapboxgl-utils.js.map +1 -1
  56. package/components/sankey/config.d.ts +28 -2
  57. package/components/sankey/config.js +2 -2
  58. package/components/sankey/config.js.map +1 -1
  59. package/components/sankey/index.d.ts +28 -2
  60. package/components/sankey/index.js +366 -46
  61. package/components/sankey/index.js.map +1 -1
  62. package/components/sankey/modules/label.d.ts +8 -5
  63. package/components/sankey/modules/label.js +64 -32
  64. package/components/sankey/modules/label.js.map +1 -1
  65. package/components/sankey/modules/link.d.ts +1 -0
  66. package/components/sankey/modules/link.js +20 -25
  67. package/components/sankey/modules/link.js.map +1 -1
  68. package/components/sankey/modules/node.d.ts +5 -4
  69. package/components/sankey/modules/node.js +78 -28
  70. package/components/sankey/modules/node.js.map +1 -1
  71. package/components/sankey/style.d.ts +67 -1
  72. package/components/sankey/style.js +78 -77
  73. package/components/sankey/style.js.map +1 -1
  74. package/components/sankey/types.d.ts +5 -0
  75. package/components/sankey/types.js +9 -2
  76. package/components/sankey/types.js.map +1 -1
  77. package/components/stacked-bar/index.d.ts +1 -1
  78. package/components/stacked-bar/index.js +24 -14
  79. package/components/stacked-bar/index.js.map +1 -1
  80. package/components/stacked-bar/types.d.ts +6 -4
  81. package/components/treemap/config.d.ts +2 -0
  82. package/components/treemap/config.js +1 -1
  83. package/components/treemap/config.js.map +1 -1
  84. package/components/treemap/index.d.ts +6 -2
  85. package/components/treemap/index.js +97 -71
  86. package/components/treemap/index.js.map +1 -1
  87. package/components/treemap/style.d.ts +1 -0
  88. package/components/treemap/style.js +5 -1
  89. package/components/treemap/style.js.map +1 -1
  90. package/components/treemap/types.d.ts +1 -0
  91. package/containers/single-container/config.d.ts +3 -0
  92. package/containers/single-container/config.js.map +1 -1
  93. package/containers/single-container/index.js +2 -1
  94. package/containers/single-container/index.js.map +1 -1
  95. package/containers/xy-container/config.d.ts +3 -0
  96. package/containers/xy-container/config.js.map +1 -1
  97. package/containers/xy-container/index.d.ts +1 -0
  98. package/containers/xy-container/index.js +13 -9
  99. package/containers/xy-container/index.js.map +1 -1
  100. package/index.js +1 -1
  101. package/package.json +41 -6
  102. package/types.js +1 -1
  103. package/utils/misc.js +13 -2
  104. package/utils/misc.js.map +1 -1
  105. package/utils/text.d.ts +1 -1
  106. package/utils/text.js +17 -15
  107. package/utils/text.js.map +1 -1
  108. package/utils/to-px.d.ts +1 -0
  109. package/utils/to-px.js +110 -0
  110. 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),
@@ -46,50 +56,97 @@ class Sankey extends ComponentCore {
46
56
  };
47
57
  if (config)
48
58
  this.setConfig(config);
49
- 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');
50
62
  this._linksGroup = this.g.append('g').attr('class', links);
51
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);
52
72
  }
53
73
  get bleed() {
54
- var _a;
55
74
  const { config, datamodel: { nodes, links } } = this;
56
- const labelFontSize = (_a = config.labelFontSize) !== null && _a !== void 0 ? _a : getCSSVariableValueInPixels('var(--vis-sankey-label-font-size)', this.element);
57
- const labelSize = requiredLabelSpace(config.labelMaxWidth, labelFontSize);
58
- let left = 0;
59
- let right = 0;
60
- // We pre-calculate sankey layout to get information about node labels placement and calculate bleed properly
61
- // 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 };
62
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
63
81
  const sankeyProbeSize = 1000;
64
82
  this._populateLinkAndNodeValues();
65
83
  this._sankey.size([sankeyProbeSize, sankeyProbeSize]);
66
84
  this._sankey({ nodes, links });
67
- const maxDepth = max(nodes, d => d.depth);
68
- const zeroDepthNodes = nodes.filter(d => d.depth === 0);
69
- const maxDepthNodes = nodes.filter(d => d.depth === maxDepth);
70
- left = zeroDepthNodes.some(d => getLabelOrientation(d, sankeyProbeSize, config.labelPosition) === Position.Left) ? labelSize.width : 0;
71
- 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
+ };
72
118
  }
73
- const top = config.labelVerticalAlign === VerticalAlign.Top ? 0
74
- : config.labelVerticalAlign === VerticalAlign.Bottom ? labelSize.height
75
- : labelSize.height / 2;
76
- const bottom = config.labelVerticalAlign === VerticalAlign.Top ? labelSize.height
77
- : config.labelVerticalAlign === VerticalAlign.Bottom ? 0
78
- : labelSize.height / 2;
79
- return { top, bottom, left, right };
119
+ // Cache bleed for onZoom
120
+ this._bleedCached = bleed;
121
+ return bleed;
80
122
  }
81
123
  setData(data) {
82
124
  super.setData(data);
83
125
  // Pre-calculate component size for Sizing.EXTEND
84
126
  if ((this.sizing !== Sizing.Fit) || !this._hasLinks())
85
127
  this._preCalculateComponentSize();
128
+ this._bleedCached = null;
86
129
  }
87
130
  setConfig(config) {
88
131
  super.setConfig(config);
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
+ }
89
147
  // Pre-calculate component size for Sizing.EXTEND
90
148
  if ((this.sizing !== Sizing.Fit) || !this._hasLinks())
91
149
  this._preCalculateComponentSize();
92
- // Using "as any" because typings are not full ("@types/d3-sankey": "^0.11.2")
93
150
  const nodeId = ((d, i) => getString(d, this.config.id, i));
94
151
  this._sankey.linkSort(this.config.linkSort);
95
152
  this._sankey
@@ -99,9 +156,22 @@ class Sankey extends ComponentCore {
99
156
  .nodeAlign(SankeyLayout[this.config.nodeAlign])
100
157
  .nodeSort(this.config.nodeSort)
101
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;
102
168
  }
103
169
  _render(customDuration) {
104
- const { config, bleed, datamodel: { nodes, links } } = this;
170
+ var _a;
171
+ const { config, datamodel: { nodes, links } } = this;
172
+ const wasResized = this._prevWidth !== this._width;
173
+ this._prevWidth = this._width;
174
+ const bleed = wasResized || !this._bleedCached ? this.bleed : this._bleedCached;
105
175
  const duration = isNumber(customDuration) ? customDuration : config.duration;
106
176
  if ((nodes.length === 0) ||
107
177
  (nodes.length === 1 && links.length > 0) ||
@@ -112,30 +182,183 @@ class Sankey extends ComponentCore {
112
182
  }
113
183
  // Prepare Layout
114
184
  this._prepareLayout();
185
+ (_a = config.onLayoutCalculated) === null || _a === void 0 ? void 0 : _a.call(config, nodes, links, this.getSankeyDepth(), this.getWidth(), this.getHeight(), bleed);
115
186
  // Links
116
- smartTransition(this._linksGroup, duration).attr('transform', `translate(${bleed.left},${bleed.top})`);
117
187
  const linkSelection = this._linksGroup.selectAll(`.${link}`)
118
- .data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : i; });
188
+ .data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : `${d.source.id}-${d.target.id}`; });
119
189
  const linkSelectionEnter = linkSelection.enter().append('g').attr('class', link);
120
190
  linkSelectionEnter.call(createLinks);
121
191
  linkSelection.merge(linkSelectionEnter).call(updateLinks, config, duration);
122
192
  linkSelection.exit().call(removeLinks);
123
193
  // Nodes
124
- smartTransition(this._nodesGroup, duration).attr('transform', `translate(${bleed.left},${bleed.top})`);
194
+ // Sort nodes by x0 for optimize label rendering performance (see `getXDistanceToNextNode` in `modules/node.ts`)
195
+ nodes.sort((a, b) => a.x0 - b.x0);
196
+ const nodeSpacing = this._getLayerSpacing(nodes);
125
197
  const nodeSelection = this._nodesGroup.selectAll(`.${nodeGroup}`)
126
198
  .data(nodes, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : i; });
127
199
  const nodeSelectionEnter = nodeSelection.enter().append('g').attr('class', nodeGroup);
128
- nodeSelectionEnter.call(createNodes, this.config, this._width, bleed);
129
- nodeSelection.merge(nodeSelectionEnter).call(updateNodes, config, this._width, bleed, this._hasLinks(), duration);
200
+ const sankeyWidth = (this.sizing === Sizing.Fit ? this._width : this._extendedWidth) * this._zoomScale[0];
201
+ nodeSelectionEnter.call(createNodes, this.config, sankeyWidth, bleed);
202
+ nodeSelection.merge(nodeSelectionEnter).call(updateNodes, config, sankeyWidth, bleed, this._hasLinks(), duration, nodeSpacing);
130
203
  nodeSelection.exit()
131
204
  .attr('class', nodeExit)
132
205
  .call(removeNodes, config, duration);
206
+ // Pan
207
+ this._applyPanTransform(duration, bleed);
133
208
  // Background
134
209
  this._backgroundRect
135
210
  .attr('width', this.getWidth())
136
211
  .attr('height', this.getHeight())
137
212
  .attr('opacity', 0);
138
213
  }
214
+ _applyPanTransform(duration, bleed) {
215
+ var _a;
216
+ const pan = (_a = this.config.zoomPan) !== null && _a !== void 0 ? _a : this._pan;
217
+ const tx = bleed.left + pan[0];
218
+ const ty = bleed.top + pan[1];
219
+ smartTransition(this._linksGroup, duration).attr('transform', `translate(${tx},${ty})`);
220
+ smartTransition(this._nodesGroup, duration).attr('transform', `translate(${tx},${ty})`);
221
+ }
222
+ _scheduleRender(duration) {
223
+ if (this._animationFrameId != null)
224
+ return;
225
+ this._animationFrameId = requestAnimationFrame(() => {
226
+ this._render(duration);
227
+ this._animationFrameId = null;
228
+ });
229
+ }
230
+ _getConstrainedPan(currentPan, zoomScaleChange = [1, 1]) {
231
+ var _a, _b, _c, _d, _e;
232
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
233
+ const nodes = this.datamodel.nodes;
234
+ // We use zoomScaleChange to adjust the max values of the viewport
235
+ // because the values are scaled (mutated) during the render
236
+ // but here they are not mutated yet
237
+ const minX = zoomScaleChange[0] * ((_b = min(nodes, d => d.x0)) !== null && _b !== void 0 ? _b : 0);
238
+ const minY = zoomScaleChange[1] * ((_c = min(nodes, d => d.y0)) !== null && _c !== void 0 ? _c : 0);
239
+ const maxX = zoomScaleChange[0] * ((_d = max(nodes, d => d.x1)) !== null && _d !== void 0 ? _d : 0);
240
+ const maxY = zoomScaleChange[1] * ((_e = max(nodes, d => d.y1)) !== null && _e !== void 0 ? _e : 0);
241
+ const viewportWidth = this.getWidth() - bleed.left - bleed.right;
242
+ const viewportHeight = this.getHeight() - bleed.top - bleed.bottom;
243
+ const constrainedX = clamp(currentPan[0], viewportWidth - maxX, minX);
244
+ const constrainedY = clamp(currentPan[1], viewportHeight - maxY, minY);
245
+ return [constrainedX, constrainedY];
246
+ }
247
+ setZoomScale(horizontalScale, verticalScale, duration = this.config.duration) {
248
+ var _a, _b;
249
+ // If zoomScale is controlled by config, do nothing
250
+ if (this.config.zoomScale !== undefined)
251
+ return;
252
+ const zoomScaleChange = [horizontalScale / this._zoomScale[0], verticalScale / this._zoomScale[1]];
253
+ const [extMin, extMax] = this.config.zoomExtent;
254
+ if (isNumber(horizontalScale))
255
+ this._zoomScale[0] = Math.min(extMax, Math.max(extMin, horizontalScale));
256
+ if (isNumber(verticalScale))
257
+ this._zoomScale[1] = Math.min(extMax, Math.max(extMin, verticalScale));
258
+ // Sync D3's zoom transform to match our scale
259
+ // Use the geometric mean as a reasonable approximation for D3's single scale
260
+ const effectiveScale = Math.sqrt(((_a = this._zoomScale[0]) !== null && _a !== void 0 ? _a : 1) * ((_b = this._zoomScale[1]) !== null && _b !== void 0 ? _b : 1));
261
+ const currentTransform = zoomIdentity.scale(effectiveScale);
262
+ this._gNode.__zoom = currentTransform;
263
+ this._prevZoomTransform.k = effectiveScale;
264
+ // Constrain pan
265
+ this._pan = this._getConstrainedPan(this._pan, zoomScaleChange);
266
+ // Reset transform values to avoid jumping when panning
267
+ this._prevZoomTransform.x = 0;
268
+ this._prevZoomTransform.y = 0;
269
+ this._render(duration);
270
+ }
271
+ getZoomScale() {
272
+ // If zoomPan is controlled by config, do nothing
273
+ if (this.config.zoomPan !== undefined)
274
+ return;
275
+ return [this._zoomScale[0] || 1, this._zoomScale[1] || 1];
276
+ }
277
+ setPan(x, y, duration = this.config.duration, shouldConstraint = false) {
278
+ const pan = [x !== null && x !== void 0 ? x : 0, y !== null && y !== void 0 ? y : 0];
279
+ this._pan = shouldConstraint ? this._getConstrainedPan(pan) : pan;
280
+ // Reset transform values to avoid jumping when panning
281
+ this._prevZoomTransform.x = 0;
282
+ this._prevZoomTransform.y = 0;
283
+ this._scheduleRender(duration);
284
+ }
285
+ getPan() {
286
+ return this._pan;
287
+ }
288
+ fitView(duration = this.config.duration) {
289
+ var _a, _b;
290
+ this._zoomScale = (_a = this.config.zoomScale) !== null && _a !== void 0 ? _a : [1, 1];
291
+ this._pan = (_b = this.config.zoomPan) !== null && _b !== void 0 ? _b : [0, 0];
292
+ // Sync D3 zoom transform with our scales
293
+ const effectiveScale = Math.sqrt(this._zoomScale[0] * this._zoomScale[1]);
294
+ const currentTransform = zoomIdentity.scale(effectiveScale);
295
+ this._gNode.__zoom = currentTransform;
296
+ this._prevZoomTransform.k = effectiveScale;
297
+ this._prevZoomTransform.x = 0;
298
+ this._prevZoomTransform.y = 0;
299
+ this._render(duration);
300
+ }
301
+ _onZoom(event) {
302
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
303
+ const { datamodel, config } = this;
304
+ if (config.zoomScale || config.zoomPan)
305
+ return;
306
+ const nodes = datamodel.nodes;
307
+ const transform = event.transform;
308
+ const sourceEvent = event.sourceEvent;
309
+ const zoomMode = config.zoomMode || SankeyZoomMode.XY;
310
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
311
+ // Zoom pivots
312
+ const minX = (_b = min(nodes, d => d.x0)) !== null && _b !== void 0 ? _b : 0;
313
+ const minY = (_c = min(nodes, d => d.y0)) !== null && _c !== void 0 ? _c : 0;
314
+ // Determine whether this is a zoom (wheel/pinch) or a pan (drag)
315
+ const isZoomEvent = Math.abs(transform.k - this._prevZoomTransform.k) > 1e-6;
316
+ if (isZoomEvent) { // Zoom and Pan
317
+ // Compute delta factor from transform.k.
318
+ // If Cmd (metaKey) is pressed, only change horizontal scale.
319
+ // If Alt/Option (altKey) is pressed, only change vertical scale.
320
+ const deltaK = transform.k / this._prevZoomTransform.k;
321
+ const isHorizontalOnlyKey = Boolean(sourceEvent === null || sourceEvent === void 0 ? void 0 : sourceEvent.metaKey);
322
+ const isVerticalOnlyKey = !isHorizontalOnlyKey && Boolean(sourceEvent === null || sourceEvent === void 0 ? void 0 : sourceEvent.altKey);
323
+ const isHorizontalOnly = isHorizontalOnlyKey || zoomMode === SankeyZoomMode.X;
324
+ const isVerticalOnly = isVerticalOnlyKey || zoomMode === SankeyZoomMode.Y;
325
+ // Use our scale state as the source of truth (not config, not D3's k)
326
+ const [hCurrent, vCurrent] = this._zoomScale;
327
+ const hNext = isVerticalOnly ? hCurrent : clamp(hCurrent * deltaK, config.zoomExtent[0], config.zoomExtent[1]);
328
+ const vNext = isHorizontalOnly ? vCurrent : clamp(vCurrent * deltaK, config.zoomExtent[0], config.zoomExtent[1]);
329
+ this._zoomScale = [hNext, vNext];
330
+ // Pointer-centric compensation: keep the point under cursor fixed
331
+ const pos = sourceEvent ? pointer(sourceEvent, this.g.node()) : [this._width / 2, this._height / 2];
332
+ // Invert current mapping to get layout coordinates under pointer
333
+ const panX = (_e = (_d = config.zoomPan) === null || _d === void 0 ? void 0 : _d[0]) !== null && _e !== void 0 ? _e : this._pan[0];
334
+ const panY = (_g = (_f = config.zoomPan) === null || _f === void 0 ? void 0 : _f[1]) !== null && _g !== void 0 ? _g : this._pan[1];
335
+ const layoutX = minX + (pos[0] - bleed.left - panX - minX) / hCurrent;
336
+ const layoutY = minY + (pos[1] - bleed.top - panY - minY) / vCurrent;
337
+ // Solve for new pan to keep pointer fixed after new scales
338
+ if (!isVerticalOnly && !isFinite((_h = config.zoomPan) === null || _h === void 0 ? void 0 : _h[0]) && zoomMode !== SankeyZoomMode.Y) {
339
+ this._pan[0] = pos[0] - bleed.left - (minX + (layoutX - minX) * hNext);
340
+ }
341
+ if (!isHorizontalOnly && !isFinite((_j = config.zoomPan) === null || _j === void 0 ? void 0 : _j[1]) && zoomMode !== SankeyZoomMode.X) {
342
+ this._pan[1] = pos[1] - bleed.top - (minY + (layoutY - minY) * vNext);
343
+ }
344
+ }
345
+ else { // Just Pan: apply translation delta directly
346
+ const dx = transform.x - this._prevZoomTransform.x;
347
+ const dy = transform.y - this._prevZoomTransform.y;
348
+ if (zoomMode !== SankeyZoomMode.Y)
349
+ this._pan[0] += dx;
350
+ if (zoomMode !== SankeyZoomMode.X)
351
+ this._pan[1] += dy;
352
+ }
353
+ // Pan constraints
354
+ this._pan = this._getConstrainedPan(this._pan);
355
+ // Update last zoom state
356
+ this._prevZoomTransform.k = transform.k;
357
+ this._prevZoomTransform.x = transform.x;
358
+ this._prevZoomTransform.y = transform.y;
359
+ (_k = config.onZoom) === null || _k === void 0 ? void 0 : _k.call(config, this._zoomScale[0], this._zoomScale[1], this._pan[0], this._pan[1], config.zoomExtent, event);
360
+ this._scheduleRender(0);
361
+ }
139
362
  _populateLinkAndNodeValues() {
140
363
  const { config, datamodel } = this;
141
364
  const nodes = datamodel.nodes;
@@ -168,8 +391,9 @@ class Sankey extends ComponentCore {
168
391
  this._extendedWidth = Math.max(0, (config.nodeWidth + config.nodeHorizontalSpacing) * Object.keys(groupedByColumn).length - config.nodeHorizontalSpacing + bleed.left + bleed.right);
169
392
  }
170
393
  _prepareLayout() {
171
- var _a, _b;
172
- const { config, bleed, datamodel } = this;
394
+ var _a, _b, _c;
395
+ const { config, datamodel } = this;
396
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
173
397
  const isExtendedSize = this.sizing === Sizing.Extend;
174
398
  const sankeyHeight = this.sizing === Sizing.Fit ? this._height : this._extendedHeight;
175
399
  const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
@@ -196,7 +420,18 @@ class Sankey extends ComponentCore {
196
420
  node.layer = 0;
197
421
  y = node.y1 + config.nodePadding;
198
422
  }
199
- this._extendedHeightIncreased = undefined;
423
+ // Apply scaling for manual layout as well
424
+ this._applyLayoutScaling();
425
+ if (isExtendedSize) {
426
+ const height = max(nodes, d => d.y1) || 0;
427
+ const width = max(nodes, d => d.x1) || 0;
428
+ this._extendedHeightIncreased = height + bleed.top + bleed.bottom;
429
+ this._extendedWidthIncreased = width + bleed.left + bleed.right;
430
+ }
431
+ else {
432
+ this._extendedHeightIncreased = undefined;
433
+ this._extendedWidthIncreased = undefined;
434
+ }
200
435
  return;
201
436
  }
202
437
  // Calculate sankey
@@ -206,30 +441,85 @@ class Sankey extends ComponentCore {
206
441
  // Default: 1px
207
442
  // Extended size nodes that have no links: config.nodeMinHeight
208
443
  for (const node of nodes) {
209
- 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);
444
+ 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);
210
445
  const h = Math.max(singleExtendedSize ? config.nodeMinHeight : 1, node.y1 - node.y0);
211
446
  const y = (node.y0 + node.y1) / 2;
212
447
  node.y0 = y - h / 2;
213
448
  node.y1 = y + h / 2;
214
449
  }
450
+ // Apply layout scaling (affects spacing only, not node width/height)
451
+ this._applyLayoutScaling();
215
452
  if (isExtendedSize) {
216
- const height = max(nodes, d => d.y1);
453
+ const height = max(nodes, d => d.y1) || 0;
454
+ const width = max(nodes, d => d.x1) || 0;
217
455
  this._extendedHeightIncreased = height + bleed.top + bleed.bottom;
456
+ this._extendedWidthIncreased = width + bleed.left + bleed.right;
457
+ }
458
+ }
459
+ _applyLayoutScaling() {
460
+ var _a, _b;
461
+ const { datamodel } = this;
462
+ const nodes = datamodel.nodes;
463
+ const links = datamodel.links;
464
+ // Use our scale state as the single source of truth
465
+ const [hScale, vScale] = this._zoomScale;
466
+ if ((hScale === 1 || !isFinite(hScale)) && (vScale === 1 || !isFinite(vScale)))
467
+ return;
468
+ const minX = (_a = min(nodes, d => d.x0)) !== null && _a !== void 0 ? _a : 0;
469
+ // Preserve original node positions to realign link anchors after scaling
470
+ const prevNodePos = new Map(nodes.map(n => [n, { x0: n.x0, x1: n.x1, y0: n.y0, y1: n.y1 }]));
471
+ const prevLinkY = new Map(links.map(l => [l, { y0: l.y0, y1: l.y1 }]));
472
+ // Horizontal spacing: scale relative to leftmost x
473
+ if (isFinite(hScale) && hScale !== 1) {
474
+ for (const n of nodes) {
475
+ const nodeWidth = n.width || (n.x1 - n.x0);
476
+ const relX0 = n.x0 - minX;
477
+ n.x0 = minX + relX0 * hScale;
478
+ n.x1 = n.x0 + nodeWidth;
479
+ }
480
+ }
481
+ // Vertical spacing: scale from the topmost node of the graph
482
+ if (isFinite(vScale) && vScale !== 1) {
483
+ const minY = (_b = min(nodes, d => d.y0)) !== null && _b !== void 0 ? _b : 0;
484
+ for (const n of nodes) {
485
+ const nodeHeight = n.y1 - n.y0;
486
+ const relY0 = n.y0 - minY;
487
+ n.y0 = minY + relY0 * vScale;
488
+ n.y1 = n.y0 + nodeHeight;
489
+ }
490
+ // Re-anchor links by maintaining their offset within the source/target nodes
491
+ for (const l of links) {
492
+ const prev = prevLinkY.get(l);
493
+ const prevSrc = prevNodePos.get(l.source);
494
+ const prevTrg = prevNodePos.get(l.target);
495
+ const deltaSrc = l.source.y0 - prevSrc.y0;
496
+ const deltaTrg = l.target.y0 - prevTrg.y0;
497
+ l.y0 = prev.y0 + deltaSrc;
498
+ l.y1 = prev.y1 + deltaTrg;
499
+ }
218
500
  }
219
501
  }
220
502
  getWidth() {
221
- return this.sizing === Sizing.Fit ? this._width : (this._extendedWidth || 0);
503
+ return this.sizing === Sizing.Fit ? this._width : Math.max(this._extendedWidthIncreased || 0, this._extendedWidth || 0);
222
504
  }
223
505
  getHeight() {
224
506
  return this.sizing === Sizing.Fit ? this._height : Math.max(this._extendedHeightIncreased || 0, this._extendedHeight || 0);
225
507
  }
226
508
  getLayoutWidth() {
227
- return this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
509
+ return this.sizing === Sizing.Fit ? this._width : (this._extendedWidthIncreased || this._extendedWidth);
228
510
  }
229
511
  getLayoutHeight() {
230
512
  return this.sizing === Sizing.Fit ? this._height : (this._extendedHeightIncreased || this._extendedHeight);
231
513
  }
514
+ getSankeyDepth() {
515
+ const { datamodel } = this;
516
+ return max(datamodel.nodes, d => d.layer);
517
+ }
518
+ /** @deprecated Use getLayerXCenters instead */
232
519
  getColumnCenters() {
520
+ return this.getLayerXCenters();
521
+ }
522
+ getLayerXCenters() {
233
523
  const { datamodel } = this;
234
524
  const nodes = datamodel.nodes;
235
525
  const centers = nodes.reduce((pos, node) => {
@@ -241,6 +531,18 @@ class Sankey extends ComponentCore {
241
531
  }, []);
242
532
  return centers;
243
533
  }
534
+ getLayerYCenters() {
535
+ const { datamodel } = this;
536
+ const nodes = datamodel.nodes;
537
+ const nodesByLayer = groupBy(nodes, d => d.layer);
538
+ const layerYCenters = [];
539
+ Object.values(nodesByLayer).forEach((layerNodes, idx) => {
540
+ const minY = Math.min(...layerNodes.map(n => n.y0));
541
+ const maxY = Math.max(...layerNodes.map(n => n.y1));
542
+ layerYCenters[idx] = (minY + maxY) / 2;
543
+ });
544
+ return layerYCenters;
545
+ }
244
546
  highlightSubtree(node) {
245
547
  const { config, datamodel } = this;
246
548
  clearTimeout(this._highlightTimeoutId);
@@ -278,26 +580,44 @@ class Sankey extends ComponentCore {
278
580
  const { datamodel } = this;
279
581
  return datamodel.links.length > 0;
280
582
  }
583
+ _getLayerSpacing(nodes) {
584
+ const { config } = this;
585
+ if (!(nodes === null || nodes === void 0 ? void 0 : nodes.length))
586
+ return 0;
587
+ const firstLayerNode = nodes.find(d => d.layer === 0);
588
+ const nextLayerNode = nodes.find(d => d.layer === firstLayerNode.layer + 1);
589
+ return nextLayerNode ? nextLayerNode.x0 - (firstLayerNode.x0 + config.nodeWidth) : this._width - firstLayerNode.x1;
590
+ }
281
591
  _onNodeMouseOver(d, event) {
282
- onNodeMouseOver(d, select(event.currentTarget), this.config, this._width);
592
+ var _a;
593
+ const { datamodel } = this;
594
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
595
+ const nodeSelection = select(event.currentTarget);
596
+ const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
597
+ onNodeMouseOver(d, datamodel.nodes, nodeSelection, this.config, sankeyWidth, this._getLayerSpacing(this.datamodel.nodes), bleed);
283
598
  }
284
599
  _onNodeMouseOut(d, event) {
285
- onNodeMouseOut(d, select(event.currentTarget), this.config, this._width);
600
+ var _a;
601
+ const { datamodel } = this;
602
+ const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
603
+ const nodeSelection = select(event.currentTarget);
604
+ const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
605
+ onNodeMouseOut(d, datamodel.nodes, nodeSelection, this.config, sankeyWidth, this._getLayerSpacing(this.datamodel.nodes), bleed);
286
606
  }
287
607
  _onNodeRectMouseOver(d) {
288
608
  const { config } = this;
289
609
  if (config.highlightSubtreeOnHover)
290
610
  this.highlightSubtree(d);
291
611
  }
292
- _onNodeRectMouseOut(d) {
612
+ _onNodeRectMouseOut() {
293
613
  this.disableHighlight();
294
614
  }
295
- _onLinkMouseOver(d, event) {
615
+ _onLinkMouseOver(d) {
296
616
  const { config } = this;
297
617
  if (config.highlightSubtreeOnHover)
298
618
  this.highlightSubtree(d.target);
299
619
  }
300
- _onLinkMouseOut(d, event) {
620
+ _onLinkMouseOut() {
301
621
  this.disableHighlight();
302
622
  }
303
623
  }