chartjs-chart-sankey 0.8.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 +19 -5
- package/dist/chartjs-chart-sankey.esm.js +101 -84
- package/dist/chartjs-chart-sankey.js +100 -83
- package/dist/chartjs-chart-sankey.min.js +2 -2
- package/package.json +13 -13
- package/types/index.esm.d.ts +3 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chartjs-chart-sankey
|
|
2
2
|
|
|
3
|
-
[Chart.js](https://www.chartjs.org/)
|
|
3
|
+
[Chart.js](https://www.chartjs.org/) **^3.3** module for creating sankey diagrams
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/chartjs-chart-sankey)
|
|
6
6
|
[](https://github.com/kurkle/chartjs-chart-sankey/releases/latest)
|
|
@@ -49,6 +49,15 @@ const chart = new Chart(ctx, {
|
|
|
49
49
|
Example:
|
|
50
50
|
|
|
51
51
|
```js
|
|
52
|
+
const colors = {
|
|
53
|
+
a: 'red',
|
|
54
|
+
b: 'green',
|
|
55
|
+
c: 'blue',
|
|
56
|
+
d: 'gray'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getColor = (key) => colors[key];
|
|
60
|
+
|
|
52
61
|
const chart = new Chart(ctx, {
|
|
53
62
|
type: 'sankey',
|
|
54
63
|
data: {
|
|
@@ -57,7 +66,8 @@ const chart = new Chart(ctx, {
|
|
|
57
66
|
data: [
|
|
58
67
|
{from: 'a', to: 'b', flow: 10},
|
|
59
68
|
{from: 'a', to: 'c', flow: 5},
|
|
60
|
-
{from: 'b', to: 'c', flow: 10}
|
|
69
|
+
{from: 'b', to: 'c', flow: 10},
|
|
70
|
+
{from: 'd', to: 'c', flow: 7}
|
|
61
71
|
],
|
|
62
72
|
colorFrom: (c) => getColor(c.dataset.data[c.dataIndex].from),
|
|
63
73
|
colorTo: (c) => getColor(c.dataset.data[c.dataIndex].to),
|
|
@@ -66,13 +76,17 @@ const chart = new Chart(ctx, {
|
|
|
66
76
|
labels: {
|
|
67
77
|
a: 'Label A',
|
|
68
78
|
b: 'Label B',
|
|
69
|
-
c: 'Label C'
|
|
79
|
+
c: 'Label C',
|
|
80
|
+
d: 'Label D'
|
|
70
81
|
},
|
|
71
82
|
/* optional priority */
|
|
72
83
|
priority: {
|
|
73
|
-
a: 0,
|
|
74
84
|
b: 1,
|
|
75
|
-
|
|
85
|
+
d: 0
|
|
86
|
+
},
|
|
87
|
+
/* optional column overrides */
|
|
88
|
+
column: {
|
|
89
|
+
d: 1
|
|
76
90
|
},
|
|
77
91
|
size: 'max', // or 'min' if flow overlap is preferred
|
|
78
92
|
}]
|
|
@@ -1,11 +1,50 @@
|
|
|
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
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, toFont,
|
|
8
|
+
import { isArray, isNullOrUndef, valueOrDefault, toFont, color } from 'chart.js/helpers';
|
|
9
|
+
|
|
10
|
+
/**
|
|
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;
|
|
9
48
|
|
|
10
49
|
/**
|
|
11
50
|
* @param {Map<string, SankeyNode>} nodes
|
|
@@ -13,15 +52,18 @@ import { valueOrDefault, toFont, isNullOrUndef, color } from 'chart.js/helpers';
|
|
|
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,7 +74,11 @@ 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;
|
|
@@ -48,12 +94,6 @@ function nextColumn(keys, to) {
|
|
|
48
94
|
return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
|
|
49
95
|
}
|
|
50
96
|
|
|
51
|
-
/**
|
|
52
|
-
* @param x {any}
|
|
53
|
-
* @return {boolean}
|
|
54
|
-
*/
|
|
55
|
-
const defined = x => x !== undefined;
|
|
56
|
-
|
|
57
97
|
/**
|
|
58
98
|
* @param {SankeyNode} a
|
|
59
99
|
* @param {SankeyNode} b
|
|
@@ -74,26 +114,21 @@ const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop]
|
|
|
74
114
|
*/
|
|
75
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
|
-
/**
|
|
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
117
|
/**
|
|
84
118
|
* @param {SankeyNode} node
|
|
85
119
|
* @param {number} y
|
|
86
120
|
* @return {number}
|
|
87
121
|
*/
|
|
88
122
|
function processFrom(node, y) {
|
|
89
|
-
node.from.sort(flowByNodeCount('from'))
|
|
123
|
+
node.from.sort(flowByNodeCount('from'));
|
|
124
|
+
for (const flow of node.from) {
|
|
90
125
|
const n = flow.node;
|
|
91
126
|
if (!defined(n.y)) {
|
|
92
127
|
n.y = y;
|
|
93
128
|
processFrom(n, y);
|
|
94
129
|
}
|
|
95
130
|
y = Math.max(n.y + n.out, y);
|
|
96
|
-
}
|
|
131
|
+
}
|
|
97
132
|
return y;
|
|
98
133
|
}
|
|
99
134
|
|
|
@@ -103,14 +138,15 @@ function processFrom(node, y) {
|
|
|
103
138
|
* @return {number}
|
|
104
139
|
*/
|
|
105
140
|
function processTo(node, y) {
|
|
106
|
-
node.to.sort(flowByNodeCount('to'))
|
|
141
|
+
node.to.sort(flowByNodeCount('to'));
|
|
142
|
+
for (const flow of node.to) {
|
|
107
143
|
const n = flow.node;
|
|
108
144
|
if (!defined(n.y)) {
|
|
109
145
|
n.y = y;
|
|
110
146
|
processTo(n, y);
|
|
111
147
|
}
|
|
112
148
|
y = Math.max(n.y + n.in, y);
|
|
113
|
-
}
|
|
149
|
+
}
|
|
114
150
|
return y;
|
|
115
151
|
}
|
|
116
152
|
|
|
@@ -182,7 +218,8 @@ function processRest(nodeArray, maxX) {
|
|
|
182
218
|
* @return {number}
|
|
183
219
|
*/
|
|
184
220
|
function calculateY(nodeArray, maxX) {
|
|
185
|
-
|
|
221
|
+
nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
|
|
222
|
+
const start = nodeArray[0];
|
|
186
223
|
start.y = 0;
|
|
187
224
|
const left = processFrom(start, 0);
|
|
188
225
|
const right = processTo(start, 0);
|
|
@@ -222,7 +259,8 @@ function addPadding(nodeArray, padding) {
|
|
|
222
259
|
let prev = 0;
|
|
223
260
|
let maxY = 0;
|
|
224
261
|
const rows = [];
|
|
225
|
-
nodeArray.sort(nodeByXY)
|
|
262
|
+
nodeArray.sort(nodeByXY);
|
|
263
|
+
for (const node of nodeArray) {
|
|
226
264
|
if (node.y) {
|
|
227
265
|
if (node.x === 0) {
|
|
228
266
|
rows.push(node.y);
|
|
@@ -243,7 +281,7 @@ function addPadding(nodeArray, padding) {
|
|
|
243
281
|
i++;
|
|
244
282
|
}
|
|
245
283
|
maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
|
|
246
|
-
}
|
|
284
|
+
}
|
|
247
285
|
return maxY;
|
|
248
286
|
}
|
|
249
287
|
|
|
@@ -365,17 +403,6 @@ function getAddY(arr, key, index) {
|
|
|
365
403
|
return 0;
|
|
366
404
|
}
|
|
367
405
|
|
|
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
|
-
}
|
|
378
|
-
|
|
379
406
|
class SankeyController extends DatasetController {
|
|
380
407
|
/**
|
|
381
408
|
* @param {ChartMeta<Flow, Element>} meta
|
|
@@ -394,7 +421,7 @@ class SankeyController extends DatasetController {
|
|
|
394
421
|
const parsed = []; /* Array<SankeyParsedData> */
|
|
395
422
|
const nodes = me._nodes = buildNodesFromRawData(data);
|
|
396
423
|
/* getDataset() => SankeyControllerDatasetOptions */
|
|
397
|
-
const {priority, size} = me.getDataset();
|
|
424
|
+
const {column, priority, size} = me.getDataset();
|
|
398
425
|
if (priority) {
|
|
399
426
|
for (const node of nodes.values()) {
|
|
400
427
|
if (node.key in priority) {
|
|
@@ -402,17 +429,24 @@ class SankeyController extends DatasetController {
|
|
|
402
429
|
}
|
|
403
430
|
}
|
|
404
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
|
+
}
|
|
405
440
|
|
|
406
441
|
const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
|
|
407
442
|
|
|
408
443
|
me._maxX = maxX;
|
|
409
444
|
me._maxY = maxY;
|
|
410
445
|
|
|
411
|
-
/* loop over raw data elements {SankeyDataPoint} */
|
|
412
446
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
413
|
-
const dataPoint = data[i];
|
|
414
|
-
const from = nodes.get(dataPoint.from);
|
|
415
|
-
const to = nodes.get(dataPoint.to);
|
|
447
|
+
const dataPoint = data[i];
|
|
448
|
+
const from = nodes.get(dataPoint.from);
|
|
449
|
+
const to = nodes.get(dataPoint.to);
|
|
416
450
|
const fromY = from.y + getAddY(from.to, dataPoint.to, i);
|
|
417
451
|
const toY = to.y + getAddY(to.from, dataPoint.from, i);
|
|
418
452
|
parsed.push({
|
|
@@ -531,7 +565,7 @@ class SankeyController extends DatasetController {
|
|
|
531
565
|
_drawLabel(label, y, height, ctx, textX) {
|
|
532
566
|
const me = this;
|
|
533
567
|
const font = toFont(me.options.font, me.chart.options.font);
|
|
534
|
-
const lines = isNullOrUndef(label) ? [] :
|
|
568
|
+
const lines = isNullOrUndef(label) ? [] : toTextLines(label);
|
|
535
569
|
const linesLength = lines.length;
|
|
536
570
|
const middle = y + height / 2;
|
|
537
571
|
const textHeight = font.lineHeight;
|
|
@@ -549,30 +583,6 @@ class SankeyController extends DatasetController {
|
|
|
549
583
|
}
|
|
550
584
|
}
|
|
551
585
|
|
|
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
|
-
|
|
576
586
|
_drawNodes() {
|
|
577
587
|
const me = this;
|
|
578
588
|
const ctx = me._ctx;
|
|
@@ -752,6 +762,28 @@ const controlPoints = (x, y, x2, y2) => x < x2
|
|
|
752
762
|
*/
|
|
753
763
|
const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
|
|
754
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
|
+
|
|
755
787
|
class Flow extends Element {
|
|
756
788
|
|
|
757
789
|
/**
|
|
@@ -779,7 +811,6 @@ class Flow extends Element {
|
|
|
779
811
|
const me = this;
|
|
780
812
|
const {x, x2, y, y2, height, progress} = me;
|
|
781
813
|
const {cp1, cp2} = controlPoints(x, y, x2, y2);
|
|
782
|
-
const options = me.options;
|
|
783
814
|
|
|
784
815
|
if (progress === 0) {
|
|
785
816
|
return;
|
|
@@ -791,21 +822,7 @@ class Flow extends Element {
|
|
|
791
822
|
ctx.clip();
|
|
792
823
|
}
|
|
793
824
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
if (options.colorMode === 'from') {
|
|
797
|
-
fill = color(options.colorFrom).alpha(0.5).rgbString();
|
|
798
|
-
} else if (options.colorMode === 'to') {
|
|
799
|
-
fill = color(options.colorTo).alpha(0.5).rgbString();
|
|
800
|
-
} else {
|
|
801
|
-
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
802
|
-
fill.addColorStop(0, color(options.colorFrom).alpha(0.5).rgbString());
|
|
803
|
-
fill.addColorStop(1, color(options.colorTo).alpha(0.5).rgbString());
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
ctx.fillStyle = fill;
|
|
807
|
-
ctx.strokeStyle = fill;
|
|
808
|
-
ctx.lineWidth = 0.5;
|
|
825
|
+
setStyle(ctx, me);
|
|
809
826
|
|
|
810
827
|
ctx.beginPath();
|
|
811
828
|
ctx.moveTo(x, y);
|
|
@@ -1,5 +1,5 @@
|
|
|
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
4
|
* (c) 2022 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -10,21 +10,63 @@ typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpe
|
|
|
10
10
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
|
|
11
11
|
})(this, (function (chart_js, helpers) { 'use strict';
|
|
12
12
|
|
|
13
|
+
/**
|
|
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
|
+
|
|
13
52
|
/**
|
|
14
53
|
* @param {Map<string, SankeyNode>} nodes
|
|
15
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,7 +77,11 @@ 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;
|
|
@@ -51,12 +97,6 @@ function nextColumn(keys, to) {
|
|
|
51
97
|
return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
|
|
52
98
|
}
|
|
53
99
|
|
|
54
|
-
/**
|
|
55
|
-
* @param x {any}
|
|
56
|
-
* @return {boolean}
|
|
57
|
-
*/
|
|
58
|
-
const defined = x => x !== undefined;
|
|
59
|
-
|
|
60
100
|
/**
|
|
61
101
|
* @param {SankeyNode} a
|
|
62
102
|
* @param {SankeyNode} b
|
|
@@ -77,26 +117,21 @@ const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop]
|
|
|
77
117
|
*/
|
|
78
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
|
-
/**
|
|
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];
|
|
85
|
-
|
|
86
120
|
/**
|
|
87
121
|
* @param {SankeyNode} node
|
|
88
122
|
* @param {number} y
|
|
89
123
|
* @return {number}
|
|
90
124
|
*/
|
|
91
125
|
function processFrom(node, y) {
|
|
92
|
-
node.from.sort(flowByNodeCount('from'))
|
|
126
|
+
node.from.sort(flowByNodeCount('from'));
|
|
127
|
+
for (const flow of node.from) {
|
|
93
128
|
const n = flow.node;
|
|
94
129
|
if (!defined(n.y)) {
|
|
95
130
|
n.y = y;
|
|
96
131
|
processFrom(n, y);
|
|
97
132
|
}
|
|
98
133
|
y = Math.max(n.y + n.out, y);
|
|
99
|
-
}
|
|
134
|
+
}
|
|
100
135
|
return y;
|
|
101
136
|
}
|
|
102
137
|
|
|
@@ -106,14 +141,15 @@ function processFrom(node, y) {
|
|
|
106
141
|
* @return {number}
|
|
107
142
|
*/
|
|
108
143
|
function processTo(node, y) {
|
|
109
|
-
node.to.sort(flowByNodeCount('to'))
|
|
144
|
+
node.to.sort(flowByNodeCount('to'));
|
|
145
|
+
for (const flow of node.to) {
|
|
110
146
|
const n = flow.node;
|
|
111
147
|
if (!defined(n.y)) {
|
|
112
148
|
n.y = y;
|
|
113
149
|
processTo(n, y);
|
|
114
150
|
}
|
|
115
151
|
y = Math.max(n.y + n.in, y);
|
|
116
|
-
}
|
|
152
|
+
}
|
|
117
153
|
return y;
|
|
118
154
|
}
|
|
119
155
|
|
|
@@ -185,7 +221,8 @@ function processRest(nodeArray, maxX) {
|
|
|
185
221
|
* @return {number}
|
|
186
222
|
*/
|
|
187
223
|
function calculateY(nodeArray, maxX) {
|
|
188
|
-
|
|
224
|
+
nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
|
|
225
|
+
const start = nodeArray[0];
|
|
189
226
|
start.y = 0;
|
|
190
227
|
const left = processFrom(start, 0);
|
|
191
228
|
const right = processTo(start, 0);
|
|
@@ -225,7 +262,8 @@ function addPadding(nodeArray, padding) {
|
|
|
225
262
|
let prev = 0;
|
|
226
263
|
let maxY = 0;
|
|
227
264
|
const rows = [];
|
|
228
|
-
nodeArray.sort(nodeByXY)
|
|
265
|
+
nodeArray.sort(nodeByXY);
|
|
266
|
+
for (const node of nodeArray) {
|
|
229
267
|
if (node.y) {
|
|
230
268
|
if (node.x === 0) {
|
|
231
269
|
rows.push(node.y);
|
|
@@ -246,7 +284,7 @@ function addPadding(nodeArray, padding) {
|
|
|
246
284
|
i++;
|
|
247
285
|
}
|
|
248
286
|
maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
|
|
249
|
-
}
|
|
287
|
+
}
|
|
250
288
|
return maxY;
|
|
251
289
|
}
|
|
252
290
|
|
|
@@ -368,17 +406,6 @@ function getAddY(arr, key, index) {
|
|
|
368
406
|
return 0;
|
|
369
407
|
}
|
|
370
408
|
|
|
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
|
-
}
|
|
381
|
-
|
|
382
409
|
class SankeyController extends chart_js.DatasetController {
|
|
383
410
|
/**
|
|
384
411
|
* @param {ChartMeta<Flow, Element>} meta
|
|
@@ -397,7 +424,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
397
424
|
const parsed = []; /* Array<SankeyParsedData> */
|
|
398
425
|
const nodes = me._nodes = buildNodesFromRawData(data);
|
|
399
426
|
/* getDataset() => SankeyControllerDatasetOptions */
|
|
400
|
-
const {priority, size} = me.getDataset();
|
|
427
|
+
const {column, priority, size} = me.getDataset();
|
|
401
428
|
if (priority) {
|
|
402
429
|
for (const node of nodes.values()) {
|
|
403
430
|
if (node.key in priority) {
|
|
@@ -405,17 +432,24 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
405
432
|
}
|
|
406
433
|
}
|
|
407
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
|
+
}
|
|
408
443
|
|
|
409
444
|
const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
|
|
410
445
|
|
|
411
446
|
me._maxX = maxX;
|
|
412
447
|
me._maxY = maxY;
|
|
413
448
|
|
|
414
|
-
/* loop over raw data elements {SankeyDataPoint} */
|
|
415
449
|
for (let i = 0, ilen = data.length; i < ilen; ++i) {
|
|
416
|
-
const dataPoint = data[i];
|
|
417
|
-
const from = nodes.get(dataPoint.from);
|
|
418
|
-
const to = nodes.get(dataPoint.to);
|
|
450
|
+
const dataPoint = data[i];
|
|
451
|
+
const from = nodes.get(dataPoint.from);
|
|
452
|
+
const to = nodes.get(dataPoint.to);
|
|
419
453
|
const fromY = from.y + getAddY(from.to, dataPoint.to, i);
|
|
420
454
|
const toY = to.y + getAddY(to.from, dataPoint.from, i);
|
|
421
455
|
parsed.push({
|
|
@@ -534,7 +568,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
534
568
|
_drawLabel(label, y, height, ctx, textX) {
|
|
535
569
|
const me = this;
|
|
536
570
|
const font = helpers.toFont(me.options.font, me.chart.options.font);
|
|
537
|
-
const lines = helpers.isNullOrUndef(label) ? [] :
|
|
571
|
+
const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
|
|
538
572
|
const linesLength = lines.length;
|
|
539
573
|
const middle = y + height / 2;
|
|
540
574
|
const textHeight = font.lineHeight;
|
|
@@ -552,30 +586,6 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
552
586
|
}
|
|
553
587
|
}
|
|
554
588
|
|
|
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
|
-
|
|
579
589
|
_drawNodes() {
|
|
580
590
|
const me = this;
|
|
581
591
|
const ctx = me._ctx;
|
|
@@ -755,6 +765,28 @@ const controlPoints = (x, y, x2, y2) => x < x2
|
|
|
755
765
|
*/
|
|
756
766
|
const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
|
|
757
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
|
+
|
|
758
790
|
class Flow extends chart_js.Element {
|
|
759
791
|
|
|
760
792
|
/**
|
|
@@ -782,7 +814,6 @@ class Flow extends chart_js.Element {
|
|
|
782
814
|
const me = this;
|
|
783
815
|
const {x, x2, y, y2, height, progress} = me;
|
|
784
816
|
const {cp1, cp2} = controlPoints(x, y, x2, y2);
|
|
785
|
-
const options = me.options;
|
|
786
817
|
|
|
787
818
|
if (progress === 0) {
|
|
788
819
|
return;
|
|
@@ -794,21 +825,7 @@ class Flow extends chart_js.Element {
|
|
|
794
825
|
ctx.clip();
|
|
795
826
|
}
|
|
796
827
|
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
if (options.colorMode === 'from') {
|
|
800
|
-
fill = helpers.color(options.colorFrom).alpha(0.5).rgbString();
|
|
801
|
-
} else if (options.colorMode === 'to') {
|
|
802
|
-
fill = helpers.color(options.colorTo).alpha(0.5).rgbString();
|
|
803
|
-
} else {
|
|
804
|
-
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
805
|
-
fill.addColorStop(0, helpers.color(options.colorFrom).alpha(0.5).rgbString());
|
|
806
|
-
fill.addColorStop(1, helpers.color(options.colorTo).alpha(0.5).rgbString());
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
ctx.fillStyle = fill;
|
|
810
|
-
ctx.strokeStyle = fill;
|
|
811
|
-
ctx.lineWidth = 0.5;
|
|
828
|
+
setStyle(ctx, me);
|
|
812
829
|
|
|
813
830
|
ctx.beginPath();
|
|
814
831
|
ctx.moveTo(x, y);
|
|
@@ -1,7 +1,7 @@
|
|
|
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
4
|
* (c) 2022 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Chart,t.Chart.helpers)}(this,(function(t,e){"use strict";function o(t,e){const o=t.filter((t=>!e.has(t)));return o.length?o:t.slice(0,1)}const
|
|
7
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["chart.js","chart.js/helpers"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).Chart,t.Chart.helpers)}(this,(function(t,e){"use strict";function o(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}const r=t=>void 0!==t;function n(t,e){const o=t.filter((t=>!e.has(t)));return o.length?o:t.slice(0,1)}const a=(t,e)=>t.x!==e.x?t.x-e.x:t.y-e.y,i=(t,e)=>t.reduce(((t,o)=>t+o.node[e].length+i(o.node[e],e)),0),s=t=>(e,o)=>i(e.node[t],t)-i(o.node[t],t)||e.node[t].length-o.node[t].length;function l(t,e){t.from.sort(s("from"));for(const o of t.from){const t=o.node;r(t.y)||(t.y=e,l(t,e)),e=Math.max(t.y+t.out,e)}return e}function c(t,e){t.to.sort(s("to"));for(const o of t.to){const t=o.node;r(t.y)||(t.y=e,c(t,e)),e=Math.max(t.y+t.in,e)}return e}function h(t,e){return r(t.y)?t.y:(t.y=e,e)}function f(t,e){t.sort(((t,e)=>Math.max(e.in,e.out)-Math.max(t.in,t.out)));const o=t[0];o.y=0;const n=l(o,0),a=c(o,0),i=function(t,e){const o=t.filter((t=>0===t.x)),n=t.filter((t=>t.x===e)),a=o.filter((t=>!r(t.y))),i=n.filter((t=>!r(t.y))),s=t.filter((t=>t.x>0&&t.x<e&&!r(t.y)));let f=o.reduce(((t,e)=>Math.max(t,e.y+e.out||0)),0),d=n.reduce(((t,e)=>Math.max(t,e.y+e.in||0)),0),u=0;return f>=d?(a.forEach((t=>{f=h(t,f),f=Math.max(f+t.out,c(t,f))})),i.forEach((t=>{d=h(t,d),d=Math.max(d+t.in,c(t,d))}))):(i.forEach((t=>{d=h(t,d),d=Math.max(d+t.in,c(t,d))})),a.forEach((t=>{f=h(t,f),f=Math.max(f+t.out,c(t,f))}))),s.forEach((e=>{let o=t.filter((t=>t.x===e.x&&r(t.y))).reduce(((t,e)=>Math.max(t,e.y+Math.max(e.in,e.out))),0);o=h(e,o),o=Math.max(o+e.in,l(e,o)),o=Math.max(o+e.out,c(e,o)),u=Math.max(u,o)})),Math.max(f,d,u)}(t,e);return Math.max(n,a,i)}function d(t,e,o,i){const s=[...t.values()],l=function(t,e){const o=new Set(e.map((t=>t.to))),a=new Set(e.map((t=>t.from))),i=new Set([...t.keys()]);let s=0;for(;i.size;){const a=n([...i],o);for(const e of a){const o=t.get(e);r(o.x)||(o.x=s),i.delete(e)}i.size&&(o.clear(),e.filter((t=>i.has(t.from))).forEach((t=>o.add(t.to))),s++)}return[...t.keys()].filter((t=>!a.has(t))).forEach((e=>{const o=t.get(e);o.column||(o.x=s)})),s}(t,e),c=o?function(t,e){let o=0,r=0;for(let n=0;n<=e;n++){let e=r;const a=t.filter((t=>t.x===n)).sort(((t,e)=>t.priority-e.priority));r=a[0].to.filter((t=>t.node.x>n+1)).reduce(((t,e)=>t+e.flow),0)||0;for(const t of a)t.y=e,e+=Math.max(t.out,t.in);o=Math.max(e,o)}return o}(s,l):f(s,l),h=function(t,e){let o=1,r=0,n=0,i=0;const s=[];t.sort(a);for(const a of t){if(a.y){if(0===a.x)s.push(a.y);else{for(r!==a.x&&(r=a.x,n=0),o=n+1;o<s.length&&!(s[o]>a.y);o++);n=o}a.y+=o*e,o++}i=Math.max(i,a.y+Math.max(a.in,a.out))}return i}(s,.03*c);return function(t,e){t.forEach((t=>{const o=Math[e](t.in||t.out,t.out||t.in),r=o<t.in,n=o<t.out;let a=0,i=t.from.length;t.from.sort(((t,e)=>t.node.y+t.node.out/2-(e.node.y+e.node.out/2))).forEach(((t,e)=>{r?t.addY=e*(o-t.flow)/(i-1):(t.addY=a,a+=t.flow)})),a=0,i=t.to.length,t.to.sort(((t,e)=>t.node.y+t.node.in/2-(e.node.y+e.node.in/2))).forEach(((t,e)=>{n?t.addY=e*(o-t.flow)/(i-1):(t.addY=a,a+=t.flow)}))}))}(s,i),{maxX:l,maxY:h}}function u(t,e,o){for(const r of t)if(r.key===e&&r.index===o)return r.addY;return 0}class x extends t.DatasetController{parseObjectData(t,e,r,n){if(0===n)return[];const a=this,{xScale:i,yScale:s}=t,l=[],c=a._nodes=function(t){const e=new Map;for(let o=0;o<t.length;o++){const{from:r,to:n,flow:a}=t[o];if(e.has(r)){const t=e.get(r);t.out+=a,t.to.push({key:n,flow:a,index:o})}else e.set(r,{key:r,in:0,out:a,from:[],to:[{key:n,flow:a,index:o}]});if(e.has(n)){const t=e.get(n);t.in+=a,t.from.push({key:r,flow:a,index:o})}else e.set(n,{key:n,in:a,out:0,from:[{key:r,flow:a,index:o}],to:[]})}const o=(t,e)=>e.flow-t.flow;return[...e.values()].forEach((t=>{t.from=t.from.sort(o),t.from.forEach((t=>{t.node=e.get(t.key)})),t.to=t.to.sort(o),t.to.forEach((t=>{t.node=e.get(t.key)}))})),e}(e),{column:h,priority:f,size:x}=a.getDataset();if(f)for(const t of c.values())t.key in f&&(t.priority=f[t.key]);if(h)for(const t of c.values())t.key in h&&(t.column=!0,t.x=h[t.key]);const{maxX:y,maxY:p}=d(c,e,!!f,o(x));a._maxX=y,a._maxY=p;for(let t=0,o=e.length;t<o;++t){const o=e[t],r=c.get(o.from),n=c.get(o.to),a=r.y+u(r.to,o.to,t),h=n.y+u(n.from,o.from,t);l.push({x:i.parse(r.x,t),y:s.parse(a,t),_custom:{from:r,to:n,x:i.parse(n.x,t),y:s.parse(h,t),height:s.parse(o.flow,t)}})}return l.slice(r,r+n)}getMinMax(t){return{min:0,max:t===this._cachedMeta.xScale?this._maxX:this._maxY}}update(t){const e=this._cachedMeta;this.updateElements(e.data,0,e.data.length,t)}updateElements(t,o,r,n){const a=this,{xScale:i,yScale:s}=a._cachedMeta,l=a.resolveDataElementOptions(o,n),c=a.getSharedOptions(n,t[o],l),h=a.getDataset(),f=e.valueOrDefault(h.borderWidth,1)/2+.5,d=e.valueOrDefault(h.nodeWidth,10);for(let e=o;e<o+r;e++){const o=a.getParsed(e),r=o._custom,l=s.getPixelForValue(o.y);a.updateElement(t[e],e,{x:i.getPixelForValue(o.x)+d+f,y:l,x2:i.getPixelForValue(r.x)-f,y2:s.getPixelForValue(r.y),from:r.from,to:r.to,progress:"reset"===n?0:1,height:Math.abs(s.getPixelForValue(o.y+r.height)-l),options:a.resolveDataElementOptions(e,n)},n)}a.updateSharedOptions(c,n)}_drawLabels(){const t=this,r=t._ctx,n=t._nodes||new Map,a=t.getDataset(),i=o(a.size),s=e.valueOrDefault(a.borderWidth,1),l=e.valueOrDefault(a.nodeWidth,10),c=a.labels,{xScale:h,yScale:f}=t._cachedMeta;r.save();const d=t.chart.chartArea;for(const t of n.values()){const e=h.getPixelForValue(t.x),o=f.getPixelForValue(t.y),n=Math[i](t.in||t.out,t.out||t.in),u=Math.abs(f.getPixelForValue(t.y+n)-o),x=c&&c[t.key]||t.key;let y=e;r.fillStyle=a.color||"black",r.textBaseline="middle",e<d.width/2?(r.textAlign="left",y+=l+s+4):(r.textAlign="right",y-=s+4),this._drawLabel(x,o,u,r,y)}r.restore()}_drawLabel(t,o,r,n,a){const i=this,s=e.toFont(i.options.font,i.chart.options.font),l=e.isNullOrUndef(t)?[]:function(t){const o=[],r=e.isArray(t)?t:e.isNullOrUndef(t)?[]:[t];for(;r.length;){const t=r.pop();"string"==typeof t?o.unshift.apply(o,t.split("\n")):Array.isArray(t)?r.push.apply(r,t):e.isNullOrUndef(r)||o.unshift(""+t)}return o}(t),c=l.length,h=o+r/2,f=s.lineHeight,d=e.valueOrDefault(i.options.padding,f/2);if(n.font=s.string,c>1){const t=h-f*c/2+d;for(let e=0;e<c;e++)n.fillText(l[e],a,t+e*f)}else n.fillText(t,a,h)}_drawNodes(){const t=this,r=t._ctx,n=t._nodes||new Map,a=t.getDataset(),i=o(a.size),{xScale:s,yScale:l}=t._cachedMeta,c=e.valueOrDefault(a.borderWidth,1),h=e.valueOrDefault(a.nodeWidth,10);r.save(),r.strokeStyle=a.borderColor||"black",r.lineWidth=c;for(const t of n.values()){r.fillStyle=t.color;const e=s.getPixelForValue(t.x),o=l.getPixelForValue(t.y),n=Math[i](t.in||t.out,t.out||t.in),a=Math.abs(l.getPixelForValue(t.y+n)-o);c&&r.strokeRect(e,o,h,a),r.fillRect(e,o,h,a)}r.restore()}draw(){const t=this,e=t._ctx,o=t.getMeta().data||[];for(let t=0,e=o.length;t<e;++t){const e=o[t];e.from.color=e.options.colorFrom,e.to.color=e.options.colorTo}t._drawNodes();for(let t=0,r=o.length;t<r;++t)o[t].draw(e);t._drawLabels()}}x.id="sankey",x.defaults={dataElementType:"flow",animations:{numbers:{type:"number",properties:["x","y","x2","y2","height"]},progress:{easing:"linear",duration:t=>"data"===t.type?200*(t.parsed._custom.x-t.parsed.x):void 0,delay:t=>"data"===t.type?500*t.parsed.x+20*t.dataIndex:void 0},colors:{type:"color",properties:["colorFrom","colorTo"]}},transitions:{hide:{animations:{colors:{type:"color",properties:["colorFrom","colorTo"],to:"transparent"}}},show:{animations:{colors:{type:"color",properties:["colorFrom","colorTo"],from:"transparent"}}}}},x.overrides={interaction:{mode:"nearest",intersect:!0},datasets:{color:()=>"#efefef",clip:!1,parsing:!0},plugins:{tooltip:{callbacks:{title:()=>"",label(t){const e=t.dataset.data[t.dataIndex];return e.from+" -> "+e.to+": "+e.flow}}},legend:{display:!1}},scales:{x:{type:"linear",bounds:"data",display:!1,min:0,offset:!1},y:{type:"linear",bounds:"data",display:!1,min:0,reverse:!0,offset:!1}},layout:{padding:{top:3,left:3,right:13,bottom:3}}};const y=(t,e,o,r)=>t<o?{cp1:{x:t+(o-t)/3*2,y:e},cp2:{x:t+(o-t)/3,y:r}}:{cp1:{x:t-(t-o)/3,y:0},cp2:{x:o+(t-o)/3,y:0}},p=(t,e,o)=>({x:t.x+o*(e.x-t.x),y:t.y+o*(e.y-t.y)});class g extends t.Element{constructor(t){super(),this.options=void 0,this.x=void 0,this.y=void 0,this.x2=void 0,this.y2=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const{x:o,x2:r,y:n,y2:a,height:i,progress:s}=this,{cp1:l,cp2:c}=y(o,n,r,a);0!==s&&(t.save(),s<1&&(t.beginPath(),t.rect(o,Math.min(n,a),(r-o)*s+1,Math.abs(a-n)+i+1),t.clip()),function(t,{x:o,x2:r,options:n}){let a;"from"===n.colorMode?a=e.color(n.colorFrom).alpha(.5).rgbString():"to"===n.colorMode?a=e.color(n.colorTo).alpha(.5).rgbString():(a=t.createLinearGradient(o,0,r,0),a.addColorStop(0,e.color(n.colorFrom).alpha(.5).rgbString()),a.addColorStop(1,e.color(n.colorTo).alpha(.5).rgbString())),t.fillStyle=a,t.strokeStyle=a,t.lineWidth=.5}(t,this),t.beginPath(),t.moveTo(o,n),t.bezierCurveTo(l.x,l.y,c.x,c.y,r,a),t.lineTo(r,a+i),t.bezierCurveTo(c.x,c.y+i,l.x,l.y+i,o,n+i),t.lineTo(o,n),t.stroke(),t.closePath(),t.fill(),t.restore())}inRange(t,e,o){const{x:r,y:n,x2:a,y2:i,height:s}=this.getProps(["x","y","x2","y2","height"],o);if(t<r||t>a)return!1;const{cp1:l,cp2:c}=y(r,n,a,i),h=(t-r)/(a-r),f={x:a,y:i},d=p({x:r,y:n},l,h),u=p(l,c,h),x=p(c,f,h),g=p(d,u,h),m=p(u,x,h),M=p(g,m,h).y;return e>=M&&e<=M+s}inXRange(t,e){const{x:o,x2:r}=this.getProps(["x","x2"],e);return t>=o&&t<=r}inYRange(t,e){const{y:o,y2:r,height:n}=this.getProps(["y","y2","height"],e),a=Math.min(o,r),i=Math.max(o,r)+n;return t>=a&&t<=i}getCenterPoint(t){const{x:e,y:o,x2:r,y2:n,height:a}=this.getProps(["x","y","x2","y2","height"],t);return{x:(e+r)/2,y:(o+n+a)/2}}tooltipPosition(t){return this.getCenterPoint(t)}getRange(t){return"x"===t?this.width/2:this.height/2}}g.id="flow",g.defaults={colorFrom:"red",colorTo:"green",colorMode:"gradient"},t.Chart.register(x,g)}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chartjs-chart-sankey",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Chart.js module for creating sankey diagrams",
|
|
5
5
|
"main": "dist/chartjs-chart-sankey.js",
|
|
6
6
|
"module": "dist/chartjs-chart-sankey.esm.js",
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"autobuild": "rollup -c -w",
|
|
11
11
|
"dev": "karma start --no-single-run --auto-watch --browsers chrome",
|
|
12
12
|
"dev:ff": "karma start --no-single-run --auto-watch --browsers firefox",
|
|
13
|
-
"lint": "concurrently
|
|
13
|
+
"lint": "concurrently --group \"npm:lint-*\"",
|
|
14
14
|
"lint-js": "eslint \"src/**/*.js\" \"test/**/*.js\"",
|
|
15
15
|
"lint-md": "eslint \"**/*.md\"",
|
|
16
16
|
"lint-types": "eslint \"types/**/*.ts\" && tsc -p types/tests/",
|
|
17
|
-
"test": "cross-env NODE_ENV=test concurrently \"npm:test-*\"",
|
|
17
|
+
"test": "cross-env NODE_ENV=test concurrently --group \"npm:test-*\"",
|
|
18
18
|
"test-lint": "npm run lint",
|
|
19
19
|
"test-karma": "karma start --auto-watch --single-run"
|
|
20
20
|
},
|
|
@@ -39,20 +39,20 @@
|
|
|
39
39
|
"homepage": "https://github.com/kurkle/chartjs-chart-sankey#readme",
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@rollup/plugin-node-resolve": "^13.0.0",
|
|
42
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
43
|
-
"@typescript-eslint/parser": "^
|
|
44
|
-
"chart.js": "^3.
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^5.10.2",
|
|
43
|
+
"@typescript-eslint/parser": "^5.10.2",
|
|
44
|
+
"chart.js": "^3.3.0",
|
|
45
45
|
"chartjs-adapter-date-fns": "^2.0.0",
|
|
46
|
-
"chartjs-test-utils": "^0.
|
|
47
|
-
"concurrently": "^
|
|
46
|
+
"chartjs-test-utils": "^0.4.0",
|
|
47
|
+
"concurrently": "^7.0.0",
|
|
48
48
|
"cross-env": "^7.0.3",
|
|
49
49
|
"date-fns": "^2.19.0",
|
|
50
|
-
"eslint": "^
|
|
50
|
+
"eslint": "^8.8.0",
|
|
51
51
|
"eslint-config-chartjs": "^0.3.0",
|
|
52
52
|
"eslint-plugin-es": "^4.1.0",
|
|
53
53
|
"eslint-plugin-html": "^6.1.2",
|
|
54
54
|
"eslint-plugin-markdown": "^2.2.0",
|
|
55
|
-
"jasmine-core": "^
|
|
55
|
+
"jasmine-core": "^4.0.0",
|
|
56
56
|
"karma": "^6.2.0",
|
|
57
57
|
"karma-chrome-launcher": "^3.1.0",
|
|
58
58
|
"karma-coverage": "^2.0.3",
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"karma-jasmine": "^4.0.1",
|
|
61
61
|
"karma-jasmine-html-reporter": "^1.5.4",
|
|
62
62
|
"karma-rollup-preprocessor": "^7.0.7",
|
|
63
|
-
"karma-spec-reporter": "0.0.
|
|
63
|
+
"karma-spec-reporter": "^0.0.33",
|
|
64
64
|
"rollup": "^2.42.1",
|
|
65
65
|
"rollup-plugin-istanbul": "^3.0.0",
|
|
66
66
|
"rollup-plugin-terser": "^7.0.2",
|
|
67
|
-
"typescript": "
|
|
67
|
+
"typescript": "^4.5.5"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"chart.js": "^3.
|
|
70
|
+
"chart.js": "^3.3.0"
|
|
71
71
|
}
|
|
72
72
|
}
|
package/types/index.esm.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ declare module 'chart.js' {
|
|
|
24
24
|
colorMode: 'gradient' | 'from' | 'to';
|
|
25
25
|
/* Map<node.key, priority_value> */
|
|
26
26
|
priority?: Record<string, number>
|
|
27
|
+
column?: Record<string, number>
|
|
27
28
|
/* Map<node.key, label> */
|
|
28
29
|
labels?: Record<string, string>
|
|
29
30
|
|
|
@@ -52,6 +53,8 @@ declare module 'chart.js' {
|
|
|
52
53
|
out: number
|
|
53
54
|
from: Array<FromToElement>
|
|
54
55
|
to: Array<FromToElement>
|
|
56
|
+
/* true if x is defined by SankeyControllerDatasetOptions.column map */
|
|
57
|
+
column?: boolean
|
|
55
58
|
/* priority extracted from the SankeyControllerDatasetOptions.priority map */
|
|
56
59
|
priority?: number
|
|
57
60
|
y?: number
|