@progress/kendo-charts 2.3.0-dev.202402161236 → 2.3.0-dev.202403071434
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/dist/cdn/js/kendo-charts.js +1 -1
- package/dist/cdn/main.js +1 -1
- package/dist/es/sankey/calculation.js +252 -29
- package/dist/es/sankey/link.js +4 -4
- package/dist/es/sankey/sankey.js +121 -49
- package/dist/es/sankey/title.js +0 -1
- package/dist/es2015/sankey/calculation.js +235 -27
- package/dist/es2015/sankey/link.js +3 -3
- package/dist/es2015/sankey/sankey.js +103 -30
- package/dist/es2015/sankey/title.js +0 -1
- package/dist/npm/main.js +374 -81
- package/dist/npm/sankey.d.ts +30 -0
- package/dist/systemjs/kendo-charts.js +1 -1
- package/package.json +2 -1
|
@@ -3,24 +3,39 @@ import { deepExtend } from '../common';
|
|
|
3
3
|
const max = (array, mapFn) => Math.max.apply(null, array.map(mapFn));
|
|
4
4
|
const min = (array, mapFn) => Math.min.apply(null, array.map(mapFn));
|
|
5
5
|
const sum = (array, mapFn) => array.map(mapFn).reduce((acc, curr) => (acc + curr), 0);
|
|
6
|
-
const sortAsc = (a, b) => a.y0 - b.y0;
|
|
6
|
+
const sortAsc = (a, b) => (a.y0 === b.y0 ? a.index - b.index : a.y0 + a.y1 - b.y0 - b.y1);
|
|
7
7
|
const sortSource = (a, b) => sortAsc(a.source, b.source);
|
|
8
8
|
const sortTarget = (a, b) => sortAsc(a.target, b.target);
|
|
9
9
|
const value = (node) => node.value;
|
|
10
10
|
|
|
11
11
|
function sortLinks(nodes) {
|
|
12
|
-
nodes.forEach(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
nodes.forEach(node => {
|
|
13
|
+
node.targetLinks.forEach(link => {
|
|
14
|
+
link.source.sourceLinks.sort(sortTarget);
|
|
15
|
+
});
|
|
16
|
+
node.sourceLinks.forEach(link => {
|
|
17
|
+
link.target.targetLinks.sort(sortSource);
|
|
18
|
+
});
|
|
16
19
|
});
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
const calcLayer = (node, maxDepth) => {
|
|
23
|
+
if (node.align === 'left') {
|
|
24
|
+
return node.depth;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.align === 'right') {
|
|
28
|
+
return maxDepth - node.height;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return node.sourceLinks.length ? node.depth : maxDepth;
|
|
32
|
+
};
|
|
33
|
+
|
|
19
34
|
class Sankey {
|
|
20
35
|
constructor(options) {
|
|
21
|
-
const offset = options.nodesOptions
|
|
36
|
+
const { offset = {}, align } = options.nodesOptions;
|
|
22
37
|
this.data = {
|
|
23
|
-
nodes: options.nodes.map((node) => deepExtend({}, { offset }, node)),
|
|
38
|
+
nodes: options.nodes.map((node) => deepExtend({}, { offset, align }, node)),
|
|
24
39
|
links: options.links.map((link) => deepExtend({}, link))
|
|
25
40
|
};
|
|
26
41
|
|
|
@@ -30,6 +45,10 @@ class Sankey {
|
|
|
30
45
|
this.offsetY = options.offsetY || 0;
|
|
31
46
|
this.nodeWidth = options.nodesOptions.width;
|
|
32
47
|
this.nodePadding = options.nodesOptions.padding;
|
|
48
|
+
this.reverse = options.reverse;
|
|
49
|
+
this.targetColumnIndex = options.targetColumnIndex;
|
|
50
|
+
this.loops = options.loops;
|
|
51
|
+
this.autoLayout = options.autoLayout;
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
calculate() {
|
|
@@ -37,19 +56,21 @@ class Sankey {
|
|
|
37
56
|
this.connectLinksToNodes(nodes, links);
|
|
38
57
|
this.calculateNodeValues(nodes);
|
|
39
58
|
this.calculateNodeDepths(nodes);
|
|
59
|
+
this.calculateNodeHeights(nodes);
|
|
40
60
|
|
|
41
61
|
const columns = this.calculateNodeColumns(nodes);
|
|
42
62
|
this.calculateNodeBreadths(columns);
|
|
43
63
|
this.applyNodesOffset(nodes);
|
|
44
64
|
this.calculateLinkBreadths(nodes);
|
|
45
65
|
|
|
46
|
-
return this.data;
|
|
66
|
+
return Object.assign({}, this.data, {columns});
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
connectLinksToNodes(nodes, links) {
|
|
50
70
|
const nodesMap = new Map();
|
|
51
71
|
|
|
52
|
-
nodes.forEach((node) => {
|
|
72
|
+
nodes.forEach((node, i) => {
|
|
73
|
+
node.index = i;
|
|
53
74
|
node.sourceLinks = [];
|
|
54
75
|
node.targetLinks = [];
|
|
55
76
|
node.id = node.id !== undefined ? node.id : node.label.text;
|
|
@@ -93,17 +114,36 @@ class Sankey {
|
|
|
93
114
|
}
|
|
94
115
|
}
|
|
95
116
|
|
|
117
|
+
calculateNodeHeights(nodes) {
|
|
118
|
+
let current = new Set(nodes);
|
|
119
|
+
let next = new Set;
|
|
120
|
+
let x = 0;
|
|
121
|
+
const eachNode = (node) => {
|
|
122
|
+
node.height = x;
|
|
123
|
+
node.targetLinks.forEach((link) => {
|
|
124
|
+
next.add(link.source);
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
while (current.size) {
|
|
128
|
+
current.forEach(eachNode);
|
|
129
|
+
x++;
|
|
130
|
+
current = next;
|
|
131
|
+
next = new Set;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
calculateNodeColumns(nodes) {
|
|
97
136
|
const maxDepth = max(nodes, (d) => d.depth);
|
|
98
137
|
const columnWidth = (this.width - this.offsetX - this.nodeWidth) / maxDepth;
|
|
99
138
|
const columns = new Array(maxDepth + 1);
|
|
100
139
|
for (let i = 0; i < nodes.length; i++) {
|
|
101
140
|
const node = nodes[i];
|
|
102
|
-
const
|
|
103
|
-
node.x0 = this.offsetX +
|
|
141
|
+
const layer = Math.max(0, Math.min(maxDepth, calcLayer(node, maxDepth)));
|
|
142
|
+
node.x0 = this.offsetX + layer * columnWidth;
|
|
104
143
|
node.x1 = node.x0 + this.nodeWidth;
|
|
105
|
-
|
|
106
|
-
columns[
|
|
144
|
+
node.layer = layer;
|
|
145
|
+
columns[layer] = columns[layer] || [];
|
|
146
|
+
columns[layer].push(node);
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
return columns;
|
|
@@ -112,31 +152,39 @@ class Sankey {
|
|
|
112
152
|
calculateNodeBreadths(columns) {
|
|
113
153
|
const kSize = min(columns, (c) => (this.height - this.offsetY - (c.length - 1) * this.nodePadding) / sum(c, value));
|
|
114
154
|
|
|
115
|
-
|
|
116
|
-
const nodes = columns[c];
|
|
155
|
+
columns.forEach(nodes => {
|
|
117
156
|
let y = this.offsetY;
|
|
118
|
-
|
|
119
|
-
const node = nodes[i];
|
|
157
|
+
nodes.forEach((node) => {
|
|
120
158
|
node.y0 = y;
|
|
121
159
|
node.y1 = y + node.value * kSize;
|
|
122
160
|
y = node.y1 + this.nodePadding;
|
|
123
|
-
|
|
124
|
-
const link = node.sourceLinks[l];
|
|
161
|
+
node.sourceLinks.forEach((link) => {
|
|
125
162
|
link.width = link.value * kSize;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
163
|
+
});
|
|
164
|
+
});
|
|
129
165
|
y = (this.height - y + this.nodePadding) / (nodes.length + 1);
|
|
130
|
-
|
|
131
|
-
const node = nodes[i];
|
|
166
|
+
nodes.forEach((node, i) => {
|
|
132
167
|
node.y0 += y * (i + 1);
|
|
133
168
|
node.y1 += y * (i + 1);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (this.autoLayout !== false) {
|
|
173
|
+
const loops = this.loops !== undefined ? this.loops : columns.length - 1;
|
|
174
|
+
const targetColumnIndex = this.targetColumnIndex || 1;
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < loops; i++) {
|
|
177
|
+
if (!this.reverse) {
|
|
178
|
+
this.uncurlLinksToLeft(columns, targetColumnIndex);
|
|
179
|
+
this.uncurlLinksToRight(columns, targetColumnIndex);
|
|
180
|
+
} else {
|
|
181
|
+
this.uncurlLinksToRight(columns, targetColumnIndex);
|
|
182
|
+
this.uncurlLinksToLeft(columns, targetColumnIndex);
|
|
183
|
+
}
|
|
134
184
|
}
|
|
135
185
|
}
|
|
136
186
|
|
|
137
|
-
|
|
138
|
-
sortLinks(columns[c]);
|
|
139
|
-
}
|
|
187
|
+
columns.forEach(sortLinks);
|
|
140
188
|
}
|
|
141
189
|
|
|
142
190
|
applyNodesOffset(nodes) {
|
|
@@ -156,15 +204,175 @@ class Sankey {
|
|
|
156
204
|
let y = node.y0;
|
|
157
205
|
let y1 = y;
|
|
158
206
|
sourceLinks.forEach((link) => {
|
|
207
|
+
link.x0 = link.source.x1;
|
|
159
208
|
link.y0 = y + link.width / 2;
|
|
160
209
|
y += link.width;
|
|
161
210
|
});
|
|
162
211
|
targetLinks.forEach((link) => {
|
|
212
|
+
link.x1 = link.target.x0;
|
|
163
213
|
link.y1 = y1 + link.width / 2;
|
|
164
214
|
y1 += link.width;
|
|
165
215
|
});
|
|
166
216
|
});
|
|
167
217
|
}
|
|
218
|
+
|
|
219
|
+
uncurlLinksToRight(columns, targetColumnIndex) {
|
|
220
|
+
const n = columns.length;
|
|
221
|
+
for (let i = targetColumnIndex; i < n; i++) {
|
|
222
|
+
const column = columns[i];
|
|
223
|
+
column.forEach((target) => {
|
|
224
|
+
let y = 0;
|
|
225
|
+
let sum = 0;
|
|
226
|
+
target.targetLinks.forEach((link) => {
|
|
227
|
+
let kValue = link.value * (target.layer - link.source.layer);
|
|
228
|
+
y += this.targetTopPos(link.source, target) * kValue;
|
|
229
|
+
sum += kValue;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let dy = y === 0 ? 0 : (y / sum - target.y0);
|
|
233
|
+
target.y0 += dy;
|
|
234
|
+
target.y1 += dy;
|
|
235
|
+
sortLinks([target]);
|
|
236
|
+
});
|
|
237
|
+
column.sort(sortAsc);
|
|
238
|
+
this.arrangeNodesVertically(column);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
uncurlLinksToLeft(columns, targetColumnIndex) {
|
|
243
|
+
const l = columns.length;
|
|
244
|
+
const startIndex = l - 1 - targetColumnIndex;
|
|
245
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
246
|
+
const column = columns[i];
|
|
247
|
+
for (let j = 0; j < column.length; j++) {
|
|
248
|
+
const source = column[j];
|
|
249
|
+
let y = 0;
|
|
250
|
+
let sum = 0;
|
|
251
|
+
source.sourceLinks.forEach((link) => {
|
|
252
|
+
let kValue = link.value * (link.target.layer - source.layer);
|
|
253
|
+
y += this.sourceTopPos(source, link.target) * kValue;
|
|
254
|
+
sum += kValue;
|
|
255
|
+
});
|
|
256
|
+
let dy = y === 0 ? 0 : (y / sum - source.y0);
|
|
257
|
+
source.y0 += dy;
|
|
258
|
+
source.y1 += dy;
|
|
259
|
+
sortLinks([source]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
column.sort(sortAsc);
|
|
263
|
+
this.arrangeNodesVertically(column);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
arrangeNodesVertically(nodes) {
|
|
268
|
+
const startIndex = 0;
|
|
269
|
+
const endIndex = nodes.length - 1;
|
|
270
|
+
|
|
271
|
+
this.arrangeUp(nodes, this.height, endIndex);
|
|
272
|
+
this.arrangeDown(nodes, this.offsetY, startIndex);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
arrangeDown(nodes, yPos, index) {
|
|
276
|
+
let currentY = yPos;
|
|
277
|
+
|
|
278
|
+
for (let i = index; i < nodes.length; i++) {
|
|
279
|
+
const node = nodes[i];
|
|
280
|
+
const dy = Math.max(0, currentY - node.y0);
|
|
281
|
+
node.y0 += dy;
|
|
282
|
+
node.y1 += dy;
|
|
283
|
+
currentY = node.y1 + this.nodePadding;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
arrangeUp(nodes, yPos, index) {
|
|
288
|
+
let currentY = yPos;
|
|
289
|
+
for (let i = index; i >= 0; --i) {
|
|
290
|
+
const node = nodes[i];
|
|
291
|
+
const dy = Math.max(0, node.y1 - currentY);
|
|
292
|
+
node.y0 -= dy;
|
|
293
|
+
node.y1 -= dy;
|
|
294
|
+
currentY = node.y0 - this.nodePadding;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
sourceTopPos(source, target) {
|
|
299
|
+
let y = target.y0 - ((target.targetLinks.length - 1) * this.nodePadding) / 2;
|
|
300
|
+
for (let i = 0; i < target.targetLinks.length; i++) {
|
|
301
|
+
const link = target.targetLinks[i];
|
|
302
|
+
if (link.source === source) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
y += link.width + this.nodePadding;
|
|
306
|
+
}
|
|
307
|
+
for (let i = 0; i < source.sourceLinks.length; i++) {
|
|
308
|
+
const link = source.sourceLinks[i];
|
|
309
|
+
if (link.target === target) {
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
y -= link.width;
|
|
313
|
+
}
|
|
314
|
+
return y;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
targetTopPos(source, target) {
|
|
318
|
+
let y = source.y0 - ((source.sourceLinks.length - 1) * this.nodePadding) / 2;
|
|
319
|
+
for (let i = 0; i < source.sourceLinks.length; i++) {
|
|
320
|
+
const link = source.sourceLinks[i];
|
|
321
|
+
if (link.target === target) {
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
y += link.width + this.nodePadding;
|
|
325
|
+
}
|
|
326
|
+
for (let i = 0; i < target.targetLinks.length; i++) {
|
|
327
|
+
const link = target.targetLinks[i];
|
|
328
|
+
if (link.source === source) {
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
y -= link.width;
|
|
332
|
+
}
|
|
333
|
+
return y;
|
|
334
|
+
}
|
|
168
335
|
}
|
|
169
336
|
|
|
170
337
|
export const calculateSankey = (options) => new Sankey(options).calculate();
|
|
338
|
+
|
|
339
|
+
export const crossesValue = (links) => {
|
|
340
|
+
let value = 0;
|
|
341
|
+
const linksLength = links.length;
|
|
342
|
+
|
|
343
|
+
for (let i = 0; i < linksLength; i++) {
|
|
344
|
+
const link = links[i];
|
|
345
|
+
|
|
346
|
+
for (let lNext = i + 1; lNext < linksLength; lNext++) {
|
|
347
|
+
const nextLink = links[lNext];
|
|
348
|
+
|
|
349
|
+
if (intersect(link, nextLink)) {
|
|
350
|
+
value += Math.min(link.value, nextLink.value);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return value;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
function rotationDirection(p1x, p1y, p2x, p2y, p3x, p3y) {
|
|
359
|
+
const expression1 = (p3y - p1y) * (p2x - p1x);
|
|
360
|
+
const expression2 = (p2y - p1y) * (p3x - p1x);
|
|
361
|
+
|
|
362
|
+
if (expression1 > expression2) {
|
|
363
|
+
return 1;
|
|
364
|
+
} else if (expression1 === expression2) {
|
|
365
|
+
return 0;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return -1;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function intersect(link1, link2) {
|
|
372
|
+
const f1 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x1, link2.y1);
|
|
373
|
+
const f2 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x0, link2.y0);
|
|
374
|
+
const f3 = rotationDirection(link1.x0, link1.y0, link2.x0, link2.y0, link2.x1, link2.y1);
|
|
375
|
+
const f4 = rotationDirection(link1.x1, link1.y1, link2.x0, link2.y0, link2.x1, link2.y1);
|
|
376
|
+
|
|
377
|
+
return f1 !== f2 && f3 !== f4;
|
|
378
|
+
}
|
|
@@ -6,11 +6,11 @@ import { defined } from '../drawing-utils';
|
|
|
6
6
|
export class Link extends SankeyElement {
|
|
7
7
|
getElement() {
|
|
8
8
|
const link = this.options.link;
|
|
9
|
-
const {
|
|
10
|
-
const xC = (
|
|
9
|
+
const { x0, x1, y0, y1 } = link;
|
|
10
|
+
const xC = (x0 + x1) / 2;
|
|
11
11
|
|
|
12
12
|
return new drawing.Path(this.visualOptions())
|
|
13
|
-
.moveTo(
|
|
13
|
+
.moveTo(x0, y0).curveTo([xC, y0], [xC, y1], [x1, y1]);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
visualOptions() {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { drawing } from '@progress/kendo-drawing';
|
|
1
|
+
import { geometry, drawing } from '@progress/kendo-drawing';
|
|
2
2
|
import { deepExtend, addClass, setDefaultOptions } from '../common';
|
|
3
|
-
import { calculateSankey } from './calculation';
|
|
3
|
+
import { calculateSankey, crossesValue } from './calculation';
|
|
4
4
|
import { Node, resolveNodeOptions } from './node';
|
|
5
5
|
import { Link, resolveLinkOptions } from './link';
|
|
6
6
|
import { Label, resolveLabelOptions } from './label';
|
|
@@ -10,6 +10,7 @@ import Box from '../core/box';
|
|
|
10
10
|
import rectToBox from '../core/utils/rect-to-box';
|
|
11
11
|
import { Observable } from '../common/observable';
|
|
12
12
|
import { Legend } from './legend';
|
|
13
|
+
import { defined } from '../drawing-utils';
|
|
13
14
|
|
|
14
15
|
const LINK = 'link';
|
|
15
16
|
const NODE = 'node';
|
|
@@ -59,6 +60,7 @@ export class Sankey extends Observable {
|
|
|
59
60
|
}
|
|
60
61
|
this.size = { width, height };
|
|
61
62
|
this.surface.setSize(this.size);
|
|
63
|
+
this.resize = true;
|
|
62
64
|
this._redraw();
|
|
63
65
|
});
|
|
64
66
|
});
|
|
@@ -242,11 +244,11 @@ export class Sankey extends Observable {
|
|
|
242
244
|
return legendVisual.chartElement.box;
|
|
243
245
|
}
|
|
244
246
|
|
|
245
|
-
calculateSankey(
|
|
246
|
-
const { title, legend, data } =
|
|
247
|
-
const
|
|
247
|
+
calculateSankey(calcOptions, sankeyOptions) {
|
|
248
|
+
const { title, legend, data, nodes, labels, nodesColors, disableAutoLayout } = sankeyOptions;
|
|
249
|
+
const autoLayout = !disableAutoLayout;
|
|
248
250
|
|
|
249
|
-
const sankeyBox = new Box(0, 0,
|
|
251
|
+
const sankeyBox = new Box(0, 0, calcOptions.width, calcOptions.height);
|
|
250
252
|
const titleBox = this.titleBox(title, sankeyBox);
|
|
251
253
|
|
|
252
254
|
let legendArea = sankeyBox.clone();
|
|
@@ -255,34 +257,35 @@ export class Sankey extends Observable {
|
|
|
255
257
|
const titleHeight = titleBox.height();
|
|
256
258
|
if (title.position === TOP) {
|
|
257
259
|
sankeyBox.unpad({ top: titleHeight });
|
|
258
|
-
legendArea = new Box(0, titleHeight,
|
|
260
|
+
legendArea = new Box(0, titleHeight, calcOptions.width, calcOptions.height);
|
|
259
261
|
} else {
|
|
260
262
|
sankeyBox.shrink(0, titleHeight);
|
|
261
|
-
legendArea = new Box(0, 0,
|
|
263
|
+
legendArea = new Box(0, 0, calcOptions.width, calcOptions.height - titleHeight);
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
265
267
|
const legendBox = this.legendBox(legend, data.nodes, legendArea);
|
|
268
|
+
const legendPosition = (legend && legend.position) || Legend.prototype.options.position;
|
|
266
269
|
|
|
267
270
|
if (legendBox) {
|
|
268
|
-
if (
|
|
271
|
+
if (legendPosition === LEFT) {
|
|
269
272
|
sankeyBox.unpad({ left: legendBox.width() });
|
|
270
273
|
}
|
|
271
274
|
|
|
272
|
-
if (
|
|
275
|
+
if (legendPosition === RIGHT) {
|
|
273
276
|
sankeyBox.shrink(legendBox.width(), 0);
|
|
274
277
|
}
|
|
275
278
|
|
|
276
|
-
if (
|
|
279
|
+
if (legendPosition === TOP) {
|
|
277
280
|
sankeyBox.unpad({ top: legendBox.height() });
|
|
278
281
|
}
|
|
279
282
|
|
|
280
|
-
if (
|
|
283
|
+
if (legendPosition === BOTTOM) {
|
|
281
284
|
sankeyBox.shrink(0, legendBox.height());
|
|
282
285
|
}
|
|
283
286
|
}
|
|
284
287
|
|
|
285
|
-
const calculatedNodes = calculateSankey(Object.assign({},
|
|
288
|
+
const calculatedNodes = calculateSankey(Object.assign({}, calcOptions, {offsetX: 0, offsetY: 0, width: sankeyBox.width(), height: sankeyBox.height()})).nodes;
|
|
286
289
|
const box = new Box();
|
|
287
290
|
|
|
288
291
|
calculatedNodes.forEach((nodeEl, i) => {
|
|
@@ -290,34 +293,90 @@ export class Sankey extends Observable {
|
|
|
290
293
|
const nodeInstance = new Node(nodeOps);
|
|
291
294
|
box.wrap(rectToBox(nodeInstance.exportVisual().rawBBox()));
|
|
292
295
|
|
|
293
|
-
const labelInstance = new Label(
|
|
296
|
+
const labelInstance = new Label(resolveLabelOptions(nodeEl, labels, sankeyBox.width()));
|
|
294
297
|
const labelVisual = labelInstance.exportVisual();
|
|
295
298
|
if (labelVisual) {
|
|
296
299
|
box.wrap(rectToBox(labelVisual.rawBBox()));
|
|
297
300
|
}
|
|
298
301
|
});
|
|
299
302
|
|
|
300
|
-
let offsetX =
|
|
301
|
-
let offsetY =
|
|
303
|
+
let offsetX = sankeyBox.x1;
|
|
304
|
+
let offsetY = sankeyBox.y1;
|
|
302
305
|
|
|
303
|
-
let width =
|
|
304
|
-
let height =
|
|
306
|
+
let width = sankeyBox.width() + offsetX;
|
|
307
|
+
let height = sankeyBox.height() + offsetY;
|
|
308
|
+
|
|
309
|
+
width -= box.x2 > sankeyBox.width() ? box.x2 - sankeyBox.width() : 0;
|
|
310
|
+
height -= box.y2 > sankeyBox.height() ? box.y2 - sankeyBox.height() : 0;
|
|
311
|
+
|
|
312
|
+
offsetX += box.x1 < 0 ? -box.x1 : 0;
|
|
313
|
+
offsetY += box.y1 < 0 ? -box.y1 : 0;
|
|
314
|
+
|
|
315
|
+
if (autoLayout === false) {
|
|
316
|
+
return {
|
|
317
|
+
sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})),
|
|
318
|
+
legendBox,
|
|
319
|
+
titleBox
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (this.resize && autoLayout && this.permutation) {
|
|
324
|
+
this.resize = false;
|
|
325
|
+
return {
|
|
326
|
+
sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation)),
|
|
327
|
+
legendBox,
|
|
328
|
+
titleBox
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const startColumn = 1;
|
|
333
|
+
const loops = 2;
|
|
334
|
+
const columnsLength = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})).columns.length;
|
|
335
|
+
const results = [];
|
|
336
|
+
|
|
337
|
+
const permutation = (targetColumnIndex, reverse) => {
|
|
338
|
+
let currPerm = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, loops: loops, targetColumnIndex, reverse}));
|
|
339
|
+
let crosses = crossesValue(currPerm.links);
|
|
340
|
+
results.push({
|
|
341
|
+
crosses: crosses,
|
|
342
|
+
reverse: reverse,
|
|
343
|
+
targetColumnIndex: targetColumnIndex
|
|
344
|
+
});
|
|
345
|
+
return crosses === 0;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
for (let index = startColumn; index <= columnsLength - 1; index++) {
|
|
349
|
+
if (permutation(index, false) || permutation(index, true)) {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const minCrosses = Math.min.apply(null, results.map(r => r.crosses));
|
|
355
|
+
const bestResult = results.find(r => r.crosses === minCrosses);
|
|
356
|
+
this.permutation = { targetColumnIndex: bestResult.targetColumnIndex, reverse: bestResult.reverse };
|
|
357
|
+
const result = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation));
|
|
305
358
|
|
|
306
359
|
return {
|
|
307
|
-
sankey:
|
|
360
|
+
sankey: result,
|
|
308
361
|
legendBox,
|
|
309
362
|
titleBox
|
|
310
363
|
};
|
|
311
364
|
}
|
|
312
365
|
|
|
313
|
-
_render() {
|
|
314
|
-
const
|
|
315
|
-
const
|
|
366
|
+
_render(options, context) {
|
|
367
|
+
const sankeyOptions = options || this.options;
|
|
368
|
+
const sankeyContext = context || this;
|
|
369
|
+
|
|
370
|
+
const { data, labels: labelOptions, nodes: nodesOptions, links: linkOptions, nodesColors, title, legend } = sankeyOptions;
|
|
371
|
+
const { width, height } = sankeyContext.size;
|
|
372
|
+
|
|
316
373
|
const calcOptions = Object.assign({}, data, {width, height, nodesOptions, title, legend});
|
|
317
|
-
const { sankey, titleBox, legendBox } = this.calculateSankey(calcOptions);
|
|
374
|
+
const { sankey, titleBox, legendBox } = this.calculateSankey(calcOptions, sankeyOptions);
|
|
318
375
|
const { nodes, links } = sankey;
|
|
319
376
|
|
|
320
|
-
const visual = new drawing.Group(
|
|
377
|
+
const visual = new drawing.Group({
|
|
378
|
+
clip: drawing.Path.fromRect(new geometry.Rect([0, 0], [width, height]))
|
|
379
|
+
});
|
|
321
380
|
|
|
322
381
|
if (titleBox) {
|
|
323
382
|
const titleElement = new Title(Object.assign({}, title, {drawingRect: titleBox}));
|
|
@@ -332,6 +391,7 @@ export class Sankey extends Observable {
|
|
|
332
391
|
}
|
|
333
392
|
|
|
334
393
|
const visualNodes = new Map();
|
|
394
|
+
sankeyContext.nodesVisuals = visualNodes;
|
|
335
395
|
|
|
336
396
|
nodes.forEach((node, i) => {
|
|
337
397
|
const nodeOps = resolveNodeOptions(node, nodesOptions, nodesColors, i);
|
|
@@ -354,6 +414,7 @@ export class Sankey extends Observable {
|
|
|
354
414
|
const sortedLinks = links.slice().sort((a, b) => b.value - a.value);
|
|
355
415
|
|
|
356
416
|
const linksVisuals = [];
|
|
417
|
+
sankeyContext.linksVisuals = linksVisuals;
|
|
357
418
|
|
|
358
419
|
sortedLinks.forEach(link => {
|
|
359
420
|
const { source, target } = link;
|
|
@@ -377,11 +438,9 @@ export class Sankey extends Observable {
|
|
|
377
438
|
visual.append(linkVisual);
|
|
378
439
|
});
|
|
379
440
|
|
|
380
|
-
|
|
381
|
-
this.nodesVisuals = visualNodes;
|
|
382
|
-
|
|
441
|
+
const diagramWidth = nodes.reduce((acc, node) => Math.max(acc, node.x1), 0);
|
|
383
442
|
nodes.forEach((node) => {
|
|
384
|
-
const textOps = resolveLabelOptions(node, labelOptions,
|
|
443
|
+
const textOps = resolveLabelOptions(node, labelOptions, diagramWidth);
|
|
385
444
|
const labelInstance = new Label(textOps);
|
|
386
445
|
const labelVisual = labelInstance.exportVisual();
|
|
387
446
|
|
|
@@ -393,8 +452,18 @@ export class Sankey extends Observable {
|
|
|
393
452
|
return visual;
|
|
394
453
|
}
|
|
395
454
|
|
|
396
|
-
exportVisual() {
|
|
397
|
-
|
|
455
|
+
exportVisual(exportOptions) {
|
|
456
|
+
const options = (exportOptions && exportOptions.options) ?
|
|
457
|
+
deepExtend({}, this.options, exportOptions.options) : this.options;
|
|
458
|
+
|
|
459
|
+
const context = {
|
|
460
|
+
size: {
|
|
461
|
+
width: defined(exportOptions && exportOptions.width) ? exportOptions.width : this.size.width,
|
|
462
|
+
height: defined(exportOptions && exportOptions.height) ? exportOptions.height : this.size.height
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
return this._render(options, context);
|
|
398
467
|
}
|
|
399
468
|
|
|
400
469
|
_setOptions(options) {
|
|
@@ -403,6 +472,9 @@ export class Sankey extends Observable {
|
|
|
403
472
|
}
|
|
404
473
|
|
|
405
474
|
setDefaultOptions(Sankey, {
|
|
475
|
+
title: {
|
|
476
|
+
position: TOP, // 'top', 'bottom'
|
|
477
|
+
},
|
|
406
478
|
labels: {
|
|
407
479
|
visible: true,
|
|
408
480
|
margin: {
|
|
@@ -421,6 +493,7 @@ setDefaultOptions(Sankey, {
|
|
|
421
493
|
width: 24,
|
|
422
494
|
padding: 16,
|
|
423
495
|
opacity: 1,
|
|
496
|
+
align: 'stretch', // 'left', 'right', 'stretch'
|
|
424
497
|
offset: { left: 0, top: 0 }
|
|
425
498
|
},
|
|
426
499
|
links: {
|