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