chartjs-chart-sankey 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.1
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
@@ -61,12 +101,28 @@ const defined = x => x !== undefined;
61
101
  */
62
102
  const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
63
103
 
104
+ let prevCountId = -1;
105
+ function getCountId() {
106
+ prevCountId = prevCountId < 100 ? prevCountId + 1 : 0;
107
+ return prevCountId;
108
+ }
109
+
64
110
  /**
65
111
  * @param {Array<FromToElement>} list
66
112
  * @param {string} prop
67
113
  * @return {number}
68
114
  */
69
- const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
115
+ function nodeCount(list, prop, countId = getCountId()) {
116
+ let count = 0;
117
+ for (const elem of list) {
118
+ if (elem.node._visited === countId) {
119
+ continue;
120
+ }
121
+ elem.node._visited = countId;
122
+ count += elem.node[prop].length + nodeCount(elem.node[prop], prop, countId);
123
+ }
124
+ return count;
125
+ }
70
126
 
71
127
  /**
72
128
  * @param {string} prop
@@ -74,26 +130,21 @@ const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop]
74
130
  */
75
131
  const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
76
132
 
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
133
  /**
84
134
  * @param {SankeyNode} node
85
135
  * @param {number} y
86
136
  * @return {number}
87
137
  */
88
138
  function processFrom(node, y) {
89
- node.from.sort(flowByNodeCount('from')).forEach(flow => {
139
+ node.from.sort(flowByNodeCount('from'));
140
+ for (const flow of node.from) {
90
141
  const n = flow.node;
91
142
  if (!defined(n.y)) {
92
143
  n.y = y;
93
144
  processFrom(n, y);
94
145
  }
95
146
  y = Math.max(n.y + n.out, y);
96
- });
147
+ }
97
148
  return y;
98
149
  }
99
150
 
@@ -103,14 +154,15 @@ function processFrom(node, y) {
103
154
  * @return {number}
104
155
  */
105
156
  function processTo(node, y) {
106
- node.to.sort(flowByNodeCount('to')).forEach(flow => {
157
+ node.to.sort(flowByNodeCount('to'));
158
+ for (const flow of node.to) {
107
159
  const n = flow.node;
108
160
  if (!defined(n.y)) {
109
161
  n.y = y;
110
162
  processTo(n, y);
111
163
  }
112
164
  y = Math.max(n.y + n.in, y);
113
- });
165
+ }
114
166
  return y;
115
167
  }
116
168
 
@@ -182,7 +234,8 @@ function processRest(nodeArray, maxX) {
182
234
  * @return {number}
183
235
  */
184
236
  function calculateY(nodeArray, maxX) {
185
- const start = findLargestNode(nodeArray);
237
+ nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
238
+ const start = nodeArray[0];
186
239
  start.y = 0;
187
240
  const left = processFrom(start, 0);
188
241
  const right = processTo(start, 0);
@@ -222,7 +275,8 @@ function addPadding(nodeArray, padding) {
222
275
  let prev = 0;
223
276
  let maxY = 0;
224
277
  const rows = [];
225
- nodeArray.sort(nodeByXY).forEach(node => {
278
+ nodeArray.sort(nodeByXY);
279
+ for (const node of nodeArray) {
226
280
  if (node.y) {
227
281
  if (node.x === 0) {
228
282
  rows.push(node.y);
@@ -243,7 +297,7 @@ function addPadding(nodeArray, padding) {
243
297
  i++;
244
298
  }
245
299
  maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
246
- });
300
+ }
247
301
  return maxY;
248
302
  }
249
303
 
@@ -365,17 +419,6 @@ function getAddY(arr, key, index) {
365
419
  return 0;
366
420
  }
367
421
 
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
422
  class SankeyController extends DatasetController {
380
423
  /**
381
424
  * @param {ChartMeta<Flow, Element>} meta
@@ -394,7 +437,7 @@ class SankeyController extends DatasetController {
394
437
  const parsed = []; /* Array<SankeyParsedData> */
395
438
  const nodes = me._nodes = buildNodesFromRawData(data);
396
439
  /* getDataset() => SankeyControllerDatasetOptions */
397
- const {priority, size} = me.getDataset();
440
+ const {column, priority, size} = me.getDataset();
398
441
  if (priority) {
399
442
  for (const node of nodes.values()) {
400
443
  if (node.key in priority) {
@@ -402,17 +445,24 @@ class SankeyController extends DatasetController {
402
445
  }
403
446
  }
404
447
  }
448
+ if (column) {
449
+ for (const node of nodes.values()) {
450
+ if (node.key in column) {
451
+ node.column = true;
452
+ node.x = column[node.key];
453
+ }
454
+ }
455
+ }
405
456
 
406
457
  const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
407
458
 
408
459
  me._maxX = maxX;
409
460
  me._maxY = maxY;
410
461
 
411
- /* loop over raw data elements {SankeyDataPoint} */
412
462
  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} */
463
+ const dataPoint = data[i];
464
+ const from = nodes.get(dataPoint.from);
465
+ const to = nodes.get(dataPoint.to);
416
466
  const fromY = from.y + getAddY(from.to, dataPoint.to, i);
417
467
  const toY = to.y + getAddY(to.from, dataPoint.from, i);
418
468
  parsed.push({
@@ -531,7 +581,7 @@ class SankeyController extends DatasetController {
531
581
  _drawLabel(label, y, height, ctx, textX) {
532
582
  const me = this;
533
583
  const font = toFont(me.options.font, me.chart.options.font);
534
- const lines = isNullOrUndef(label) ? [] : me.toTextLines(label);
584
+ const lines = isNullOrUndef(label) ? [] : toTextLines(label);
535
585
  const linesLength = lines.length;
536
586
  const middle = y + height / 2;
537
587
  const textHeight = font.lineHeight;
@@ -549,30 +599,6 @@ class SankeyController extends DatasetController {
549
599
  }
550
600
  }
551
601
 
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
602
  _drawNodes() {
577
603
  const me = this;
578
604
  const ctx = me._ctx;
@@ -752,6 +778,28 @@ const controlPoints = (x, y, x2, y2) => x < x2
752
778
  */
753
779
  const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
754
780
 
781
+ /**
782
+ * @param {CanvasRenderingContext2D} ctx
783
+ * @param {Flow} flow
784
+ */
785
+ function setStyle(ctx, {x, x2, options}) {
786
+ let fill;
787
+
788
+ if (options.colorMode === 'from') {
789
+ fill = color(options.colorFrom).alpha(0.5).rgbString();
790
+ } else if (options.colorMode === 'to') {
791
+ fill = color(options.colorTo).alpha(0.5).rgbString();
792
+ } else {
793
+ fill = ctx.createLinearGradient(x, 0, x2, 0);
794
+ fill.addColorStop(0, color(options.colorFrom).alpha(0.5).rgbString());
795
+ fill.addColorStop(1, color(options.colorTo).alpha(0.5).rgbString());
796
+ }
797
+
798
+ ctx.fillStyle = fill;
799
+ ctx.strokeStyle = fill;
800
+ ctx.lineWidth = 0.5;
801
+ }
802
+
755
803
  class Flow extends Element {
756
804
 
757
805
  /**
@@ -779,7 +827,6 @@ class Flow extends Element {
779
827
  const me = this;
780
828
  const {x, x2, y, y2, height, progress} = me;
781
829
  const {cp1, cp2} = controlPoints(x, y, x2, y2);
782
- const options = me.options;
783
830
 
784
831
  if (progress === 0) {
785
832
  return;
@@ -791,21 +838,7 @@ class Flow extends Element {
791
838
  ctx.clip();
792
839
  }
793
840
 
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;
841
+ setStyle(ctx, me);
809
842
 
810
843
  ctx.beginPath();
811
844
  ctx.moveTo(x, y);
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.8.1
2
+ * chartjs-chart-sankey v0.9.1
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
@@ -64,12 +104,28 @@ const defined = x => x !== undefined;
64
104
  */
65
105
  const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
66
106
 
107
+ let prevCountId = -1;
108
+ function getCountId() {
109
+ prevCountId = prevCountId < 100 ? prevCountId + 1 : 0;
110
+ return prevCountId;
111
+ }
112
+
67
113
  /**
68
114
  * @param {Array<FromToElement>} list
69
115
  * @param {string} prop
70
116
  * @return {number}
71
117
  */
72
- const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
118
+ function nodeCount(list, prop, countId = getCountId()) {
119
+ let count = 0;
120
+ for (const elem of list) {
121
+ if (elem.node._visited === countId) {
122
+ continue;
123
+ }
124
+ elem.node._visited = countId;
125
+ count += elem.node[prop].length + nodeCount(elem.node[prop], prop, countId);
126
+ }
127
+ return count;
128
+ }
73
129
 
74
130
  /**
75
131
  * @param {string} prop
@@ -77,26 +133,21 @@ const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop]
77
133
  */
78
134
  const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
79
135
 
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
136
  /**
87
137
  * @param {SankeyNode} node
88
138
  * @param {number} y
89
139
  * @return {number}
90
140
  */
91
141
  function processFrom(node, y) {
92
- node.from.sort(flowByNodeCount('from')).forEach(flow => {
142
+ node.from.sort(flowByNodeCount('from'));
143
+ for (const flow of node.from) {
93
144
  const n = flow.node;
94
145
  if (!defined(n.y)) {
95
146
  n.y = y;
96
147
  processFrom(n, y);
97
148
  }
98
149
  y = Math.max(n.y + n.out, y);
99
- });
150
+ }
100
151
  return y;
101
152
  }
102
153
 
@@ -106,14 +157,15 @@ function processFrom(node, y) {
106
157
  * @return {number}
107
158
  */
108
159
  function processTo(node, y) {
109
- node.to.sort(flowByNodeCount('to')).forEach(flow => {
160
+ node.to.sort(flowByNodeCount('to'));
161
+ for (const flow of node.to) {
110
162
  const n = flow.node;
111
163
  if (!defined(n.y)) {
112
164
  n.y = y;
113
165
  processTo(n, y);
114
166
  }
115
167
  y = Math.max(n.y + n.in, y);
116
- });
168
+ }
117
169
  return y;
118
170
  }
119
171
 
@@ -185,7 +237,8 @@ function processRest(nodeArray, maxX) {
185
237
  * @return {number}
186
238
  */
187
239
  function calculateY(nodeArray, maxX) {
188
- const start = findLargestNode(nodeArray);
240
+ nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
241
+ const start = nodeArray[0];
189
242
  start.y = 0;
190
243
  const left = processFrom(start, 0);
191
244
  const right = processTo(start, 0);
@@ -225,7 +278,8 @@ function addPadding(nodeArray, padding) {
225
278
  let prev = 0;
226
279
  let maxY = 0;
227
280
  const rows = [];
228
- nodeArray.sort(nodeByXY).forEach(node => {
281
+ nodeArray.sort(nodeByXY);
282
+ for (const node of nodeArray) {
229
283
  if (node.y) {
230
284
  if (node.x === 0) {
231
285
  rows.push(node.y);
@@ -246,7 +300,7 @@ function addPadding(nodeArray, padding) {
246
300
  i++;
247
301
  }
248
302
  maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
249
- });
303
+ }
250
304
  return maxY;
251
305
  }
252
306
 
@@ -368,17 +422,6 @@ function getAddY(arr, key, index) {
368
422
  return 0;
369
423
  }
370
424
 
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
425
  class SankeyController extends chart_js.DatasetController {
383
426
  /**
384
427
  * @param {ChartMeta<Flow, Element>} meta
@@ -397,7 +440,7 @@ class SankeyController extends chart_js.DatasetController {
397
440
  const parsed = []; /* Array<SankeyParsedData> */
398
441
  const nodes = me._nodes = buildNodesFromRawData(data);
399
442
  /* getDataset() => SankeyControllerDatasetOptions */
400
- const {priority, size} = me.getDataset();
443
+ const {column, priority, size} = me.getDataset();
401
444
  if (priority) {
402
445
  for (const node of nodes.values()) {
403
446
  if (node.key in priority) {
@@ -405,17 +448,24 @@ class SankeyController extends chart_js.DatasetController {
405
448
  }
406
449
  }
407
450
  }
451
+ if (column) {
452
+ for (const node of nodes.values()) {
453
+ if (node.key in column) {
454
+ node.column = true;
455
+ node.x = column[node.key];
456
+ }
457
+ }
458
+ }
408
459
 
409
460
  const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
410
461
 
411
462
  me._maxX = maxX;
412
463
  me._maxY = maxY;
413
464
 
414
- /* loop over raw data elements {SankeyDataPoint} */
415
465
  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} */
466
+ const dataPoint = data[i];
467
+ const from = nodes.get(dataPoint.from);
468
+ const to = nodes.get(dataPoint.to);
419
469
  const fromY = from.y + getAddY(from.to, dataPoint.to, i);
420
470
  const toY = to.y + getAddY(to.from, dataPoint.from, i);
421
471
  parsed.push({
@@ -534,7 +584,7 @@ class SankeyController extends chart_js.DatasetController {
534
584
  _drawLabel(label, y, height, ctx, textX) {
535
585
  const me = this;
536
586
  const font = helpers.toFont(me.options.font, me.chart.options.font);
537
- const lines = helpers.isNullOrUndef(label) ? [] : me.toTextLines(label);
587
+ const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
538
588
  const linesLength = lines.length;
539
589
  const middle = y + height / 2;
540
590
  const textHeight = font.lineHeight;
@@ -552,30 +602,6 @@ class SankeyController extends chart_js.DatasetController {
552
602
  }
553
603
  }
554
604
 
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
605
  _drawNodes() {
580
606
  const me = this;
581
607
  const ctx = me._ctx;
@@ -755,6 +781,28 @@ const controlPoints = (x, y, x2, y2) => x < x2
755
781
  */
756
782
  const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
757
783
 
784
+ /**
785
+ * @param {CanvasRenderingContext2D} ctx
786
+ * @param {Flow} flow
787
+ */
788
+ function setStyle(ctx, {x, x2, options}) {
789
+ let fill;
790
+
791
+ if (options.colorMode === 'from') {
792
+ fill = helpers.color(options.colorFrom).alpha(0.5).rgbString();
793
+ } else if (options.colorMode === 'to') {
794
+ fill = helpers.color(options.colorTo).alpha(0.5).rgbString();
795
+ } else {
796
+ fill = ctx.createLinearGradient(x, 0, x2, 0);
797
+ fill.addColorStop(0, helpers.color(options.colorFrom).alpha(0.5).rgbString());
798
+ fill.addColorStop(1, helpers.color(options.colorTo).alpha(0.5).rgbString());
799
+ }
800
+
801
+ ctx.fillStyle = fill;
802
+ ctx.strokeStyle = fill;
803
+ ctx.lineWidth = 0.5;
804
+ }
805
+
758
806
  class Flow extends chart_js.Element {
759
807
 
760
808
  /**
@@ -782,7 +830,6 @@ class Flow extends chart_js.Element {
782
830
  const me = this;
783
831
  const {x, x2, y, y2, height, progress} = me;
784
832
  const {cp1, cp2} = controlPoints(x, y, x2, y2);
785
- const options = me.options;
786
833
 
787
834
  if (progress === 0) {
788
835
  return;
@@ -794,21 +841,7 @@ class Flow extends chart_js.Element {
794
841
  ctx.clip();
795
842
  }
796
843
 
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;
844
+ setStyle(ctx, me);
812
845
 
813
846
  ctx.beginPath();
814
847
  ctx.moveTo(x, y);
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.8.1
2
+ * chartjs-chart-sankey v0.9.1
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;let i=-1;function s(t,e,o=function(){return i=i<100?i+1:0,i}()){let r=0;for(const n of t)n.node._visited!==o&&(n.node._visited=o,r+=n.node[e].length+s(n.node[e],e,o));return r}const l=t=>(e,o)=>s(e.node[t],t)-s(o.node[t],t)||e.node[t].length-o.node[t].length;function c(t,e){t.from.sort(l("from"));for(const o of t.from){const t=o.node;r(t.y)||(t.y=e,c(t,e)),e=Math.max(t.y+t.out,e)}return e}function f(t,e){t.to.sort(l("to"));for(const o of t.to){const t=o.node;r(t.y)||(t.y=e,f(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 d(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=c(o,0),a=f(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 l=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 l>=d?(a.forEach((t=>{l=h(t,l),l=Math.max(l+t.out,f(t,l))})),i.forEach((t=>{d=h(t,d),d=Math.max(d+t.in,f(t,d))}))):(i.forEach((t=>{d=h(t,d),d=Math.max(d+t.in,f(t,d))})),a.forEach((t=>{l=h(t,l),l=Math.max(l+t.out,f(t,l))}))),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,c(e,o)),o=Math.max(o+e.out,f(e,o)),u=Math.max(u,o)})),Math.max(l,d,u)}(t,e);return Math.max(n,a,i)}function u(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):d(s,l),f=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:f}}function x(t,e,o){for(const r of t)if(r.key===e&&r.index===o)return r.addY;return 0}class y 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:f,priority:h,size:d}=a.getDataset();if(h)for(const t of c.values())t.key in h&&(t.priority=h[t.key]);if(f)for(const t of c.values())t.key in f&&(t.column=!0,t.x=f[t.key]);const{maxX:y,maxY:p}=u(c,e,!!h,o(d));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+x(r.to,o.to,t),f=n.y+x(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(f,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),f=a.getDataset(),h=e.valueOrDefault(f.borderWidth,1)/2+.5,d=e.valueOrDefault(f.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+h,y:l,x2:i.getPixelForValue(r.x)-h,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:f,yScale:h}=t._cachedMeta;r.save();const d=t.chart.chartArea;for(const t of n.values()){const e=f.getPixelForValue(t.x),o=h.getPixelForValue(t.y),n=Math[i](t.in||t.out,t.out||t.in),u=Math.abs(h.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,f=o+r/2,h=s.lineHeight,d=e.valueOrDefault(i.options.padding,h/2);if(n.font=s.string,c>1){const t=f-h*c/2+d;for(let e=0;e<c;e++)n.fillText(l[e],a,t+e*h)}else n.fillText(t,a,f)}_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),f=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,f,a),r.fillRect(e,o,f,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()}}y.id="sankey",y.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"}}}}},y.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 p=(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}},g=(t,e,o)=>({x:t.x+o*(e.x-t.x),y:t.y+o*(e.y-t.y)});class m 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}=p(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}=p(r,n,a,i),f=(t-r)/(a-r),h={x:a,y:i},d=g({x:r,y:n},l,f),u=g(l,c,f),x=g(c,h,f),y=g(d,u,f),m=g(u,x,f),M=g(y,m,f).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}}m.id="flow",m.defaults={colorFrom:"red",colorTo:"green",colorMode:"gradient"},t.Chart.register(y,m)}));
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.1",
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,34 +39,34 @@
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",
59
59
  "karma-firefox-launcher": "^2.1.0",
60
60
  "karma-jasmine": "^4.0.1",
61
61
  "karma-jasmine-html-reporter": "^1.5.4",
62
- "karma-rollup-preprocessor": "^7.0.7",
63
- "karma-spec-reporter": "0.0.32",
62
+ "karma-rollup-preprocessor": "7.0.7",
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
  }
@@ -3,8 +3,10 @@ import {
3
3
  ChartComponent,
4
4
  DatasetController,
5
5
  Element,
6
+ VisualElement,
6
7
  FontSpec
7
8
  } from 'chart.js';
9
+ import { AnyObject } from 'chart.js/types/basic';
8
10
 
9
11
  declare module 'chart.js' {
10
12
 
@@ -24,6 +26,7 @@ declare module 'chart.js' {
24
26
  colorMode: 'gradient' | 'from' | 'to';
25
27
  /* Map<node.key, priority_value> */
26
28
  priority?: Record<string, number>
29
+ column?: Record<string, number>
27
30
  /* Map<node.key, label> */
28
31
  labels?: Record<string, string>
29
32
 
@@ -52,6 +55,8 @@ declare module 'chart.js' {
52
55
  out: number
53
56
  from: Array<FromToElement>
54
57
  to: Array<FromToElement>
58
+ /* true if x is defined by SankeyControllerDatasetOptions.column map */
59
+ column?: boolean
55
60
  /* priority extracted from the SankeyControllerDatasetOptions.priority map */
56
61
  priority?: number
57
62
  y?: number
@@ -75,28 +80,28 @@ declare module 'chart.js' {
75
80
  datasetOptions: SankeyControllerDatasetOptions;
76
81
  defaultDataPoint: SankeyDataPoint;
77
82
  parsedDataType: SankeyParsedData;
78
- metaExtensions:{}
83
+ metaExtensions: AnyObject
79
84
  /* TODO: define sankey chart options */
80
- chartOptions: any;
85
+ chartOptions: AnyObject;
81
86
  scales: keyof CartesianScaleTypeRegistry;
82
87
  };
83
88
  }
84
89
  }
85
90
 
86
- type FlowOptions = {
91
+ export interface FlowOptions {
87
92
  colorMode: 'gradient' | 'from' | 'to';
88
93
  colorFrom: string
89
94
  colorTo: string
90
95
  }
91
96
 
92
- type FlowConfig = {
97
+ export interface FlowConfig {
93
98
  x: number;
94
99
  y: number;
95
100
  x2: number;
96
101
  y2: number;
97
102
  height: number;
98
103
  options: FlowOptions
99
- };
104
+ }
100
105
 
101
106
  export type SankeyController = DatasetController
102
107
  export const SankeyController: ChartComponent & {
@@ -104,7 +109,11 @@ export const SankeyController: ChartComponent & {
104
109
  new(chart: Chart, datasetIndex: number): SankeyController;
105
110
  };
106
111
 
107
- export type Flow = Element<FlowConfig, FlowOptions>
112
+ export interface Flow<
113
+ T extends FlowConfig = FlowConfig,
114
+ O extends FlowOptions = FlowOptions
115
+ > extends Element<T, O>, VisualElement {}
116
+
108
117
  export const Flow: ChartComponent & {
109
118
  prototype: Flow;
110
119
  new(cfg: FlowConfig): Flow;