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