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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # chartjs-chart-sankey
2
2
 
3
- [Chart.js](https://www.chartjs.org/) **v3.3.x** module for creating sankey diagrams
3
+ [Chart.js](https://www.chartjs.org/) **^3.3** module for creating sankey diagrams
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/chartjs-chart-sankey.svg)](https://www.npmjs.com/package/chartjs-chart-sankey)
6
6
  [![pre-release](https://img.shields.io/github/v/release/kurkle/chartjs-chart-sankey?include_prereleases&style=flat-square)](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
- c: 2
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.8.1
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, isNullOrUndef, color } from 'chart.js/helpers';
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(x => x.to));
17
- const from = new Set(data.map(x => x.from));
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 (let i = 0; i < column.length; i++) {
23
- nodes.get(column[i]).x = x;
24
- keys.delete(column[i]);
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).x = x;
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')).forEach(flow => {
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')).forEach(flow => {
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
- const start = findLargestNode(nodeArray);
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).forEach(node => {
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]; /* {SankeyDataPoint} */
414
- const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
415
- const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
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) ? [] : me.toTextLines(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
- let fill;
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.8.1
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(x => x.to));
20
- const from = new Set(data.map(x => x.from));
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 (let i = 0; i < column.length; i++) {
26
- nodes.get(column[i]).x = x;
27
- keys.delete(column[i]);
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).x = x;
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')).forEach(flow => {
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')).forEach(flow => {
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
- const start = findLargestNode(nodeArray);
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).forEach(node => {
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]; /* {SankeyDataPoint} */
417
- const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
418
- const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
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) ? [] : me.toTextLines(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
- let fill;
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.8.1
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 r=t=>void 0!==t,a=(t,e)=>t.x!==e.x?t.x-e.x:t.y-e.y,n=(t,e)=>t.reduce(((t,o)=>t+o.node[e].length+n(o.node[e],e)),0),i=t=>(e,o)=>n(e.node[t],t)-n(o.node[t],t)||e.node[t].length-o.node[t].length;function s(t,e){return t.from.sort(i("from")).forEach((t=>{const o=t.node;r(o.y)||(o.y=e,s(o,e)),e=Math.max(o.y+o.out,e)})),e}function l(t,e){return t.to.sort(i("to")).forEach((t=>{const o=t.node;r(o.y)||(o.y=e,l(o,e)),e=Math.max(o.y+o.in,e)})),e}function c(t,e){return r(t.y)?t.y:(t.y=e,e)}function h(t,e){const o=(t=>t.sort(((t,e)=>Math.max(e.in,e.out)-Math.max(t.in,t.out)))[0])(t);o.y=0;const a=s(o,0),n=l(o,0),i=function(t,e){const o=t.filter((t=>0===t.x)),a=t.filter((t=>t.x===e)),n=o.filter((t=>!r(t.y))),i=a.filter((t=>!r(t.y))),h=t.filter((t=>t.x>0&&t.x<e&&!r(t.y)));let d=o.reduce(((t,e)=>Math.max(t,e.y+e.out||0)),0),f=a.reduce(((t,e)=>Math.max(t,e.y+e.in||0)),0),u=0;return d>=f?(n.forEach((t=>{d=c(t,d),d=Math.max(d+t.out,l(t,d))})),i.forEach((t=>{f=c(t,f),f=Math.max(f+t.in,l(t,f))}))):(i.forEach((t=>{f=c(t,f),f=Math.max(f+t.in,l(t,f))})),n.forEach((t=>{d=c(t,d),d=Math.max(d+t.out,l(t,d))}))),h.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=c(e,o),o=Math.max(o+e.in,s(e,o)),o=Math.max(o+e.out,l(e,o)),u=Math.max(u,o)})),Math.max(d,f,u)}(t,e);return Math.max(a,n,i)}function d(t,e,r,n){const i=[...t.values()],s=function(t,e){const r=new Set(e.map((t=>t.to))),a=new Set(e.map((t=>t.from))),n=new Set([...t.keys()]);let i=0;for(;n.size;){const a=o([...n],r);for(let e=0;e<a.length;e++)t.get(a[e]).x=i,n.delete(a[e]);n.size&&(r.clear(),e.filter((t=>n.has(t.from))).forEach((t=>r.add(t.to))),i++)}return[...t.keys()].filter((t=>!a.has(t))).forEach((e=>{t.get(e).x=i})),i}(t,e),l=r?function(t,e){let o=0,r=0;for(let a=0;a<=e;a++){let e=r;const n=t.filter((t=>t.x===a)).sort(((t,e)=>t.priority-e.priority));r=n[0].to.filter((t=>t.node.x>a+1)).reduce(((t,e)=>t+e.flow),0)||0;for(const t of n)t.y=e,e+=Math.max(t.out,t.in);o=Math.max(e,o)}return o}(i,s):h(i,s),c=function(t,e){let o=1,r=0,n=0,i=0;const s=[];return t.sort(a).forEach((t=>{if(t.y){if(0===t.x)s.push(t.y);else{for(r!==t.x&&(r=t.x,n=0),o=n+1;o<s.length&&!(s[o]>t.y);o++);n=o}t.y+=o*e,o++}i=Math.max(i,t.y+Math.max(t.in,t.out))})),i}(i,.03*l);return function(t,e){t.forEach((t=>{const o=Math[e](t.in||t.out,t.out||t.in),r=o<t.in,a=o<t.out;let n=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=n,n+=t.flow)})),n=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)=>{a?t.addY=e*(o-t.flow)/(i-1):(t.addY=n,n+=t.flow)}))}))}(i,n),{maxX:s,maxY:c}}function f(t,e,o){for(const r of t)if(r.key===e&&r.index===o)return r.addY;return 0}function u(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}class x extends t.DatasetController{parseObjectData(t,e,o,r){if(0===r)return[];const a=this,{xScale:n,yScale:i}=t,s=[],l=a._nodes=function(t){const e=new Map;for(let o=0;o<t.length;o++){const{from:r,to:a,flow:n}=t[o];if(e.has(r)){const t=e.get(r);t.out+=n,t.to.push({key:a,flow:n,index:o})}else e.set(r,{key:r,in:0,out:n,from:[],to:[{key:a,flow:n,index:o}]});if(e.has(a)){const t=e.get(a);t.in+=n,t.from.push({key:r,flow:n,index:o})}else e.set(a,{key:a,in:n,out:0,from:[{key:r,flow:n,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),{priority:c,size:h}=a.getDataset();if(c)for(const t of l.values())t.key in c&&(t.priority=c[t.key]);const{maxX:x,maxY:y}=d(l,e,!!c,u(h));a._maxX=x,a._maxY=y;for(let t=0,o=e.length;t<o;++t){const o=e[t],r=l.get(o.from),a=l.get(o.to),c=r.y+f(r.to,o.to,t),h=a.y+f(a.from,o.from,t);s.push({x:n.parse(r.x,t),y:i.parse(c,t),_custom:{from:r,to:a,x:n.parse(a.x,t),y:i.parse(h,t),height:i.parse(o.flow,t)}})}return s.slice(o,o+r)}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,a){const n=this,{xScale:i,yScale:s}=n._cachedMeta,l=n.resolveDataElementOptions(o,a),c=n.getSharedOptions(a,t[o],l),h=n.getDataset(),d=e.valueOrDefault(h.borderWidth,1)/2+.5,f=e.valueOrDefault(h.nodeWidth,10);for(let e=o;e<o+r;e++){const o=n.getParsed(e),r=o._custom,l=s.getPixelForValue(o.y);n.updateElement(t[e],e,{x:i.getPixelForValue(o.x)+f+d,y:l,x2:i.getPixelForValue(r.x)-d,y2:s.getPixelForValue(r.y),from:r.from,to:r.to,progress:"reset"===a?0:1,height:Math.abs(s.getPixelForValue(o.y+r.height)-l),options:n.resolveDataElementOptions(e,a)},a)}n.updateSharedOptions(c,a)}_drawLabels(){const t=this,o=t._ctx,r=t._nodes||new Map,a=t.getDataset(),n=u(a.size),i=e.valueOrDefault(a.borderWidth,1),s=e.valueOrDefault(a.nodeWidth,10),l=a.labels,{xScale:c,yScale:h}=t._cachedMeta;o.save();const d=t.chart.chartArea;for(const t of r.values()){const e=c.getPixelForValue(t.x),r=h.getPixelForValue(t.y),f=Math[n](t.in||t.out,t.out||t.in),u=Math.abs(h.getPixelForValue(t.y+f)-r),x=l&&l[t.key]||t.key;let y=e;o.fillStyle=a.color||"black",o.textBaseline="middle",e<d.width/2?(o.textAlign="left",y+=s+i+4):(o.textAlign="right",y-=i+4),this._drawLabel(x,r,u,o,y)}o.restore()}_drawLabel(t,o,r,a,n){const i=this,s=e.toFont(i.options.font,i.chart.options.font),l=e.isNullOrUndef(t)?[]:i.toTextLines(t),c=l.length,h=o+r/2,d=s.lineHeight,f=e.valueOrDefault(i.options.padding,d/2);if(a.font=s.string,c>1){const t=h-d*c/2+f;for(let e=0;e<c;e++)a.fillText(l[e],n,t+e*d)}else a.fillText(t,n,h)}toTextLines(t){let o,r=[];for(t=[].concat(t);t.length;)o=t.pop(),"string"==typeof o?r.unshift.apply(r,o.split("\n")):Array.isArray(o)?t.push.apply(t,o):e.isNullOrUndef(t)||r.unshift(""+o);return r}_drawNodes(){const t=this,o=t._ctx,r=t._nodes||new Map,a=t.getDataset(),n=u(a.size),{xScale:i,yScale:s}=t._cachedMeta,l=e.valueOrDefault(a.borderWidth,1),c=e.valueOrDefault(a.nodeWidth,10);o.save(),o.strokeStyle=a.borderColor||"black",o.lineWidth=l;for(const t of r.values()){o.fillStyle=t.color;const e=i.getPixelForValue(t.x),r=s.getPixelForValue(t.y),a=Math[n](t.in||t.out,t.out||t.in),h=Math.abs(s.getPixelForValue(t.y+a)-r);l&&o.strokeRect(e,r,c,h),o.fillRect(e,r,c,h)}o.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:a,y2:n,height:i,progress:s}=this,{cp1:l,cp2:c}=y(o,a,r,n),h=this.options;if(0===s)return;let d;t.save(),s<1&&(t.beginPath(),t.rect(o,Math.min(a,n),(r-o)*s+1,Math.abs(n-a)+i+1),t.clip()),"from"===h.colorMode?d=e.color(h.colorFrom).alpha(.5).rgbString():"to"===h.colorMode?d=e.color(h.colorTo).alpha(.5).rgbString():(d=t.createLinearGradient(o,0,r,0),d.addColorStop(0,e.color(h.colorFrom).alpha(.5).rgbString()),d.addColorStop(1,e.color(h.colorTo).alpha(.5).rgbString())),t.fillStyle=d,t.strokeStyle=d,t.lineWidth=.5,t.beginPath(),t.moveTo(o,a),t.bezierCurveTo(l.x,l.y,c.x,c.y,r,n),t.lineTo(r,n+i),t.bezierCurveTo(c.x,c.y+i,l.x,l.y+i,o,a+i),t.lineTo(o,a),t.stroke(),t.closePath(),t.fill(),t.restore()}inRange(t,e,o){const{x:r,y:a,x2:n,y2:i,height:s}=this.getProps(["x","y","x2","y2","height"],o);if(t<r||t>n)return!1;const{cp1:l,cp2:c}=y(r,a,n,i),h=(t-r)/(n-r),d={x:n,y:i},f=p({x:r,y:a},l,h),u=p(l,c,h),x=p(c,d,h),g=p(f,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:a}=this.getProps(["y","y2","height"],e),n=Math.min(o,r),i=Math.max(o,r)+a;return t>=n&&t<=i}getCenterPoint(t){const{x:e,y:o,x2:r,y2:a,height:n}=this.getProps(["x","y","x2","y2","height"],t);return{x:(e+r)/2,y:(o+a+n)/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)}));
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.8.1",
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 -r \"npm:lint-*\"",
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": "^4.26.1",
43
- "@typescript-eslint/parser": "^4.26.1",
44
- "chart.js": "^3.0.0",
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.3.0",
47
- "concurrently": "^6.0.0",
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": "^7.22.0",
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": "^3.7.1",
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.32",
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": "~4.3"
67
+ "typescript": "^4.5.5"
68
68
  },
69
69
  "peerDependencies": {
70
- "chart.js": "^3.0.0"
70
+ "chart.js": "^3.3.0"
71
71
  }
72
72
  }
@@ -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