@unovis/ts 1.6.2 → 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.
- package/components/annotations/config.d.ts +2 -0
- package/components/annotations/config.js +1 -1
- package/components/annotations/config.js.map +1 -1
- package/components/annotations/index.d.ts +1 -0
- package/components/annotations/index.js +25 -10
- package/components/annotations/index.js.map +1 -1
- package/components/annotations/style.d.ts +2 -0
- package/components/annotations/style.js +8 -1
- package/components/annotations/style.js.map +1 -1
- package/components/area/config.d.ts +11 -1
- package/components/area/config.js +1 -1
- package/components/area/config.js.map +1 -1
- package/components/area/index.d.ts +6 -0
- package/components/area/index.js +80 -7
- package/components/area/index.js.map +1 -1
- package/components/area/style.d.ts +1 -0
- package/components/area/style.js +7 -1
- package/components/area/style.js.map +1 -1
- package/components/axis/index.d.ts +2 -0
- package/components/axis/index.js +46 -7
- package/components/axis/index.js.map +1 -1
- package/components/bullet-legend/index.d.ts +2 -0
- package/components/bullet-legend/index.js +9 -5
- package/components/bullet-legend/index.js.map +1 -1
- package/components/bullet-legend/modules/shape.js +3 -2
- package/components/bullet-legend/modules/shape.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 +3 -2
- package/components/crosshair/index.js.map +1 -1
- package/components/flow-legend/config.d.ts +10 -0
- package/components/flow-legend/config.js +4 -0
- package/components/flow-legend/config.js.map +1 -1
- package/components/flow-legend/index.d.ts +6 -2
- package/components/flow-legend/index.js +34 -16
- package/components/flow-legend/index.js.map +1 -1
- package/components/flow-legend/style.d.ts +3 -3
- package/components/flow-legend/style.js +30 -26
- package/components/flow-legend/style.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 +2 -2
- package/components/graph/modules/link/index.js.map +1 -1
- package/components/graph/modules/node/index.js +2 -1
- package/components/graph/modules/node/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 +28 -2
- package/components/sankey/config.js +2 -2
- package/components/sankey/config.js.map +1 -1
- package/components/sankey/index.d.ts +28 -2
- package/components/sankey/index.js +366 -46
- package/components/sankey/index.js.map +1 -1
- package/components/sankey/modules/label.d.ts +8 -5
- package/components/sankey/modules/label.js +64 -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 +78 -28
- package/components/sankey/modules/node.js.map +1 -1
- package/components/sankey/style.d.ts +67 -1
- package/components/sankey/style.js +78 -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/stacked-bar/index.d.ts +1 -1
- package/components/stacked-bar/index.js +24 -14
- package/components/stacked-bar/index.js.map +1 -1
- package/components/stacked-bar/types.d.ts +6 -4
- package/components/treemap/config.d.ts +2 -0
- package/components/treemap/config.js +1 -1
- package/components/treemap/config.js.map +1 -1
- package/components/treemap/index.d.ts +6 -2
- package/components/treemap/index.js +97 -71
- package/components/treemap/index.js.map +1 -1
- package/components/treemap/style.d.ts +1 -0
- package/components/treemap/style.js +5 -1
- package/components/treemap/style.js.map +1 -1
- package/components/treemap/types.d.ts +1 -0
- 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 +41 -6
- package/types.js +1 -1
- package/utils/misc.js +13 -2
- package/utils/misc.js.map +1 -1
- package/utils/text.d.ts +1 -1
- package/utils/text.js +17 -15
- package/utils/text.js.map +1 -1
- package/utils/to-px.d.ts +1 -0
- package/utils/to-px.js +110 -0
- 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 {
|
|
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,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
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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,
|
|
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
|
-
|
|
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 && !((
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
612
|
+
_onNodeRectMouseOut() {
|
|
293
613
|
this.disableHighlight();
|
|
294
614
|
}
|
|
295
|
-
_onLinkMouseOver(d
|
|
615
|
+
_onLinkMouseOver(d) {
|
|
296
616
|
const { config } = this;
|
|
297
617
|
if (config.highlightSubtreeOnHover)
|
|
298
618
|
this.highlightSubtree(d.target);
|
|
299
619
|
}
|
|
300
|
-
_onLinkMouseOut(
|
|
620
|
+
_onLinkMouseOut() {
|
|
301
621
|
this.disableHighlight();
|
|
302
622
|
}
|
|
303
623
|
}
|