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