@unovis/ts 1.6.2 → 1.7.0-pre.0
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/components/area/config.d.ts +2 -0
- package/components/area/config.js +1 -1
- package/components/area/config.js.map +1 -1
- package/components/area/index.js +6 -3
- package/components/area/index.js.map +1 -1
- package/components/crosshair/config.d.ts +1 -1
- package/components/crosshair/config.js.map +1 -1
- package/components/crosshair/index.d.ts +1 -1
- package/components/crosshair/index.js.map +1 -1
- package/components/free-brush/types.js +1 -0
- package/components/free-brush/types.js.map +1 -1
- package/components/graph/index.d.ts +1 -0
- package/components/graph/index.js +35 -14
- package/components/graph/index.js.map +1 -1
- package/components/graph/modules/link/index.js +1 -1
- package/components/graph/modules/link/index.js.map +1 -1
- package/components/leaflet-map/leaflet.css.js +2 -2
- package/components/leaflet-map/modules/map.js +2 -2
- package/components/leaflet-map/modules/map.js.map +1 -1
- package/components/leaflet-map/renderer/mapboxgl-utils.d.ts +0 -1
- package/components/leaflet-map/renderer/mapboxgl-utils.js +1 -2
- package/components/leaflet-map/renderer/mapboxgl-utils.js.map +1 -1
- package/components/sankey/config.d.ts +25 -2
- package/components/sankey/config.js +2 -2
- package/components/sankey/config.js.map +1 -1
- package/components/sankey/index.d.ts +26 -2
- package/components/sankey/index.js +341 -46
- package/components/sankey/index.js.map +1 -1
- package/components/sankey/modules/label.d.ts +8 -5
- package/components/sankey/modules/label.js +70 -32
- package/components/sankey/modules/label.js.map +1 -1
- package/components/sankey/modules/link.d.ts +1 -0
- package/components/sankey/modules/link.js +20 -25
- package/components/sankey/modules/link.js.map +1 -1
- package/components/sankey/modules/node.d.ts +5 -4
- package/components/sankey/modules/node.js +65 -16
- package/components/sankey/modules/node.js.map +1 -1
- package/components/sankey/style.d.ts +67 -1
- package/components/sankey/style.js +77 -77
- package/components/sankey/style.js.map +1 -1
- package/components/sankey/types.d.ts +5 -0
- package/components/sankey/types.js +9 -2
- package/components/sankey/types.js.map +1 -1
- package/components/treemap/index.js +2 -4
- package/components/treemap/index.js.map +1 -1
- package/containers/single-container/config.d.ts +3 -0
- package/containers/single-container/config.js.map +1 -1
- package/containers/single-container/index.js +2 -1
- package/containers/single-container/index.js.map +1 -1
- package/containers/xy-container/config.d.ts +3 -0
- package/containers/xy-container/config.js.map +1 -1
- package/containers/xy-container/index.d.ts +1 -0
- package/containers/xy-container/index.js +13 -9
- package/containers/xy-container/index.js.map +1 -1
- package/index.js +1 -1
- package/package.json +42 -10
- package/types.js +1 -1
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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,21 @@ 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,
|
|
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;
|
|
105
174
|
const duration = isNumber(customDuration) ? customDuration : config.duration;
|
|
106
175
|
if ((nodes.length === 0) ||
|
|
107
176
|
(nodes.length === 1 && links.length > 0) ||
|
|
@@ -113,29 +182,162 @@ class Sankey extends ComponentCore {
|
|
|
113
182
|
// Prepare Layout
|
|
114
183
|
this._prepareLayout();
|
|
115
184
|
// Links
|
|
116
|
-
smartTransition(this._linksGroup, duration).attr('transform', `translate(${bleed.left},${bleed.top})`);
|
|
117
185
|
const linkSelection = this._linksGroup.selectAll(`.${link}`)
|
|
118
|
-
.data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a :
|
|
186
|
+
.data(links, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : `${d.source.id}-${d.target.id}`; });
|
|
119
187
|
const linkSelectionEnter = linkSelection.enter().append('g').attr('class', link);
|
|
120
188
|
linkSelectionEnter.call(createLinks);
|
|
121
189
|
linkSelection.merge(linkSelectionEnter).call(updateLinks, config, duration);
|
|
122
190
|
linkSelection.exit().call(removeLinks);
|
|
123
191
|
// Nodes
|
|
124
|
-
|
|
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);
|
|
125
195
|
const nodeSelection = this._nodesGroup.selectAll(`.${nodeGroup}`)
|
|
126
196
|
.data(nodes, (d, i) => { var _a; return (_a = config.id(d, i)) !== null && _a !== void 0 ? _a : i; });
|
|
127
197
|
const nodeSelectionEnter = nodeSelection.enter().append('g').attr('class', nodeGroup);
|
|
128
|
-
|
|
129
|
-
|
|
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);
|
|
130
201
|
nodeSelection.exit()
|
|
131
202
|
.attr('class', nodeExit)
|
|
132
203
|
.call(removeNodes, config, duration);
|
|
204
|
+
// Pan
|
|
205
|
+
this._applyPanTransform(duration, bleed);
|
|
133
206
|
// Background
|
|
134
207
|
this._backgroundRect
|
|
135
208
|
.attr('width', this.getWidth())
|
|
136
209
|
.attr('height', this.getHeight())
|
|
137
210
|
.attr('opacity', 0);
|
|
138
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
|
+
}
|
|
139
341
|
_populateLinkAndNodeValues() {
|
|
140
342
|
const { config, datamodel } = this;
|
|
141
343
|
const nodes = datamodel.nodes;
|
|
@@ -168,8 +370,9 @@ class Sankey extends ComponentCore {
|
|
|
168
370
|
this._extendedWidth = Math.max(0, (config.nodeWidth + config.nodeHorizontalSpacing) * Object.keys(groupedByColumn).length - config.nodeHorizontalSpacing + bleed.left + bleed.right);
|
|
169
371
|
}
|
|
170
372
|
_prepareLayout() {
|
|
171
|
-
var _a, _b;
|
|
172
|
-
const { config,
|
|
373
|
+
var _a, _b, _c;
|
|
374
|
+
const { config, datamodel } = this;
|
|
375
|
+
const bleed = (_a = this._bleedCached) !== null && _a !== void 0 ? _a : this.bleed;
|
|
173
376
|
const isExtendedSize = this.sizing === Sizing.Extend;
|
|
174
377
|
const sankeyHeight = this.sizing === Sizing.Fit ? this._height : this._extendedHeight;
|
|
175
378
|
const sankeyWidth = this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
|
|
@@ -196,7 +399,18 @@ class Sankey extends ComponentCore {
|
|
|
196
399
|
node.layer = 0;
|
|
197
400
|
y = node.y1 + config.nodePadding;
|
|
198
401
|
}
|
|
199
|
-
|
|
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
|
+
}
|
|
200
414
|
return;
|
|
201
415
|
}
|
|
202
416
|
// Calculate sankey
|
|
@@ -206,30 +420,81 @@ class Sankey extends ComponentCore {
|
|
|
206
420
|
// Default: 1px
|
|
207
421
|
// Extended size nodes that have no links: config.nodeMinHeight
|
|
208
422
|
for (const node of nodes) {
|
|
209
|
-
const singleExtendedSize = isExtendedSize && !((
|
|
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);
|
|
210
424
|
const h = Math.max(singleExtendedSize ? config.nodeMinHeight : 1, node.y1 - node.y0);
|
|
211
425
|
const y = (node.y0 + node.y1) / 2;
|
|
212
426
|
node.y0 = y - h / 2;
|
|
213
427
|
node.y1 = y + h / 2;
|
|
214
428
|
}
|
|
429
|
+
// Apply layout scaling (affects spacing only, not node width/height)
|
|
430
|
+
this._applyLayoutScaling();
|
|
215
431
|
if (isExtendedSize) {
|
|
216
|
-
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;
|
|
217
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
|
+
}
|
|
218
479
|
}
|
|
219
480
|
}
|
|
220
481
|
getWidth() {
|
|
221
|
-
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);
|
|
222
483
|
}
|
|
223
484
|
getHeight() {
|
|
224
485
|
return this.sizing === Sizing.Fit ? this._height : Math.max(this._extendedHeightIncreased || 0, this._extendedHeight || 0);
|
|
225
486
|
}
|
|
226
487
|
getLayoutWidth() {
|
|
227
|
-
return this.sizing === Sizing.Fit ? this._width : this._extendedWidth;
|
|
488
|
+
return this.sizing === Sizing.Fit ? this._width : (this._extendedWidthIncreased || this._extendedWidth);
|
|
228
489
|
}
|
|
229
490
|
getLayoutHeight() {
|
|
230
491
|
return this.sizing === Sizing.Fit ? this._height : (this._extendedHeightIncreased || this._extendedHeight);
|
|
231
492
|
}
|
|
493
|
+
/** @deprecated Use getLayerXCenters instead */
|
|
232
494
|
getColumnCenters() {
|
|
495
|
+
return this.getLayerXCenters();
|
|
496
|
+
}
|
|
497
|
+
getLayerXCenters() {
|
|
233
498
|
const { datamodel } = this;
|
|
234
499
|
const nodes = datamodel.nodes;
|
|
235
500
|
const centers = nodes.reduce((pos, node) => {
|
|
@@ -241,6 +506,18 @@ class Sankey extends ComponentCore {
|
|
|
241
506
|
}, []);
|
|
242
507
|
return centers;
|
|
243
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
|
+
}
|
|
244
521
|
highlightSubtree(node) {
|
|
245
522
|
const { config, datamodel } = this;
|
|
246
523
|
clearTimeout(this._highlightTimeoutId);
|
|
@@ -278,26 +555,44 @@ class Sankey extends ComponentCore {
|
|
|
278
555
|
const { datamodel } = this;
|
|
279
556
|
return datamodel.links.length > 0;
|
|
280
557
|
}
|
|
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;
|
|
565
|
+
}
|
|
281
566
|
_onNodeMouseOver(d, event) {
|
|
282
|
-
|
|
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);
|
|
283
573
|
}
|
|
284
574
|
_onNodeMouseOut(d, event) {
|
|
285
|
-
|
|
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);
|
|
286
581
|
}
|
|
287
582
|
_onNodeRectMouseOver(d) {
|
|
288
583
|
const { config } = this;
|
|
289
584
|
if (config.highlightSubtreeOnHover)
|
|
290
585
|
this.highlightSubtree(d);
|
|
291
586
|
}
|
|
292
|
-
_onNodeRectMouseOut(
|
|
587
|
+
_onNodeRectMouseOut() {
|
|
293
588
|
this.disableHighlight();
|
|
294
589
|
}
|
|
295
|
-
_onLinkMouseOver(d
|
|
590
|
+
_onLinkMouseOver(d) {
|
|
296
591
|
const { config } = this;
|
|
297
592
|
if (config.highlightSubtreeOnHover)
|
|
298
593
|
this.highlightSubtree(d.target);
|
|
299
594
|
}
|
|
300
|
-
_onLinkMouseOut(
|
|
595
|
+
_onLinkMouseOut() {
|
|
301
596
|
this.disableHighlight();
|
|
302
597
|
}
|
|
303
598
|
}
|