chartjs-chart-sankey 0.6.0 → 0.8.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/README.md +47 -20
- package/dist/chartjs-chart-sankey.esm.js +335 -77
- package/dist/chartjs-chart-sankey.js +338 -84
- package/dist/chartjs-chart-sankey.min.js +3 -3
- package/package.json +14 -5
- package/types/index.esm.d.ts +111 -0
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.8.1
|
|
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
|
import { DatasetController, Element } from 'chart.js';
|
|
8
|
-
import { valueOrDefault, color } from 'chart.js/helpers';
|
|
8
|
+
import { valueOrDefault, toFont, isNullOrUndef, color } from 'chart.js/helpers';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @param {Map<string, SankeyNode>} nodes
|
|
12
|
+
* @param {Array<SankeyDataPoint>} data
|
|
13
|
+
* @return {number}
|
|
14
|
+
*/
|
|
10
15
|
function calculateX(nodes, data) {
|
|
11
16
|
const to = new Set(data.map(x => x.to));
|
|
12
17
|
const from = new Set(data.map(x => x.from));
|
|
@@ -33,42 +38,87 @@ function calculateX(nodes, data) {
|
|
|
33
38
|
return x;
|
|
34
39
|
}
|
|
35
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @param {Array<string>} keys
|
|
43
|
+
* @param {Set<string>} to
|
|
44
|
+
* @return {Array<string>}
|
|
45
|
+
*/
|
|
36
46
|
function nextColumn(keys, to) {
|
|
37
47
|
const columnsNotInTo = keys.filter(key => !to.has(key));
|
|
38
48
|
return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
|
|
39
49
|
}
|
|
40
50
|
|
|
51
|
+
/**
|
|
52
|
+
* @param x {any}
|
|
53
|
+
* @return {boolean}
|
|
54
|
+
*/
|
|
41
55
|
const defined = x => x !== undefined;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {SankeyNode} a
|
|
59
|
+
* @param {SankeyNode} b
|
|
60
|
+
* @return {number}
|
|
61
|
+
*/
|
|
42
62
|
const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {Array<FromToElement>} list
|
|
66
|
+
* @param {string} prop
|
|
67
|
+
* @return {number}
|
|
68
|
+
*/
|
|
43
69
|
const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
|
|
44
|
-
const flowByNodeCount = (prop) => (a, b) => nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop);
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
71
|
+
/**
|
|
72
|
+
* @param {string} prop
|
|
73
|
+
* @return {function(FromToElement, FromToElement): number}
|
|
74
|
+
*/
|
|
75
|
+
const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
|
|
49
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
79
|
+
* @return {SankeyNode}
|
|
80
|
+
*/
|
|
81
|
+
const findLargestNode = (nodeArray) => nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out))[0];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {SankeyNode} node
|
|
85
|
+
* @param {number} y
|
|
86
|
+
* @return {number}
|
|
87
|
+
*/
|
|
50
88
|
function processFrom(node, y) {
|
|
51
89
|
node.from.sort(flowByNodeCount('from')).forEach(flow => {
|
|
52
90
|
const n = flow.node;
|
|
53
91
|
if (!defined(n.y)) {
|
|
54
92
|
n.y = y;
|
|
55
|
-
|
|
93
|
+
processFrom(n, y);
|
|
56
94
|
}
|
|
95
|
+
y = Math.max(n.y + n.out, y);
|
|
57
96
|
});
|
|
58
97
|
return y;
|
|
59
98
|
}
|
|
60
99
|
|
|
100
|
+
/**
|
|
101
|
+
* @param {SankeyNode} node
|
|
102
|
+
* @param {number} y
|
|
103
|
+
* @return {number}
|
|
104
|
+
*/
|
|
61
105
|
function processTo(node, y) {
|
|
62
106
|
node.to.sort(flowByNodeCount('to')).forEach(flow => {
|
|
63
107
|
const n = flow.node;
|
|
64
108
|
if (!defined(n.y)) {
|
|
65
109
|
n.y = y;
|
|
66
|
-
|
|
110
|
+
processTo(n, y);
|
|
67
111
|
}
|
|
112
|
+
y = Math.max(n.y + n.in, y);
|
|
68
113
|
});
|
|
69
114
|
return y;
|
|
70
115
|
}
|
|
71
116
|
|
|
117
|
+
/**
|
|
118
|
+
* @param {SankeyNode} node
|
|
119
|
+
* @param {number} value
|
|
120
|
+
* @return {number}
|
|
121
|
+
*/
|
|
72
122
|
function setOrGetY(node, value) {
|
|
73
123
|
if (defined(node.y)) {
|
|
74
124
|
return node.y;
|
|
@@ -77,14 +127,21 @@ function setOrGetY(node, value) {
|
|
|
77
127
|
return value;
|
|
78
128
|
}
|
|
79
129
|
|
|
130
|
+
/**
|
|
131
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
132
|
+
* @param {number} maxX
|
|
133
|
+
* @return {number}
|
|
134
|
+
*/
|
|
80
135
|
function processRest(nodeArray, maxX) {
|
|
81
136
|
const leftNodes = nodeArray.filter(node => node.x === 0);
|
|
82
137
|
const rightNodes = nodeArray.filter(node => node.x === maxX);
|
|
83
138
|
const leftToDo = leftNodes.filter(node => !defined(node.y));
|
|
84
139
|
const rightToDo = rightNodes.filter(node => !defined(node.y));
|
|
140
|
+
const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
|
|
85
141
|
|
|
86
142
|
let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
|
|
87
143
|
let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
|
|
144
|
+
let centerY = 0;
|
|
88
145
|
|
|
89
146
|
if (leftY >= rightY) {
|
|
90
147
|
leftToDo.forEach(node => {
|
|
@@ -107,10 +164,23 @@ function processRest(nodeArray, maxX) {
|
|
|
107
164
|
leftY = Math.max(leftY + node.out, processTo(node, leftY));
|
|
108
165
|
});
|
|
109
166
|
}
|
|
167
|
+
centerToDo.forEach(node => {
|
|
168
|
+
let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
|
|
169
|
+
.reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
|
|
170
|
+
y = setOrGetY(node, y);
|
|
171
|
+
y = Math.max(y + node.in, processFrom(node, y));
|
|
172
|
+
y = Math.max(y + node.out, processTo(node, y));
|
|
173
|
+
centerY = Math.max(centerY, y);
|
|
174
|
+
});
|
|
110
175
|
|
|
111
|
-
return Math.max(leftY, rightY);
|
|
176
|
+
return Math.max(leftY, rightY, centerY);
|
|
112
177
|
}
|
|
113
178
|
|
|
179
|
+
/**
|
|
180
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
181
|
+
* @param {number} maxX
|
|
182
|
+
* @return {number}
|
|
183
|
+
*/
|
|
114
184
|
function calculateY(nodeArray, maxX) {
|
|
115
185
|
const start = findLargestNode(nodeArray);
|
|
116
186
|
start.y = 0;
|
|
@@ -120,11 +190,18 @@ function calculateY(nodeArray, maxX) {
|
|
|
120
190
|
return Math.max(left, right, rest);
|
|
121
191
|
}
|
|
122
192
|
|
|
193
|
+
/**
|
|
194
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
195
|
+
* @param {number} maxX
|
|
196
|
+
* @return {number}
|
|
197
|
+
*/
|
|
123
198
|
function calculateYUsingPriority(nodeArray, maxX) {
|
|
124
199
|
let maxY = 0;
|
|
200
|
+
let nextYStart = 0;
|
|
125
201
|
for (let x = 0; x <= maxX; x++) {
|
|
126
|
-
let y =
|
|
202
|
+
let y = nextYStart;
|
|
127
203
|
const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
|
|
204
|
+
nextYStart = nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0;
|
|
128
205
|
for (const node of nodes) {
|
|
129
206
|
node.y = y;
|
|
130
207
|
y += Math.max(node.out, node.in);
|
|
@@ -134,6 +211,11 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
134
211
|
return maxY;
|
|
135
212
|
}
|
|
136
213
|
|
|
214
|
+
/**
|
|
215
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
216
|
+
* @param {number} padding
|
|
217
|
+
* @return {number}
|
|
218
|
+
*/
|
|
137
219
|
function addPadding(nodeArray, padding) {
|
|
138
220
|
let i = 1;
|
|
139
221
|
let x = 0;
|
|
@@ -158,95 +240,150 @@ function addPadding(nodeArray, padding) {
|
|
|
158
240
|
prev = i;
|
|
159
241
|
}
|
|
160
242
|
node.y += i * padding;
|
|
161
|
-
maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
|
|
162
243
|
i++;
|
|
163
244
|
}
|
|
245
|
+
maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
|
|
164
246
|
});
|
|
165
247
|
return maxY;
|
|
166
248
|
}
|
|
167
249
|
|
|
168
|
-
|
|
169
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @param {Array<SankeyNode>} nodeArray
|
|
252
|
+
* @param {'min' | 'max'} size
|
|
253
|
+
*/
|
|
254
|
+
function sortFlows(nodeArray, size) {
|
|
255
|
+
nodeArray.forEach((node) => {
|
|
256
|
+
const nodeSize = Math[size](node.in || node.out, node.out || node.in);
|
|
257
|
+
const overlapFrom = nodeSize < node.in;
|
|
258
|
+
const overlapTo = nodeSize < node.out;
|
|
170
259
|
let addY = 0;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
260
|
+
let len = node.from.length;
|
|
261
|
+
node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
|
|
262
|
+
if (overlapFrom) {
|
|
263
|
+
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
264
|
+
} else {
|
|
265
|
+
flow.addY = addY;
|
|
266
|
+
addY += flow.flow;
|
|
267
|
+
}
|
|
174
268
|
});
|
|
175
269
|
addY = 0;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
270
|
+
len = node.to.length;
|
|
271
|
+
node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
|
|
272
|
+
if (overlapTo) {
|
|
273
|
+
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
274
|
+
} else {
|
|
275
|
+
flow.addY = addY;
|
|
276
|
+
addY += flow.flow;
|
|
277
|
+
}
|
|
179
278
|
});
|
|
180
279
|
});
|
|
181
280
|
}
|
|
182
281
|
|
|
183
|
-
|
|
282
|
+
/**
|
|
283
|
+
* @param {Map<string, SankeyNode>} nodes
|
|
284
|
+
* @param {Array<SankeyDataPoint>} data
|
|
285
|
+
* @param {boolean} priority
|
|
286
|
+
* @param {'min' | 'max'} size
|
|
287
|
+
* @return {{maxY: number, maxX: number}}
|
|
288
|
+
*/
|
|
289
|
+
function layout(nodes, data, priority, size) {
|
|
184
290
|
const nodeArray = [...nodes.values()];
|
|
185
291
|
const maxX = calculateX(nodes, data);
|
|
186
292
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
187
293
|
const padding = maxY * 0.03; // rows;
|
|
188
|
-
|
|
189
294
|
const maxYWithPadding = addPadding(nodeArray, padding);
|
|
190
|
-
sortFlows(nodeArray);
|
|
191
|
-
|
|
295
|
+
sortFlows(nodeArray, size);
|
|
192
296
|
return {maxX, maxY: maxYWithPadding};
|
|
193
297
|
}
|
|
194
298
|
|
|
195
|
-
|
|
299
|
+
/**
|
|
300
|
+
* @param {Array<SankeyDataPoint>} data Array of raw data elements
|
|
301
|
+
* @return {Map<string, SankeyNode>}
|
|
302
|
+
*/
|
|
303
|
+
function buildNodesFromRawData(data) {
|
|
196
304
|
const nodes = new Map();
|
|
197
305
|
for (let i = 0; i < data.length; i++) {
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
306
|
+
const {from, to, flow} = data[i];
|
|
307
|
+
|
|
308
|
+
if (!nodes.has(from)) {
|
|
309
|
+
nodes.set(from, {
|
|
310
|
+
key: from,
|
|
311
|
+
in: 0,
|
|
312
|
+
out: flow,
|
|
313
|
+
from: [],
|
|
314
|
+
to: [{key: to, flow: flow, index: i}],
|
|
315
|
+
});
|
|
201
316
|
} else {
|
|
202
|
-
const node = nodes.get(
|
|
203
|
-
node.out +=
|
|
204
|
-
node.to.push({key:
|
|
317
|
+
const node = nodes.get(from);
|
|
318
|
+
node.out += flow;
|
|
319
|
+
node.to.push({key: to, flow: flow, index: i});
|
|
205
320
|
}
|
|
206
|
-
if (!nodes.has(
|
|
207
|
-
nodes.set(
|
|
321
|
+
if (!nodes.has(to)) {
|
|
322
|
+
nodes.set(to, {
|
|
323
|
+
key: to,
|
|
324
|
+
in: flow,
|
|
325
|
+
out: 0,
|
|
326
|
+
from: [{key: from, flow: flow, index: i}],
|
|
327
|
+
to: [],
|
|
328
|
+
});
|
|
208
329
|
} else {
|
|
209
|
-
const node = nodes.get(
|
|
210
|
-
node.in +=
|
|
211
|
-
node.from.push({key:
|
|
330
|
+
const node = nodes.get(to);
|
|
331
|
+
node.in += flow;
|
|
332
|
+
node.from.push({key: from, flow: flow, index: i});
|
|
212
333
|
}
|
|
213
334
|
}
|
|
214
335
|
|
|
215
336
|
const flowSort = (a, b) => b.flow - a.flow;
|
|
216
337
|
|
|
217
338
|
[...nodes.values()].forEach(node => {
|
|
218
|
-
let tmp = 0;
|
|
219
339
|
node.from = node.from.sort(flowSort);
|
|
220
340
|
node.from.forEach(x => {
|
|
221
341
|
x.node = nodes.get(x.key);
|
|
222
|
-
x.addY = tmp;
|
|
223
|
-
tmp += x.flow;
|
|
224
342
|
});
|
|
225
343
|
|
|
226
|
-
tmp = 0;
|
|
227
344
|
node.to = node.to.sort(flowSort);
|
|
228
345
|
node.to.forEach(x => {
|
|
229
346
|
x.node = nodes.get(x.key);
|
|
230
|
-
x.addY = tmp;
|
|
231
|
-
tmp += x.flow;
|
|
232
347
|
});
|
|
233
348
|
});
|
|
234
349
|
|
|
235
350
|
return nodes;
|
|
236
351
|
}
|
|
237
352
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
353
|
+
/**
|
|
354
|
+
* @param {Array<FromToElement>} arr
|
|
355
|
+
* @param {string} key
|
|
356
|
+
* @param {number} index
|
|
357
|
+
* @return {number}
|
|
358
|
+
*/
|
|
359
|
+
function getAddY(arr, key, index) {
|
|
360
|
+
for (const item of arr) {
|
|
361
|
+
if (item.key === key && item.index === index) {
|
|
362
|
+
return item.addY;
|
|
243
363
|
}
|
|
244
364
|
}
|
|
245
365
|
return 0;
|
|
246
366
|
}
|
|
247
367
|
|
|
248
|
-
|
|
368
|
+
/**
|
|
369
|
+
* @param {any} size
|
|
370
|
+
* @return {'min' | 'max'}
|
|
371
|
+
*/
|
|
372
|
+
function validateSizeValue(size) {
|
|
373
|
+
if (!size || ['min', 'max'].indexOf(size) === -1) {
|
|
374
|
+
return 'max';
|
|
375
|
+
}
|
|
376
|
+
return size;
|
|
377
|
+
}
|
|
249
378
|
|
|
379
|
+
class SankeyController extends DatasetController {
|
|
380
|
+
/**
|
|
381
|
+
* @param {ChartMeta<Flow, Element>} meta
|
|
382
|
+
* @param {Array<SankeyDataPoint>} data Array of original data elements
|
|
383
|
+
* @param {number} start
|
|
384
|
+
* @param {number} count
|
|
385
|
+
* @return {Array<SankeyParsedData>}
|
|
386
|
+
*/
|
|
250
387
|
parseObjectData(meta, data, start, count) {
|
|
251
388
|
// https://github.com/chartjs/Chart.js/pull/8379
|
|
252
389
|
if (count === 0) {
|
|
@@ -254,9 +391,10 @@ class SankeyController extends DatasetController {
|
|
|
254
391
|
}
|
|
255
392
|
const me = this;
|
|
256
393
|
const {xScale, yScale} = meta;
|
|
257
|
-
const parsed = [];
|
|
258
|
-
const nodes = me._nodes =
|
|
259
|
-
|
|
394
|
+
const parsed = []; /* Array<SankeyParsedData> */
|
|
395
|
+
const nodes = me._nodes = buildNodesFromRawData(data);
|
|
396
|
+
/* getDataset() => SankeyControllerDatasetOptions */
|
|
397
|
+
const {priority, size} = me.getDataset();
|
|
260
398
|
if (priority) {
|
|
261
399
|
for (const node of nodes.values()) {
|
|
262
400
|
if (node.key in priority) {
|
|
@@ -265,17 +403,18 @@ class SankeyController extends DatasetController {
|
|
|
265
403
|
}
|
|
266
404
|
}
|
|
267
405
|
|
|
268
|
-
const {maxX, maxY} = layout(nodes, data, !!priority);
|
|
406
|
+
const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
|
|
269
407
|
|
|
270
408
|
me._maxX = maxX;
|
|
271
409
|
me._maxY = maxY;
|
|
272
410
|
|
|
411
|
+
/* loop over raw data elements {SankeyDataPoint} */
|
|
273
412
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
274
|
-
const
|
|
275
|
-
const from = nodes.get(
|
|
276
|
-
const to = nodes.get(
|
|
277
|
-
const fromY = from.y + getAddY(from.to,
|
|
278
|
-
const toY = to.y + getAddY(to.from,
|
|
413
|
+
const dataPoint = data[i]; /* {SankeyDataPoint} */
|
|
414
|
+
const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
|
|
415
|
+
const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
|
|
416
|
+
const fromY = from.y + getAddY(from.to, dataPoint.to, i);
|
|
417
|
+
const toY = to.y + getAddY(to.from, dataPoint.from, i);
|
|
279
418
|
parsed.push({
|
|
280
419
|
x: xScale.parse(from.x, i),
|
|
281
420
|
y: yScale.parse(fromY, i),
|
|
@@ -284,7 +423,7 @@ class SankeyController extends DatasetController {
|
|
|
284
423
|
to,
|
|
285
424
|
x: xScale.parse(to.x, i),
|
|
286
425
|
y: yScale.parse(toY, i),
|
|
287
|
-
height: yScale.parse(
|
|
426
|
+
height: yScale.parse(dataPoint.flow, i),
|
|
288
427
|
}
|
|
289
428
|
});
|
|
290
429
|
}
|
|
@@ -306,6 +445,12 @@ class SankeyController extends DatasetController {
|
|
|
306
445
|
me.updateElements(meta.data, 0, meta.data.length, mode);
|
|
307
446
|
}
|
|
308
447
|
|
|
448
|
+
/**
|
|
449
|
+
* @param {Array<Flow>} elems
|
|
450
|
+
* @param {number} start
|
|
451
|
+
* @param {number} count
|
|
452
|
+
* @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
|
|
453
|
+
*/
|
|
309
454
|
updateElements(elems, start, count, mode) {
|
|
310
455
|
const me = this;
|
|
311
456
|
const {xScale, yScale} = me._cachedMeta;
|
|
@@ -316,6 +461,7 @@ class SankeyController extends DatasetController {
|
|
|
316
461
|
const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
|
|
317
462
|
|
|
318
463
|
for (let i = start; i < start + count; i++) {
|
|
464
|
+
/* getParsed(idx: number) => SankeyParsedData */
|
|
319
465
|
const parsed = me.getParsed(i);
|
|
320
466
|
const custom = parsed._custom;
|
|
321
467
|
const y = yScale.getPixelForValue(parsed.y);
|
|
@@ -343,7 +489,8 @@ class SankeyController extends DatasetController {
|
|
|
343
489
|
const me = this;
|
|
344
490
|
const ctx = me._ctx;
|
|
345
491
|
const nodes = me._nodes || new Map();
|
|
346
|
-
const dataset = me.getDataset();
|
|
492
|
+
const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
|
|
493
|
+
const size = validateSizeValue(dataset.size);
|
|
347
494
|
const borderWidth = valueOrDefault(dataset.borderWidth, 1);
|
|
348
495
|
const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
|
|
349
496
|
const labels = dataset.labels;
|
|
@@ -351,11 +498,11 @@ class SankeyController extends DatasetController {
|
|
|
351
498
|
|
|
352
499
|
ctx.save();
|
|
353
500
|
const chartArea = me.chart.chartArea;
|
|
354
|
-
|
|
355
501
|
for (const node of nodes.values()) {
|
|
356
502
|
const x = xScale.getPixelForValue(node.x);
|
|
357
503
|
const y = yScale.getPixelForValue(node.y);
|
|
358
|
-
|
|
504
|
+
|
|
505
|
+
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
359
506
|
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
|
|
360
507
|
const label = labels && labels[node.key] || node.key;
|
|
361
508
|
let textX = x;
|
|
@@ -368,16 +515,70 @@ class SankeyController extends DatasetController {
|
|
|
368
515
|
ctx.textAlign = 'right';
|
|
369
516
|
textX -= borderWidth + 4;
|
|
370
517
|
}
|
|
371
|
-
|
|
518
|
+
this._drawLabel(label, y, height, ctx, textX);
|
|
372
519
|
}
|
|
373
520
|
ctx.restore();
|
|
374
521
|
}
|
|
375
522
|
|
|
523
|
+
/**
|
|
524
|
+
* @param {string} label
|
|
525
|
+
* @param {number} y
|
|
526
|
+
* @param {number} height
|
|
527
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
528
|
+
* @param {number} textX
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
_drawLabel(label, y, height, ctx, textX) {
|
|
532
|
+
const me = this;
|
|
533
|
+
const font = toFont(me.options.font, me.chart.options.font);
|
|
534
|
+
const lines = isNullOrUndef(label) ? [] : me.toTextLines(label);
|
|
535
|
+
const linesLength = lines.length;
|
|
536
|
+
const middle = y + height / 2;
|
|
537
|
+
const textHeight = font.lineHeight;
|
|
538
|
+
const padding = valueOrDefault(me.options.padding, textHeight / 2);
|
|
539
|
+
|
|
540
|
+
ctx.font = font.string;
|
|
541
|
+
|
|
542
|
+
if (linesLength > 1) {
|
|
543
|
+
const top = middle - (textHeight * linesLength / 2) + padding;
|
|
544
|
+
for (let i = 0; i < linesLength; i++) {
|
|
545
|
+
ctx.fillText(lines[i], textX, top + (i * textHeight));
|
|
546
|
+
}
|
|
547
|
+
} else {
|
|
548
|
+
ctx.fillText(label, textX, middle);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @param {string | Array<string>} inputs
|
|
554
|
+
* @return {Array<string>}
|
|
555
|
+
* @todo move this in Chart.helpers.toTextLines
|
|
556
|
+
*/
|
|
557
|
+
toTextLines(inputs) {
|
|
558
|
+
let lines = [];
|
|
559
|
+
let input;
|
|
560
|
+
|
|
561
|
+
inputs = [].concat(inputs);
|
|
562
|
+
while (inputs.length) {
|
|
563
|
+
input = inputs.pop();
|
|
564
|
+
if (typeof input === 'string') {
|
|
565
|
+
lines.unshift.apply(lines, input.split('\n'));
|
|
566
|
+
} else if (Array.isArray(input)) {
|
|
567
|
+
inputs.push.apply(inputs, input);
|
|
568
|
+
} else if (!isNullOrUndef(inputs)) {
|
|
569
|
+
lines.unshift('' + input);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return lines;
|
|
574
|
+
}
|
|
575
|
+
|
|
376
576
|
_drawNodes() {
|
|
377
577
|
const me = this;
|
|
378
578
|
const ctx = me._ctx;
|
|
379
579
|
const nodes = me._nodes || new Map();
|
|
380
|
-
const dataset = me.getDataset();
|
|
580
|
+
const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
|
|
581
|
+
const size = validateSizeValue(dataset.size);
|
|
381
582
|
const {xScale, yScale} = me._cachedMeta;
|
|
382
583
|
const borderWidth = valueOrDefault(dataset.borderWidth, 1);
|
|
383
584
|
const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
|
|
@@ -390,7 +591,8 @@ class SankeyController extends DatasetController {
|
|
|
390
591
|
ctx.fillStyle = node.color;
|
|
391
592
|
const x = xScale.getPixelForValue(node.x);
|
|
392
593
|
const y = yScale.getPixelForValue(node.y);
|
|
393
|
-
|
|
594
|
+
|
|
595
|
+
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
394
596
|
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
|
|
395
597
|
if (borderWidth) {
|
|
396
598
|
ctx.strokeRect(x, y, nodeWidth, height);
|
|
@@ -400,28 +602,35 @@ class SankeyController extends DatasetController {
|
|
|
400
602
|
ctx.restore();
|
|
401
603
|
}
|
|
402
604
|
|
|
605
|
+
/**
|
|
606
|
+
* That's where the drawing process happens
|
|
607
|
+
*/
|
|
403
608
|
draw() {
|
|
404
609
|
const me = this;
|
|
405
610
|
const ctx = me._ctx;
|
|
406
|
-
const data = me.getMeta().data || [];
|
|
611
|
+
const data = me.getMeta().data || []; /* Array<Flow> */
|
|
407
612
|
|
|
408
613
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
409
|
-
const flow = data[i];
|
|
614
|
+
const flow = data[i]; /* Flow at index i */
|
|
410
615
|
flow.from.color = flow.options.colorFrom;
|
|
411
616
|
flow.to.color = flow.options.colorTo;
|
|
412
617
|
}
|
|
413
618
|
|
|
619
|
+
/* draw SankeyNodes on the canvas */
|
|
414
620
|
me._drawNodes();
|
|
415
621
|
|
|
622
|
+
/* draw Flow elements on the canvas */
|
|
416
623
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
417
624
|
data[i].draw(ctx);
|
|
418
625
|
}
|
|
419
626
|
|
|
627
|
+
/* draw labels (for SankeyNodes) on the canvas */
|
|
420
628
|
me._drawLabels();
|
|
421
629
|
}
|
|
422
630
|
}
|
|
423
631
|
|
|
424
632
|
SankeyController.id = 'sankey';
|
|
633
|
+
|
|
425
634
|
SankeyController.defaults = {
|
|
426
635
|
dataElementType: 'flow',
|
|
427
636
|
animations: {
|
|
@@ -457,9 +666,10 @@ SankeyController.defaults = {
|
|
|
457
666
|
from: 'transparent'
|
|
458
667
|
}
|
|
459
668
|
}
|
|
460
|
-
}
|
|
669
|
+
}
|
|
461
670
|
}
|
|
462
671
|
};
|
|
672
|
+
|
|
463
673
|
SankeyController.overrides = {
|
|
464
674
|
interaction: {
|
|
465
675
|
mode: 'nearest',
|
|
@@ -480,10 +690,10 @@ SankeyController.overrides = {
|
|
|
480
690
|
const item = context.dataset.data[context.dataIndex];
|
|
481
691
|
return item.from + ' -> ' + item.to + ': ' + item.flow;
|
|
482
692
|
}
|
|
483
|
-
}
|
|
693
|
+
},
|
|
484
694
|
},
|
|
485
695
|
legend: {
|
|
486
|
-
display: false
|
|
696
|
+
display: false,
|
|
487
697
|
},
|
|
488
698
|
},
|
|
489
699
|
scales: {
|
|
@@ -492,7 +702,7 @@ SankeyController.overrides = {
|
|
|
492
702
|
bounds: 'data',
|
|
493
703
|
display: false,
|
|
494
704
|
min: 0,
|
|
495
|
-
offset: false
|
|
705
|
+
offset: false,
|
|
496
706
|
},
|
|
497
707
|
y: {
|
|
498
708
|
type: 'linear',
|
|
@@ -500,19 +710,29 @@ SankeyController.overrides = {
|
|
|
500
710
|
display: false,
|
|
501
711
|
min: 0,
|
|
502
712
|
reverse: true,
|
|
503
|
-
offset: false
|
|
504
|
-
}
|
|
713
|
+
offset: false,
|
|
714
|
+
},
|
|
505
715
|
},
|
|
506
716
|
layout: {
|
|
507
717
|
padding: {
|
|
508
718
|
top: 3,
|
|
509
719
|
left: 3,
|
|
510
720
|
right: 13,
|
|
511
|
-
bottom: 3
|
|
512
|
-
}
|
|
513
|
-
}
|
|
721
|
+
bottom: 3,
|
|
722
|
+
},
|
|
723
|
+
},
|
|
514
724
|
};
|
|
515
725
|
|
|
726
|
+
/**
|
|
727
|
+
* @typedef {{x: number, y: number}} ControlPoint
|
|
728
|
+
* @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
|
|
729
|
+
*
|
|
730
|
+
* @param {number} x
|
|
731
|
+
* @param {number} y
|
|
732
|
+
* @param {number} x2
|
|
733
|
+
* @param {number} y2
|
|
734
|
+
* @return {ControlPoints}
|
|
735
|
+
*/
|
|
516
736
|
const controlPoints = (x, y, x2, y2) => x < x2
|
|
517
737
|
? {
|
|
518
738
|
cp1: {x: x + (x2 - x) / 3 * 2, y},
|
|
@@ -523,9 +743,20 @@ const controlPoints = (x, y, x2, y2) => x < x2
|
|
|
523
743
|
cp2: {x: x2 + (x - x2) / 3, y: 0}
|
|
524
744
|
};
|
|
525
745
|
|
|
746
|
+
/**
|
|
747
|
+
*
|
|
748
|
+
* @param {ControlPoint} p1
|
|
749
|
+
* @param {ControlPoint} p2
|
|
750
|
+
* @param {number} t
|
|
751
|
+
* @return {ControlPoint}
|
|
752
|
+
*/
|
|
526
753
|
const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
|
|
527
754
|
|
|
528
755
|
class Flow extends Element {
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* @param {FlowConfig} cfg
|
|
759
|
+
*/
|
|
529
760
|
constructor(cfg) {
|
|
530
761
|
super();
|
|
531
762
|
|
|
@@ -541,6 +772,9 @@ class Flow extends Element {
|
|
|
541
772
|
}
|
|
542
773
|
}
|
|
543
774
|
|
|
775
|
+
/**
|
|
776
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
777
|
+
*/
|
|
544
778
|
draw(ctx) {
|
|
545
779
|
const me = this;
|
|
546
780
|
const {x, x2, y, y2, height, progress} = me;
|
|
@@ -587,6 +821,12 @@ class Flow extends Element {
|
|
|
587
821
|
ctx.restore();
|
|
588
822
|
}
|
|
589
823
|
|
|
824
|
+
/**
|
|
825
|
+
* @param {number} mouseX
|
|
826
|
+
* @param {number} mouseY
|
|
827
|
+
* @param {boolean} useFinalPosition
|
|
828
|
+
* @return {boolean}
|
|
829
|
+
*/
|
|
590
830
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
591
831
|
const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
|
|
592
832
|
if (mouseX < x || mouseX > x2) {
|
|
@@ -605,11 +845,21 @@ class Flow extends Element {
|
|
|
605
845
|
return mouseY >= topY && mouseY <= topY + height;
|
|
606
846
|
}
|
|
607
847
|
|
|
848
|
+
/**
|
|
849
|
+
* @param {number} mouseX
|
|
850
|
+
* @param {boolean} useFinalPosition
|
|
851
|
+
* @return {boolean}
|
|
852
|
+
*/
|
|
608
853
|
inXRange(mouseX, useFinalPosition) {
|
|
609
854
|
const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition);
|
|
610
855
|
return mouseX >= x && mouseX <= x2;
|
|
611
856
|
}
|
|
612
857
|
|
|
858
|
+
/**
|
|
859
|
+
* @param {number} mouseY
|
|
860
|
+
* @param {boolean} useFinalPosition
|
|
861
|
+
* @return {boolean}
|
|
862
|
+
*/
|
|
613
863
|
inYRange(mouseY, useFinalPosition) {
|
|
614
864
|
const {y, y2, height} = this.getProps(['y', 'y2', 'height'], useFinalPosition);
|
|
615
865
|
const minY = Math.min(y, y2);
|
|
@@ -617,6 +867,10 @@ class Flow extends Element {
|
|
|
617
867
|
return mouseY >= minY && mouseY <= maxY;
|
|
618
868
|
}
|
|
619
869
|
|
|
870
|
+
/**
|
|
871
|
+
* @param {boolean} useFinalPosition
|
|
872
|
+
* @return {{x: number, y:number}}
|
|
873
|
+
*/
|
|
620
874
|
getCenterPoint(useFinalPosition) {
|
|
621
875
|
const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
|
|
622
876
|
return {
|
|
@@ -625,10 +879,14 @@ class Flow extends Element {
|
|
|
625
879
|
};
|
|
626
880
|
}
|
|
627
881
|
|
|
628
|
-
tooltipPosition() {
|
|
629
|
-
return this.getCenterPoint();
|
|
882
|
+
tooltipPosition(useFinalPosition) {
|
|
883
|
+
return this.getCenterPoint(useFinalPosition);
|
|
630
884
|
}
|
|
631
885
|
|
|
886
|
+
/**
|
|
887
|
+
* @param {"x" | "y"} axis
|
|
888
|
+
* @return {number}
|
|
889
|
+
*/
|
|
632
890
|
getRange(axis) {
|
|
633
891
|
return axis === 'x' ? this.width / 2 : this.height / 2;
|
|
634
892
|
}
|