chartjs-chart-sankey 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/dist/chartjs-chart-sankey.esm.js +243 -185
- package/dist/chartjs-chart-sankey.js +244 -186
- package/dist/chartjs-chart-sankey.min.js +3 -3
- package/package.json +14 -13
- package/types/index.esm.d.ts +12 -3
|
@@ -1,30 +1,72 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.9.0
|
|
3
3
|
* https://github.com/kurkle/chartjs-chart-sankey#readme
|
|
4
|
-
* (c)
|
|
4
|
+
* (c) 2022 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
7
|
(function (global, factory) {
|
|
8
8
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js'), require('chart.js/helpers')) :
|
|
9
9
|
typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) :
|
|
10
10
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
|
|
11
|
-
}(this, (function (chart_js, helpers) { 'use strict';
|
|
11
|
+
})(this, (function (chart_js, helpers) { 'use strict';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* @param
|
|
15
|
-
* @
|
|
14
|
+
* @param {string | Array<string>} raw
|
|
15
|
+
* @return {Array<string>}
|
|
16
|
+
*/
|
|
17
|
+
function toTextLines(raw) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
const inputs = helpers.isArray(raw) ? raw : helpers.isNullOrUndef(raw) ? [] : [raw];
|
|
20
|
+
|
|
21
|
+
while (inputs.length) {
|
|
22
|
+
const input = inputs.pop();
|
|
23
|
+
if (typeof input === 'string') {
|
|
24
|
+
lines.unshift.apply(lines, input.split('\n'));
|
|
25
|
+
} else if (Array.isArray(input)) {
|
|
26
|
+
inputs.push.apply(inputs, input);
|
|
27
|
+
} else if (!helpers.isNullOrUndef(inputs)) {
|
|
28
|
+
lines.unshift('' + input);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {any} size
|
|
37
|
+
* @return {'min' | 'max'}
|
|
38
|
+
*/
|
|
39
|
+
function validateSizeValue(size) {
|
|
40
|
+
if (!size || ['min', 'max'].indexOf(size) === -1) {
|
|
41
|
+
return 'max';
|
|
42
|
+
}
|
|
43
|
+
return size;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param x {any}
|
|
48
|
+
* @return {boolean}
|
|
49
|
+
*/
|
|
50
|
+
const defined = x => x !== undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {Map<string, SankeyNode>} nodes
|
|
54
|
+
* @param {Array<SankeyDataPoint>} data
|
|
16
55
|
* @return {number}
|
|
17
56
|
*/
|
|
18
57
|
function calculateX(nodes, data) {
|
|
19
|
-
const to = new Set(data.map(
|
|
20
|
-
const from = new Set(data.map(
|
|
58
|
+
const to = new Set(data.map(dataPoint => dataPoint.to));
|
|
59
|
+
const from = new Set(data.map(dataPoint => dataPoint.from));
|
|
21
60
|
const keys = new Set([...nodes.keys()]);
|
|
22
61
|
let x = 0;
|
|
23
62
|
while (keys.size) {
|
|
24
63
|
const column = nextColumn([...keys], to);
|
|
25
|
-
for (
|
|
26
|
-
nodes.get(
|
|
27
|
-
|
|
64
|
+
for (const key of column) {
|
|
65
|
+
const node = nodes.get(key);
|
|
66
|
+
if (!defined(node.x)) {
|
|
67
|
+
node.x = x;
|
|
68
|
+
}
|
|
69
|
+
keys.delete(key);
|
|
28
70
|
}
|
|
29
71
|
if (keys.size) {
|
|
30
72
|
to.clear();
|
|
@@ -35,15 +77,19 @@ function calculateX(nodes, data) {
|
|
|
35
77
|
[...nodes.keys()]
|
|
36
78
|
.filter(key => !from.has(key))
|
|
37
79
|
.forEach(key => {
|
|
38
|
-
nodes.get(key)
|
|
80
|
+
const node = nodes.get(key);
|
|
81
|
+
// Only move the node to right edge, if it's column is not defined
|
|
82
|
+
if (!node.column) {
|
|
83
|
+
node.x = x;
|
|
84
|
+
}
|
|
39
85
|
});
|
|
40
86
|
|
|
41
87
|
return x;
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
/**
|
|
45
|
-
* @param
|
|
46
|
-
* @param
|
|
91
|
+
* @param {Array<string>} keys
|
|
92
|
+
* @param {Set<string>} to
|
|
47
93
|
* @return {Array<string>}
|
|
48
94
|
*/
|
|
49
95
|
function nextColumn(keys, to) {
|
|
@@ -52,74 +98,64 @@ function nextColumn(keys, to) {
|
|
|
52
98
|
}
|
|
53
99
|
|
|
54
100
|
/**
|
|
55
|
-
* @param
|
|
56
|
-
* @
|
|
57
|
-
*/
|
|
58
|
-
const defined = x => x !== undefined;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @param a {SankeyNode}
|
|
62
|
-
* @param b {SankeyNode}
|
|
101
|
+
* @param {SankeyNode} a
|
|
102
|
+
* @param {SankeyNode} b
|
|
63
103
|
* @return {number}
|
|
64
104
|
*/
|
|
65
105
|
const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
|
|
66
106
|
|
|
67
107
|
/**
|
|
68
|
-
* @param
|
|
69
|
-
* @param
|
|
108
|
+
* @param {Array<FromToElement>} list
|
|
109
|
+
* @param {string} prop
|
|
70
110
|
* @return {number}
|
|
71
111
|
*/
|
|
72
112
|
const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
|
|
73
113
|
|
|
74
114
|
/**
|
|
75
|
-
* @param
|
|
115
|
+
* @param {string} prop
|
|
76
116
|
* @return {function(FromToElement, FromToElement): number}
|
|
77
117
|
*/
|
|
78
|
-
const flowByNodeCount = (prop) => (a, b) => nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop);
|
|
118
|
+
const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
|
|
79
119
|
|
|
80
120
|
/**
|
|
81
|
-
* @param
|
|
82
|
-
* @
|
|
83
|
-
*/
|
|
84
|
-
function findLargestNode(nodeArray) {
|
|
85
|
-
return nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out))[0];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* @param node {SankeyNode}
|
|
90
|
-
* @param y {number}
|
|
121
|
+
* @param {SankeyNode} node
|
|
122
|
+
* @param {number} y
|
|
91
123
|
* @return {number}
|
|
92
124
|
*/
|
|
93
125
|
function processFrom(node, y) {
|
|
94
|
-
node.from.sort(flowByNodeCount('from'))
|
|
126
|
+
node.from.sort(flowByNodeCount('from'));
|
|
127
|
+
for (const flow of node.from) {
|
|
95
128
|
const n = flow.node;
|
|
96
129
|
if (!defined(n.y)) {
|
|
97
130
|
n.y = y;
|
|
98
|
-
|
|
131
|
+
processFrom(n, y);
|
|
99
132
|
}
|
|
100
|
-
|
|
133
|
+
y = Math.max(n.y + n.out, y);
|
|
134
|
+
}
|
|
101
135
|
return y;
|
|
102
136
|
}
|
|
103
137
|
|
|
104
138
|
/**
|
|
105
|
-
* @param
|
|
106
|
-
* @param
|
|
139
|
+
* @param {SankeyNode} node
|
|
140
|
+
* @param {number} y
|
|
107
141
|
* @return {number}
|
|
108
142
|
*/
|
|
109
143
|
function processTo(node, y) {
|
|
110
|
-
node.to.sort(flowByNodeCount('to'))
|
|
144
|
+
node.to.sort(flowByNodeCount('to'));
|
|
145
|
+
for (const flow of node.to) {
|
|
111
146
|
const n = flow.node;
|
|
112
147
|
if (!defined(n.y)) {
|
|
113
148
|
n.y = y;
|
|
114
|
-
|
|
149
|
+
processTo(n, y);
|
|
115
150
|
}
|
|
116
|
-
|
|
151
|
+
y = Math.max(n.y + n.in, y);
|
|
152
|
+
}
|
|
117
153
|
return y;
|
|
118
154
|
}
|
|
119
155
|
|
|
120
156
|
/**
|
|
121
|
-
* @param
|
|
122
|
-
* @param
|
|
157
|
+
* @param {SankeyNode} node
|
|
158
|
+
* @param {number} value
|
|
123
159
|
* @return {number}
|
|
124
160
|
*/
|
|
125
161
|
function setOrGetY(node, value) {
|
|
@@ -131,8 +167,8 @@ function setOrGetY(node, value) {
|
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
/**
|
|
134
|
-
* @param
|
|
135
|
-
* @param
|
|
170
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
171
|
+
* @param {number} maxX
|
|
136
172
|
* @return {number}
|
|
137
173
|
*/
|
|
138
174
|
function processRest(nodeArray, maxX) {
|
|
@@ -140,9 +176,11 @@ function processRest(nodeArray, maxX) {
|
|
|
140
176
|
const rightNodes = nodeArray.filter(node => node.x === maxX);
|
|
141
177
|
const leftToDo = leftNodes.filter(node => !defined(node.y));
|
|
142
178
|
const rightToDo = rightNodes.filter(node => !defined(node.y));
|
|
179
|
+
const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
|
|
143
180
|
|
|
144
181
|
let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
|
|
145
182
|
let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
|
|
183
|
+
let centerY = 0;
|
|
146
184
|
|
|
147
185
|
if (leftY >= rightY) {
|
|
148
186
|
leftToDo.forEach(node => {
|
|
@@ -165,17 +203,26 @@ function processRest(nodeArray, maxX) {
|
|
|
165
203
|
leftY = Math.max(leftY + node.out, processTo(node, leftY));
|
|
166
204
|
});
|
|
167
205
|
}
|
|
206
|
+
centerToDo.forEach(node => {
|
|
207
|
+
let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
|
|
208
|
+
.reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
|
|
209
|
+
y = setOrGetY(node, y);
|
|
210
|
+
y = Math.max(y + node.in, processFrom(node, y));
|
|
211
|
+
y = Math.max(y + node.out, processTo(node, y));
|
|
212
|
+
centerY = Math.max(centerY, y);
|
|
213
|
+
});
|
|
168
214
|
|
|
169
|
-
return Math.max(leftY, rightY);
|
|
215
|
+
return Math.max(leftY, rightY, centerY);
|
|
170
216
|
}
|
|
171
217
|
|
|
172
218
|
/**
|
|
173
|
-
* @param
|
|
174
|
-
* @param
|
|
219
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
220
|
+
* @param {number} maxX
|
|
175
221
|
* @return {number}
|
|
176
222
|
*/
|
|
177
223
|
function calculateY(nodeArray, maxX) {
|
|
178
|
-
|
|
224
|
+
nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
|
|
225
|
+
const start = nodeArray[0];
|
|
179
226
|
start.y = 0;
|
|
180
227
|
const left = processFrom(start, 0);
|
|
181
228
|
const right = processTo(start, 0);
|
|
@@ -184,15 +231,17 @@ function calculateY(nodeArray, maxX) {
|
|
|
184
231
|
}
|
|
185
232
|
|
|
186
233
|
/**
|
|
187
|
-
* @param
|
|
188
|
-
* @param
|
|
234
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
235
|
+
* @param {number} maxX
|
|
189
236
|
* @return {number}
|
|
190
237
|
*/
|
|
191
238
|
function calculateYUsingPriority(nodeArray, maxX) {
|
|
192
239
|
let maxY = 0;
|
|
240
|
+
let nextYStart = 0;
|
|
193
241
|
for (let x = 0; x <= maxX; x++) {
|
|
194
|
-
let y =
|
|
242
|
+
let y = nextYStart;
|
|
195
243
|
const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
|
|
244
|
+
nextYStart = nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0;
|
|
196
245
|
for (const node of nodes) {
|
|
197
246
|
node.y = y;
|
|
198
247
|
y += Math.max(node.out, node.in);
|
|
@@ -203,8 +252,8 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
203
252
|
}
|
|
204
253
|
|
|
205
254
|
/**
|
|
206
|
-
* @param
|
|
207
|
-
* @param
|
|
255
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
256
|
+
* @param {number} padding
|
|
208
257
|
* @return {number}
|
|
209
258
|
*/
|
|
210
259
|
function addPadding(nodeArray, padding) {
|
|
@@ -213,7 +262,8 @@ function addPadding(nodeArray, padding) {
|
|
|
213
262
|
let prev = 0;
|
|
214
263
|
let maxY = 0;
|
|
215
264
|
const rows = [];
|
|
216
|
-
nodeArray.sort(nodeByXY)
|
|
265
|
+
nodeArray.sort(nodeByXY);
|
|
266
|
+
for (const node of nodeArray) {
|
|
217
267
|
if (node.y) {
|
|
218
268
|
if (node.x === 0) {
|
|
219
269
|
rows.push(node.y);
|
|
@@ -234,49 +284,61 @@ function addPadding(nodeArray, padding) {
|
|
|
234
284
|
i++;
|
|
235
285
|
}
|
|
236
286
|
maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
|
|
237
|
-
}
|
|
238
|
-
|
|
287
|
+
}
|
|
239
288
|
return maxY;
|
|
240
289
|
}
|
|
241
290
|
|
|
242
291
|
/**
|
|
243
|
-
* @param
|
|
292
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
293
|
+
* @param {'min' | 'max'} size
|
|
244
294
|
*/
|
|
245
|
-
function sortFlows(nodeArray) {
|
|
246
|
-
nodeArray.forEach(node => {
|
|
295
|
+
function sortFlows(nodeArray, size) {
|
|
296
|
+
nodeArray.forEach((node) => {
|
|
297
|
+
const nodeSize = Math[size](node.in || node.out, node.out || node.in);
|
|
298
|
+
const overlapFrom = nodeSize < node.in;
|
|
299
|
+
const overlapTo = nodeSize < node.out;
|
|
247
300
|
let addY = 0;
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
301
|
+
let len = node.from.length;
|
|
302
|
+
node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
|
|
303
|
+
if (overlapFrom) {
|
|
304
|
+
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
305
|
+
} else {
|
|
306
|
+
flow.addY = addY;
|
|
307
|
+
addY += flow.flow;
|
|
308
|
+
}
|
|
251
309
|
});
|
|
252
310
|
addY = 0;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
311
|
+
len = node.to.length;
|
|
312
|
+
node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
|
|
313
|
+
if (overlapTo) {
|
|
314
|
+
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
315
|
+
} else {
|
|
316
|
+
flow.addY = addY;
|
|
317
|
+
addY += flow.flow;
|
|
318
|
+
}
|
|
256
319
|
});
|
|
257
320
|
});
|
|
258
321
|
}
|
|
259
322
|
|
|
260
323
|
/**
|
|
261
|
-
* @param
|
|
262
|
-
* @param
|
|
263
|
-
* @param
|
|
324
|
+
* @param {Map<string, SankeyNode>} nodes
|
|
325
|
+
* @param {Array<SankeyDataPoint>} data
|
|
326
|
+
* @param {boolean} priority
|
|
327
|
+
* @param {'min' | 'max'} size
|
|
264
328
|
* @return {{maxY: number, maxX: number}}
|
|
265
329
|
*/
|
|
266
|
-
function layout(nodes, data, priority) {
|
|
330
|
+
function layout(nodes, data, priority, size) {
|
|
267
331
|
const nodeArray = [...nodes.values()];
|
|
268
332
|
const maxX = calculateX(nodes, data);
|
|
269
333
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
270
334
|
const padding = maxY * 0.03; // rows;
|
|
271
|
-
|
|
272
335
|
const maxYWithPadding = addPadding(nodeArray, padding);
|
|
273
|
-
sortFlows(nodeArray);
|
|
274
|
-
|
|
336
|
+
sortFlows(nodeArray, size);
|
|
275
337
|
return {maxX, maxY: maxYWithPadding};
|
|
276
338
|
}
|
|
277
339
|
|
|
278
340
|
/**
|
|
279
|
-
* @param
|
|
341
|
+
* @param {Array<SankeyDataPoint>} data Array of raw data elements
|
|
280
342
|
* @return {Map<string, SankeyNode>}
|
|
281
343
|
*/
|
|
282
344
|
function buildNodesFromRawData(data) {
|
|
@@ -290,45 +352,39 @@ function buildNodesFromRawData(data) {
|
|
|
290
352
|
in: 0,
|
|
291
353
|
out: flow,
|
|
292
354
|
from: [],
|
|
293
|
-
to: [{key: to, flow: flow}],
|
|
355
|
+
to: [{key: to, flow: flow, index: i}],
|
|
294
356
|
});
|
|
295
357
|
} else {
|
|
296
358
|
const node = nodes.get(from);
|
|
297
359
|
node.out += flow;
|
|
298
|
-
node.to.push({key: to, flow: flow});
|
|
360
|
+
node.to.push({key: to, flow: flow, index: i});
|
|
299
361
|
}
|
|
300
362
|
if (!nodes.has(to)) {
|
|
301
363
|
nodes.set(to, {
|
|
302
364
|
key: to,
|
|
303
365
|
in: flow,
|
|
304
366
|
out: 0,
|
|
305
|
-
from: [{key: from, flow: flow}],
|
|
367
|
+
from: [{key: from, flow: flow, index: i}],
|
|
306
368
|
to: [],
|
|
307
369
|
});
|
|
308
370
|
} else {
|
|
309
371
|
const node = nodes.get(to);
|
|
310
372
|
node.in += flow;
|
|
311
|
-
node.from.push({key: from, flow: flow});
|
|
373
|
+
node.from.push({key: from, flow: flow, index: i});
|
|
312
374
|
}
|
|
313
375
|
}
|
|
314
376
|
|
|
315
377
|
const flowSort = (a, b) => b.flow - a.flow;
|
|
316
378
|
|
|
317
379
|
[...nodes.values()].forEach(node => {
|
|
318
|
-
let tmp = 0;
|
|
319
380
|
node.from = node.from.sort(flowSort);
|
|
320
381
|
node.from.forEach(x => {
|
|
321
382
|
x.node = nodes.get(x.key);
|
|
322
|
-
x.addY = tmp;
|
|
323
|
-
tmp += x.flow;
|
|
324
383
|
});
|
|
325
384
|
|
|
326
|
-
tmp = 0;
|
|
327
385
|
node.to = node.to.sort(flowSort);
|
|
328
386
|
node.to.forEach(x => {
|
|
329
387
|
x.node = nodes.get(x.key);
|
|
330
|
-
x.addY = tmp;
|
|
331
|
-
tmp += x.flow;
|
|
332
388
|
});
|
|
333
389
|
});
|
|
334
390
|
|
|
@@ -336,14 +392,15 @@ function buildNodesFromRawData(data) {
|
|
|
336
392
|
}
|
|
337
393
|
|
|
338
394
|
/**
|
|
339
|
-
* @param
|
|
340
|
-
* @param
|
|
395
|
+
* @param {Array<FromToElement>} arr
|
|
396
|
+
* @param {string} key
|
|
397
|
+
* @param {number} index
|
|
341
398
|
* @return {number}
|
|
342
399
|
*/
|
|
343
|
-
function getAddY(arr, key) {
|
|
344
|
-
for (
|
|
345
|
-
if (
|
|
346
|
-
return
|
|
400
|
+
function getAddY(arr, key, index) {
|
|
401
|
+
for (const item of arr) {
|
|
402
|
+
if (item.key === key && item.index === index) {
|
|
403
|
+
return item.addY;
|
|
347
404
|
}
|
|
348
405
|
}
|
|
349
406
|
return 0;
|
|
@@ -351,10 +408,10 @@ function getAddY(arr, key) {
|
|
|
351
408
|
|
|
352
409
|
class SankeyController extends chart_js.DatasetController {
|
|
353
410
|
/**
|
|
354
|
-
* @param
|
|
355
|
-
* @param
|
|
356
|
-
* @param
|
|
357
|
-
* @param
|
|
411
|
+
* @param {ChartMeta<Flow, Element>} meta
|
|
412
|
+
* @param {Array<SankeyDataPoint>} data Array of original data elements
|
|
413
|
+
* @param {number} start
|
|
414
|
+
* @param {number} count
|
|
358
415
|
* @return {Array<SankeyParsedData>}
|
|
359
416
|
*/
|
|
360
417
|
parseObjectData(meta, data, start, count) {
|
|
@@ -367,7 +424,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
367
424
|
const parsed = []; /* Array<SankeyParsedData> */
|
|
368
425
|
const nodes = me._nodes = buildNodesFromRawData(data);
|
|
369
426
|
/* getDataset() => SankeyControllerDatasetOptions */
|
|
370
|
-
const priority = me.getDataset()
|
|
427
|
+
const {column, priority, size} = me.getDataset();
|
|
371
428
|
if (priority) {
|
|
372
429
|
for (const node of nodes.values()) {
|
|
373
430
|
if (node.key in priority) {
|
|
@@ -375,18 +432,26 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
375
432
|
}
|
|
376
433
|
}
|
|
377
434
|
}
|
|
435
|
+
if (column) {
|
|
436
|
+
for (const node of nodes.values()) {
|
|
437
|
+
if (node.key in column) {
|
|
438
|
+
node.column = true;
|
|
439
|
+
node.x = column[node.key];
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
378
443
|
|
|
379
|
-
const {maxX, maxY} = layout(nodes, data, !!priority);
|
|
444
|
+
const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
|
|
380
445
|
|
|
381
446
|
me._maxX = maxX;
|
|
382
447
|
me._maxY = maxY;
|
|
383
448
|
|
|
384
449
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
385
|
-
const
|
|
386
|
-
const from = nodes.get(
|
|
387
|
-
const to = nodes.get(
|
|
388
|
-
const fromY = from.y + getAddY(from.to,
|
|
389
|
-
const toY = to.y + getAddY(to.from,
|
|
450
|
+
const dataPoint = data[i];
|
|
451
|
+
const from = nodes.get(dataPoint.from);
|
|
452
|
+
const to = nodes.get(dataPoint.to);
|
|
453
|
+
const fromY = from.y + getAddY(from.to, dataPoint.to, i);
|
|
454
|
+
const toY = to.y + getAddY(to.from, dataPoint.from, i);
|
|
390
455
|
parsed.push({
|
|
391
456
|
x: xScale.parse(from.x, i),
|
|
392
457
|
y: yScale.parse(fromY, i),
|
|
@@ -395,7 +460,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
395
460
|
to,
|
|
396
461
|
x: xScale.parse(to.x, i),
|
|
397
462
|
y: yScale.parse(toY, i),
|
|
398
|
-
height: yScale.parse(
|
|
463
|
+
height: yScale.parse(dataPoint.flow, i),
|
|
399
464
|
}
|
|
400
465
|
});
|
|
401
466
|
}
|
|
@@ -418,10 +483,10 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
418
483
|
}
|
|
419
484
|
|
|
420
485
|
/**
|
|
421
|
-
* @param
|
|
422
|
-
* @param
|
|
423
|
-
* @param
|
|
424
|
-
* @param
|
|
486
|
+
* @param {Array<Flow>} elems
|
|
487
|
+
* @param {number} start
|
|
488
|
+
* @param {number} count
|
|
489
|
+
* @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
|
|
425
490
|
*/
|
|
426
491
|
updateElements(elems, start, count, mode) {
|
|
427
492
|
const me = this;
|
|
@@ -462,6 +527,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
462
527
|
const ctx = me._ctx;
|
|
463
528
|
const nodes = me._nodes || new Map();
|
|
464
529
|
const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
|
|
530
|
+
const size = validateSizeValue(dataset.size);
|
|
465
531
|
const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
|
|
466
532
|
const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
|
|
467
533
|
const labels = dataset.labels;
|
|
@@ -472,7 +538,8 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
472
538
|
for (const node of nodes.values()) {
|
|
473
539
|
const x = xScale.getPixelForValue(node.x);
|
|
474
540
|
const y = yScale.getPixelForValue(node.y);
|
|
475
|
-
|
|
541
|
+
|
|
542
|
+
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
476
543
|
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
|
|
477
544
|
const label = labels && labels[node.key] || node.key;
|
|
478
545
|
let textX = x;
|
|
@@ -491,19 +558,23 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
491
558
|
}
|
|
492
559
|
|
|
493
560
|
/**
|
|
494
|
-
* @param
|
|
495
|
-
* @param
|
|
496
|
-
* @param
|
|
497
|
-
* @param
|
|
498
|
-
* @param
|
|
561
|
+
* @param {string} label
|
|
562
|
+
* @param {number} y
|
|
563
|
+
* @param {number} height
|
|
564
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
565
|
+
* @param {number} textX
|
|
499
566
|
* @private
|
|
500
567
|
*/
|
|
501
568
|
_drawLabel(label, y, height, ctx, textX) {
|
|
502
|
-
const
|
|
569
|
+
const me = this;
|
|
570
|
+
const font = helpers.toFont(me.options.font, me.chart.options.font);
|
|
571
|
+
const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
|
|
503
572
|
const linesLength = lines.length;
|
|
504
573
|
const middle = y + height / 2;
|
|
505
|
-
const
|
|
506
|
-
const
|
|
574
|
+
const textHeight = font.lineHeight;
|
|
575
|
+
const padding = helpers.valueOrDefault(me.options.padding, textHeight / 2);
|
|
576
|
+
|
|
577
|
+
ctx.font = font.string;
|
|
507
578
|
|
|
508
579
|
if (linesLength > 1) {
|
|
509
580
|
const top = middle - (textHeight * linesLength / 2) + padding;
|
|
@@ -515,35 +586,12 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
515
586
|
}
|
|
516
587
|
}
|
|
517
588
|
|
|
518
|
-
/**
|
|
519
|
-
* @param inputs {string | Array<string>}
|
|
520
|
-
* @return {Array<string>}
|
|
521
|
-
* @todo move this in Chart.helpers.toTextLines
|
|
522
|
-
*/
|
|
523
|
-
toTextLines(inputs) {
|
|
524
|
-
let lines = [];
|
|
525
|
-
let input;
|
|
526
|
-
|
|
527
|
-
inputs = [].concat(inputs);
|
|
528
|
-
while (inputs.length) {
|
|
529
|
-
input = inputs.pop();
|
|
530
|
-
if (typeof input === 'string') {
|
|
531
|
-
lines.unshift.apply(lines, input.split('\n'));
|
|
532
|
-
} else if (Array.isArray(input)) {
|
|
533
|
-
inputs.push.apply(inputs, input);
|
|
534
|
-
} else if (!helpers.isNullOrUndef(inputs)) {
|
|
535
|
-
lines.unshift('' + input);
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return lines;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
589
|
_drawNodes() {
|
|
543
590
|
const me = this;
|
|
544
591
|
const ctx = me._ctx;
|
|
545
592
|
const nodes = me._nodes || new Map();
|
|
546
593
|
const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
|
|
594
|
+
const size = validateSizeValue(dataset.size);
|
|
547
595
|
const {xScale, yScale} = me._cachedMeta;
|
|
548
596
|
const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
|
|
549
597
|
const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
|
|
@@ -556,7 +604,8 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
556
604
|
ctx.fillStyle = node.color;
|
|
557
605
|
const x = xScale.getPixelForValue(node.x);
|
|
558
606
|
const y = yScale.getPixelForValue(node.y);
|
|
559
|
-
|
|
607
|
+
|
|
608
|
+
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
560
609
|
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
|
|
561
610
|
if (borderWidth) {
|
|
562
611
|
ctx.strokeRect(x, y, nodeWidth, height);
|
|
@@ -594,6 +643,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
594
643
|
}
|
|
595
644
|
|
|
596
645
|
SankeyController.id = 'sankey';
|
|
646
|
+
|
|
597
647
|
SankeyController.defaults = {
|
|
598
648
|
dataElementType: 'flow',
|
|
599
649
|
animations: {
|
|
@@ -629,9 +679,10 @@ SankeyController.defaults = {
|
|
|
629
679
|
from: 'transparent'
|
|
630
680
|
}
|
|
631
681
|
}
|
|
632
|
-
}
|
|
682
|
+
}
|
|
633
683
|
}
|
|
634
684
|
};
|
|
685
|
+
|
|
635
686
|
SankeyController.overrides = {
|
|
636
687
|
interaction: {
|
|
637
688
|
mode: 'nearest',
|
|
@@ -652,10 +703,10 @@ SankeyController.overrides = {
|
|
|
652
703
|
const item = context.dataset.data[context.dataIndex];
|
|
653
704
|
return item.from + ' -> ' + item.to + ': ' + item.flow;
|
|
654
705
|
}
|
|
655
|
-
}
|
|
706
|
+
},
|
|
656
707
|
},
|
|
657
708
|
legend: {
|
|
658
|
-
display: false
|
|
709
|
+
display: false,
|
|
659
710
|
},
|
|
660
711
|
},
|
|
661
712
|
scales: {
|
|
@@ -664,7 +715,7 @@ SankeyController.overrides = {
|
|
|
664
715
|
bounds: 'data',
|
|
665
716
|
display: false,
|
|
666
717
|
min: 0,
|
|
667
|
-
offset: false
|
|
718
|
+
offset: false,
|
|
668
719
|
},
|
|
669
720
|
y: {
|
|
670
721
|
type: 'linear',
|
|
@@ -672,27 +723,27 @@ SankeyController.overrides = {
|
|
|
672
723
|
display: false,
|
|
673
724
|
min: 0,
|
|
674
725
|
reverse: true,
|
|
675
|
-
offset: false
|
|
676
|
-
}
|
|
726
|
+
offset: false,
|
|
727
|
+
},
|
|
677
728
|
},
|
|
678
729
|
layout: {
|
|
679
730
|
padding: {
|
|
680
731
|
top: 3,
|
|
681
732
|
left: 3,
|
|
682
733
|
right: 13,
|
|
683
|
-
bottom: 3
|
|
684
|
-
}
|
|
685
|
-
}
|
|
734
|
+
bottom: 3,
|
|
735
|
+
},
|
|
736
|
+
},
|
|
686
737
|
};
|
|
687
738
|
|
|
688
739
|
/**
|
|
689
740
|
* @typedef {{x: number, y: number}} ControlPoint
|
|
690
741
|
* @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
|
|
691
742
|
*
|
|
692
|
-
* @param
|
|
693
|
-
* @param
|
|
694
|
-
* @param
|
|
695
|
-
* @param
|
|
743
|
+
* @param {number} x
|
|
744
|
+
* @param {number} y
|
|
745
|
+
* @param {number} x2
|
|
746
|
+
* @param {number} y2
|
|
696
747
|
* @return {ControlPoints}
|
|
697
748
|
*/
|
|
698
749
|
const controlPoints = (x, y, x2, y2) => x < x2
|
|
@@ -707,17 +758,39 @@ const controlPoints = (x, y, x2, y2) => x < x2
|
|
|
707
758
|
|
|
708
759
|
/**
|
|
709
760
|
*
|
|
710
|
-
* @param
|
|
711
|
-
* @param
|
|
712
|
-
* @param
|
|
761
|
+
* @param {ControlPoint} p1
|
|
762
|
+
* @param {ControlPoint} p2
|
|
763
|
+
* @param {number} t
|
|
713
764
|
* @return {ControlPoint}
|
|
714
765
|
*/
|
|
715
766
|
const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
|
|
716
767
|
|
|
768
|
+
/**
|
|
769
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
770
|
+
* @param {Flow} flow
|
|
771
|
+
*/
|
|
772
|
+
function setStyle(ctx, {x, x2, options}) {
|
|
773
|
+
let fill;
|
|
774
|
+
|
|
775
|
+
if (options.colorMode === 'from') {
|
|
776
|
+
fill = helpers.color(options.colorFrom).alpha(0.5).rgbString();
|
|
777
|
+
} else if (options.colorMode === 'to') {
|
|
778
|
+
fill = helpers.color(options.colorTo).alpha(0.5).rgbString();
|
|
779
|
+
} else {
|
|
780
|
+
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
781
|
+
fill.addColorStop(0, helpers.color(options.colorFrom).alpha(0.5).rgbString());
|
|
782
|
+
fill.addColorStop(1, helpers.color(options.colorTo).alpha(0.5).rgbString());
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
ctx.fillStyle = fill;
|
|
786
|
+
ctx.strokeStyle = fill;
|
|
787
|
+
ctx.lineWidth = 0.5;
|
|
788
|
+
}
|
|
789
|
+
|
|
717
790
|
class Flow extends chart_js.Element {
|
|
718
791
|
|
|
719
792
|
/**
|
|
720
|
-
* @param
|
|
793
|
+
* @param {FlowConfig} cfg
|
|
721
794
|
*/
|
|
722
795
|
constructor(cfg) {
|
|
723
796
|
super();
|
|
@@ -735,13 +808,12 @@ class Flow extends chart_js.Element {
|
|
|
735
808
|
}
|
|
736
809
|
|
|
737
810
|
/**
|
|
738
|
-
* @param
|
|
811
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
739
812
|
*/
|
|
740
813
|
draw(ctx) {
|
|
741
814
|
const me = this;
|
|
742
815
|
const {x, x2, y, y2, height, progress} = me;
|
|
743
816
|
const {cp1, cp2} = controlPoints(x, y, x2, y2);
|
|
744
|
-
const options = me.options;
|
|
745
817
|
|
|
746
818
|
if (progress === 0) {
|
|
747
819
|
return;
|
|
@@ -753,21 +825,7 @@ class Flow extends chart_js.Element {
|
|
|
753
825
|
ctx.clip();
|
|
754
826
|
}
|
|
755
827
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
if (options.colorMode === 'from') {
|
|
759
|
-
fill = helpers.color(options.colorFrom).alpha(0.5).rgbString();
|
|
760
|
-
} else if (options.colorMode === 'to') {
|
|
761
|
-
fill = helpers.color(options.colorTo).alpha(0.5).rgbString();
|
|
762
|
-
} else {
|
|
763
|
-
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
764
|
-
fill.addColorStop(0, helpers.color(options.colorFrom).alpha(0.5).rgbString());
|
|
765
|
-
fill.addColorStop(1, helpers.color(options.colorTo).alpha(0.5).rgbString());
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
ctx.fillStyle = fill;
|
|
769
|
-
ctx.strokeStyle = fill;
|
|
770
|
-
ctx.lineWidth = 0.5;
|
|
828
|
+
setStyle(ctx, me);
|
|
771
829
|
|
|
772
830
|
ctx.beginPath();
|
|
773
831
|
ctx.moveTo(x, y);
|
|
@@ -784,9 +842,9 @@ class Flow extends chart_js.Element {
|
|
|
784
842
|
}
|
|
785
843
|
|
|
786
844
|
/**
|
|
787
|
-
* @param
|
|
788
|
-
* @param
|
|
789
|
-
* @param
|
|
845
|
+
* @param {number} mouseX
|
|
846
|
+
* @param {number} mouseY
|
|
847
|
+
* @param {boolean} useFinalPosition
|
|
790
848
|
* @return {boolean}
|
|
791
849
|
*/
|
|
792
850
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
@@ -808,8 +866,8 @@ class Flow extends chart_js.Element {
|
|
|
808
866
|
}
|
|
809
867
|
|
|
810
868
|
/**
|
|
811
|
-
* @param
|
|
812
|
-
* @param
|
|
869
|
+
* @param {number} mouseX
|
|
870
|
+
* @param {boolean} useFinalPosition
|
|
813
871
|
* @return {boolean}
|
|
814
872
|
*/
|
|
815
873
|
inXRange(mouseX, useFinalPosition) {
|
|
@@ -818,8 +876,8 @@ class Flow extends chart_js.Element {
|
|
|
818
876
|
}
|
|
819
877
|
|
|
820
878
|
/**
|
|
821
|
-
* @param
|
|
822
|
-
* @param
|
|
879
|
+
* @param {number} mouseY
|
|
880
|
+
* @param {boolean} useFinalPosition
|
|
823
881
|
* @return {boolean}
|
|
824
882
|
*/
|
|
825
883
|
inYRange(mouseY, useFinalPosition) {
|
|
@@ -830,7 +888,7 @@ class Flow extends chart_js.Element {
|
|
|
830
888
|
}
|
|
831
889
|
|
|
832
890
|
/**
|
|
833
|
-
* @param
|
|
891
|
+
* @param {boolean} useFinalPosition
|
|
834
892
|
* @return {{x: number, y:number}}
|
|
835
893
|
*/
|
|
836
894
|
getCenterPoint(useFinalPosition) {
|
|
@@ -846,7 +904,7 @@ class Flow extends chart_js.Element {
|
|
|
846
904
|
}
|
|
847
905
|
|
|
848
906
|
/**
|
|
849
|
-
* @param
|
|
907
|
+
* @param {"x" | "y"} axis
|
|
850
908
|
* @return {number}
|
|
851
909
|
*/
|
|
852
910
|
getRange(axis) {
|
|
@@ -863,4 +921,4 @@ Flow.defaults = {
|
|
|
863
921
|
|
|
864
922
|
chart_js.Chart.register(SankeyController, Flow);
|
|
865
923
|
|
|
866
|
-
}))
|
|
924
|
+
}));
|