chartjs-chart-sankey 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.8.0
2
+ * chartjs-chart-sankey v0.8.1
3
3
  * https://github.com/kurkle/chartjs-chart-sankey#readme
4
- * (c) 2021 Jukka Kurkela
4
+ * (c) 2022 Jukka Kurkela
5
5
  * Released under the MIT license
6
6
  */
7
7
  import { DatasetController, Element } from 'chart.js';
8
8
  import { valueOrDefault, toFont, isNullOrUndef, color } from 'chart.js/helpers';
9
9
 
10
10
  /**
11
- * @param nodes {Map<string, SankeyNode>}
12
- * @param data {Array<SankeyDataPoint>}
11
+ * @param {Map<string, SankeyNode>} nodes
12
+ * @param {Array<SankeyDataPoint>} data
13
13
  * @return {number}
14
14
  */
15
15
  function calculateX(nodes, data) {
@@ -39,8 +39,8 @@ function calculateX(nodes, data) {
39
39
  }
40
40
 
41
41
  /**
42
- * @param keys {Array<string>}
43
- * @param to {Set<string>}
42
+ * @param {Array<string>} keys
43
+ * @param {Set<string>} to
44
44
  * @return {Array<string>}
45
45
  */
46
46
  function nextColumn(keys, to) {
@@ -55,34 +55,34 @@ function nextColumn(keys, to) {
55
55
  const defined = x => x !== undefined;
56
56
 
57
57
  /**
58
- * @param a {SankeyNode}
59
- * @param b {SankeyNode}
58
+ * @param {SankeyNode} a
59
+ * @param {SankeyNode} b
60
60
  * @return {number}
61
61
  */
62
62
  const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
63
63
 
64
64
  /**
65
- * @param list {Array<FromToElement>}
66
- * @param prop {string}
65
+ * @param {Array<FromToElement>} list
66
+ * @param {string} prop
67
67
  * @return {number}
68
68
  */
69
69
  const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
70
70
 
71
71
  /**
72
- * @param prop {string}
72
+ * @param {string} prop
73
73
  * @return {function(FromToElement, FromToElement): number}
74
74
  */
75
- const flowByNodeCount = (prop) => (a, b) => nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop);
75
+ const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
76
76
 
77
77
  /**
78
- * @param nodeArray {Array<SankeyNode>}
78
+ * @param {Array<SankeyNode>} nodeArray
79
79
  * @return {SankeyNode}
80
80
  */
81
81
  const findLargestNode = (nodeArray) => nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out))[0];
82
82
 
83
83
  /**
84
- * @param node {SankeyNode}
85
- * @param y {number}
84
+ * @param {SankeyNode} node
85
+ * @param {number} y
86
86
  * @return {number}
87
87
  */
88
88
  function processFrom(node, y) {
@@ -90,15 +90,16 @@ function processFrom(node, y) {
90
90
  const n = flow.node;
91
91
  if (!defined(n.y)) {
92
92
  n.y = y;
93
- y = Math.max(y + n.out, processFrom(n, y));
93
+ processFrom(n, y);
94
94
  }
95
+ y = Math.max(n.y + n.out, y);
95
96
  });
96
97
  return y;
97
98
  }
98
99
 
99
100
  /**
100
- * @param node {SankeyNode}
101
- * @param y {number}
101
+ * @param {SankeyNode} node
102
+ * @param {number} y
102
103
  * @return {number}
103
104
  */
104
105
  function processTo(node, y) {
@@ -106,15 +107,16 @@ function processTo(node, y) {
106
107
  const n = flow.node;
107
108
  if (!defined(n.y)) {
108
109
  n.y = y;
109
- y = Math.max(y + n.in, processTo(n, y));
110
+ processTo(n, y);
110
111
  }
112
+ y = Math.max(n.y + n.in, y);
111
113
  });
112
114
  return y;
113
115
  }
114
116
 
115
117
  /**
116
- * @param node {SankeyNode}
117
- * @param value {number}
118
+ * @param {SankeyNode} node
119
+ * @param {number} value
118
120
  * @return {number}
119
121
  */
120
122
  function setOrGetY(node, value) {
@@ -126,8 +128,8 @@ function setOrGetY(node, value) {
126
128
  }
127
129
 
128
130
  /**
129
- * @param nodeArray {Array<SankeyNode>}
130
- * @param maxX {number}
131
+ * @param {Array<SankeyNode>} nodeArray
132
+ * @param {number} maxX
131
133
  * @return {number}
132
134
  */
133
135
  function processRest(nodeArray, maxX) {
@@ -135,9 +137,11 @@ function processRest(nodeArray, maxX) {
135
137
  const rightNodes = nodeArray.filter(node => node.x === maxX);
136
138
  const leftToDo = leftNodes.filter(node => !defined(node.y));
137
139
  const rightToDo = rightNodes.filter(node => !defined(node.y));
140
+ const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
138
141
 
139
142
  let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
140
143
  let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
144
+ let centerY = 0;
141
145
 
142
146
  if (leftY >= rightY) {
143
147
  leftToDo.forEach(node => {
@@ -160,13 +164,21 @@ function processRest(nodeArray, maxX) {
160
164
  leftY = Math.max(leftY + node.out, processTo(node, leftY));
161
165
  });
162
166
  }
167
+ centerToDo.forEach(node => {
168
+ let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
169
+ .reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
170
+ y = setOrGetY(node, y);
171
+ y = Math.max(y + node.in, processFrom(node, y));
172
+ y = Math.max(y + node.out, processTo(node, y));
173
+ centerY = Math.max(centerY, y);
174
+ });
163
175
 
164
- return Math.max(leftY, rightY);
176
+ return Math.max(leftY, rightY, centerY);
165
177
  }
166
178
 
167
179
  /**
168
- * @param nodeArray {Array<SankeyNode>}
169
- * @param maxX {number}
180
+ * @param {Array<SankeyNode>} nodeArray
181
+ * @param {number} maxX
170
182
  * @return {number}
171
183
  */
172
184
  function calculateY(nodeArray, maxX) {
@@ -179,15 +191,17 @@ function calculateY(nodeArray, maxX) {
179
191
  }
180
192
 
181
193
  /**
182
- * @param nodeArray {Array<SankeyNode>}
183
- * @param maxX {number}
194
+ * @param {Array<SankeyNode>} nodeArray
195
+ * @param {number} maxX
184
196
  * @return {number}
185
197
  */
186
198
  function calculateYUsingPriority(nodeArray, maxX) {
187
199
  let maxY = 0;
200
+ let nextYStart = 0;
188
201
  for (let x = 0; x <= maxX; x++) {
189
- let y = 0;
202
+ let y = nextYStart;
190
203
  const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
204
+ nextYStart = nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0;
191
205
  for (const node of nodes) {
192
206
  node.y = y;
193
207
  y += Math.max(node.out, node.in);
@@ -198,8 +212,8 @@ function calculateYUsingPriority(nodeArray, maxX) {
198
212
  }
199
213
 
200
214
  /**
201
- * @param nodeArray {Array<SankeyNode>}
202
- * @param padding {number}
215
+ * @param {Array<SankeyNode>} nodeArray
216
+ * @param {number} padding
203
217
  * @return {number}
204
218
  */
205
219
  function addPadding(nodeArray, padding) {
@@ -234,8 +248,8 @@ function addPadding(nodeArray, padding) {
234
248
  }
235
249
 
236
250
  /**
237
- * @param nodeArray {Array<SankeyNode>}
238
- * @param size {'min' | 'max'}
251
+ * @param {Array<SankeyNode>} nodeArray
252
+ * @param {'min' | 'max'} size
239
253
  */
240
254
  function sortFlows(nodeArray, size) {
241
255
  nodeArray.forEach((node) => {
@@ -266,10 +280,10 @@ function sortFlows(nodeArray, size) {
266
280
  }
267
281
 
268
282
  /**
269
- * @param nodes {Map<string, SankeyNode>}
270
- * @param data {Array<SankeyDataPoint>}
271
- * @param priority {boolean}
272
- * @param size {'min' | 'max'}
283
+ * @param {Map<string, SankeyNode>} nodes
284
+ * @param {Array<SankeyDataPoint>} data
285
+ * @param {boolean} priority
286
+ * @param {'min' | 'max'} size
273
287
  * @return {{maxY: number, maxX: number}}
274
288
  */
275
289
  function layout(nodes, data, priority, size) {
@@ -283,7 +297,7 @@ function layout(nodes, data, priority, size) {
283
297
  }
284
298
 
285
299
  /**
286
- * @param data {Array<SankeyDataPoint>} Array of raw data elements
300
+ * @param {Array<SankeyDataPoint>} data Array of raw data elements
287
301
  * @return {Map<string, SankeyNode>}
288
302
  */
289
303
  function buildNodesFromRawData(data) {
@@ -297,25 +311,25 @@ function buildNodesFromRawData(data) {
297
311
  in: 0,
298
312
  out: flow,
299
313
  from: [],
300
- to: [{key: to, flow: flow}],
314
+ to: [{key: to, flow: flow, index: i}],
301
315
  });
302
316
  } else {
303
317
  const node = nodes.get(from);
304
318
  node.out += flow;
305
- node.to.push({key: to, flow: flow});
319
+ node.to.push({key: to, flow: flow, index: i});
306
320
  }
307
321
  if (!nodes.has(to)) {
308
322
  nodes.set(to, {
309
323
  key: to,
310
324
  in: flow,
311
325
  out: 0,
312
- from: [{key: from, flow: flow}],
326
+ from: [{key: from, flow: flow, index: i}],
313
327
  to: [],
314
328
  });
315
329
  } else {
316
330
  const node = nodes.get(to);
317
331
  node.in += flow;
318
- node.from.push({key: from, flow: flow});
332
+ node.from.push({key: from, flow: flow, index: i});
319
333
  }
320
334
  }
321
335
 
@@ -337,21 +351,22 @@ function buildNodesFromRawData(data) {
337
351
  }
338
352
 
339
353
  /**
340
- * @param arr {Array<FromToElement>}
341
- * @param key {string}
354
+ * @param {Array<FromToElement>} arr
355
+ * @param {string} key
356
+ * @param {number} index
342
357
  * @return {number}
343
358
  */
344
- function getAddY(arr, key) {
345
- for (let i = 0; i < arr.length; i++) {
346
- if (arr[i].key === key) {
347
- return arr[i].addY;
359
+ function getAddY(arr, key, index) {
360
+ for (const item of arr) {
361
+ if (item.key === key && item.index === index) {
362
+ return item.addY;
348
363
  }
349
364
  }
350
365
  return 0;
351
366
  }
352
367
 
353
368
  /**
354
- * @param size {any}
369
+ * @param {any} size
355
370
  * @return {'min' | 'max'}
356
371
  */
357
372
  function validateSizeValue(size) {
@@ -363,10 +378,10 @@ function validateSizeValue(size) {
363
378
 
364
379
  class SankeyController extends DatasetController {
365
380
  /**
366
- * @param meta {ChartMeta<Flow, Element>}
367
- * @param data {Array<SankeyDataPoint>} Array of original data elements
368
- * @param start {number}
369
- * @param count {number}
381
+ * @param {ChartMeta<Flow, Element>} meta
382
+ * @param {Array<SankeyDataPoint>} data Array of original data elements
383
+ * @param {number} start
384
+ * @param {number} count
370
385
  * @return {Array<SankeyParsedData>}
371
386
  */
372
387
  parseObjectData(meta, data, start, count) {
@@ -398,8 +413,8 @@ class SankeyController extends DatasetController {
398
413
  const dataPoint = data[i]; /* {SankeyDataPoint} */
399
414
  const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
400
415
  const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
401
- const fromY = from.y + getAddY(from.to, dataPoint.to);
402
- const toY = to.y + getAddY(to.from, dataPoint.from);
416
+ const fromY = from.y + getAddY(from.to, dataPoint.to, i);
417
+ const toY = to.y + getAddY(to.from, dataPoint.from, i);
403
418
  parsed.push({
404
419
  x: xScale.parse(from.x, i),
405
420
  y: yScale.parse(fromY, i),
@@ -431,10 +446,10 @@ class SankeyController extends DatasetController {
431
446
  }
432
447
 
433
448
  /**
434
- * @param elems {Array<Flow>}
435
- * @param start {number}
436
- * @param count {number}
437
- * @param mode {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"}
449
+ * @param {Array<Flow>} elems
450
+ * @param {number} start
451
+ * @param {number} count
452
+ * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
438
453
  */
439
454
  updateElements(elems, start, count, mode) {
440
455
  const me = this;
@@ -506,11 +521,11 @@ class SankeyController extends DatasetController {
506
521
  }
507
522
 
508
523
  /**
509
- * @param label {string}
510
- * @param y {number}
511
- * @param height {number}
512
- * @param ctx {CanvasRenderingContext2D}
513
- * @param textX {number}
524
+ * @param {string} label
525
+ * @param {number} y
526
+ * @param {number} height
527
+ * @param {CanvasRenderingContext2D} ctx
528
+ * @param {number} textX
514
529
  * @private
515
530
  */
516
531
  _drawLabel(label, y, height, ctx, textX) {
@@ -535,7 +550,7 @@ class SankeyController extends DatasetController {
535
550
  }
536
551
 
537
552
  /**
538
- * @param inputs {string | Array<string>}
553
+ * @param {string | Array<string>} inputs
539
554
  * @return {Array<string>}
540
555
  * @todo move this in Chart.helpers.toTextLines
541
556
  */
@@ -615,6 +630,7 @@ class SankeyController extends DatasetController {
615
630
  }
616
631
 
617
632
  SankeyController.id = 'sankey';
633
+
618
634
  SankeyController.defaults = {
619
635
  dataElementType: 'flow',
620
636
  animations: {
@@ -653,6 +669,7 @@ SankeyController.defaults = {
653
669
  }
654
670
  }
655
671
  };
672
+
656
673
  SankeyController.overrides = {
657
674
  interaction: {
658
675
  mode: 'nearest',
@@ -710,10 +727,10 @@ SankeyController.overrides = {
710
727
  * @typedef {{x: number, y: number}} ControlPoint
711
728
  * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
712
729
  *
713
- * @param x {number}
714
- * @param y {number}
715
- * @param x2 {number}
716
- * @param y2 {number}
730
+ * @param {number} x
731
+ * @param {number} y
732
+ * @param {number} x2
733
+ * @param {number} y2
717
734
  * @return {ControlPoints}
718
735
  */
719
736
  const controlPoints = (x, y, x2, y2) => x < x2
@@ -728,9 +745,9 @@ const controlPoints = (x, y, x2, y2) => x < x2
728
745
 
729
746
  /**
730
747
  *
731
- * @param p1 {ControlPoint}
732
- * @param p2 {ControlPoint}
733
- * @param t {number}
748
+ * @param {ControlPoint} p1
749
+ * @param {ControlPoint} p2
750
+ * @param {number} t
734
751
  * @return {ControlPoint}
735
752
  */
736
753
  const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
@@ -738,7 +755,7 @@ const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t *
738
755
  class Flow extends Element {
739
756
 
740
757
  /**
741
- * @param cfg {FlowConfig}
758
+ * @param {FlowConfig} cfg
742
759
  */
743
760
  constructor(cfg) {
744
761
  super();
@@ -756,7 +773,7 @@ class Flow extends Element {
756
773
  }
757
774
 
758
775
  /**
759
- * @param ctx {CanvasRenderingContext2D}
776
+ * @param {CanvasRenderingContext2D} ctx
760
777
  */
761
778
  draw(ctx) {
762
779
  const me = this;
@@ -805,9 +822,9 @@ class Flow extends Element {
805
822
  }
806
823
 
807
824
  /**
808
- * @param mouseX {number}
809
- * @param mouseY {number}
810
- * @param useFinalPosition {boolean}
825
+ * @param {number} mouseX
826
+ * @param {number} mouseY
827
+ * @param {boolean} useFinalPosition
811
828
  * @return {boolean}
812
829
  */
813
830
  inRange(mouseX, mouseY, useFinalPosition) {
@@ -829,8 +846,8 @@ class Flow extends Element {
829
846
  }
830
847
 
831
848
  /**
832
- * @param mouseX {number}
833
- * @param useFinalPosition {boolean}
849
+ * @param {number} mouseX
850
+ * @param {boolean} useFinalPosition
834
851
  * @return {boolean}
835
852
  */
836
853
  inXRange(mouseX, useFinalPosition) {
@@ -839,8 +856,8 @@ class Flow extends Element {
839
856
  }
840
857
 
841
858
  /**
842
- * @param mouseY {number}
843
- * @param useFinalPosition {boolean}
859
+ * @param {number} mouseY
860
+ * @param {boolean} useFinalPosition
844
861
  * @return {boolean}
845
862
  */
846
863
  inYRange(mouseY, useFinalPosition) {
@@ -851,7 +868,7 @@ class Flow extends Element {
851
868
  }
852
869
 
853
870
  /**
854
- * @param useFinalPosition {boolean}
871
+ * @param {boolean} useFinalPosition
855
872
  * @return {{x: number, y:number}}
856
873
  */
857
874
  getCenterPoint(useFinalPosition) {
@@ -867,7 +884,7 @@ class Flow extends Element {
867
884
  }
868
885
 
869
886
  /**
870
- * @param axis {"x" | "y"}
887
+ * @param {"x" | "y"} axis
871
888
  * @return {number}
872
889
  */
873
890
  getRange(axis) {
@@ -1,18 +1,18 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.8.0
2
+ * chartjs-chart-sankey v0.8.1
3
3
  * https://github.com/kurkle/chartjs-chart-sankey#readme
4
- * (c) 2021 Jukka Kurkela
4
+ * (c) 2022 Jukka Kurkela
5
5
  * Released under the MIT license
6
6
  */
7
7
  (function (global, factory) {
8
8
  typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js'), require('chart.js/helpers')) :
9
9
  typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) :
10
10
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
11
- }(this, (function (chart_js, helpers) { 'use strict';
11
+ })(this, (function (chart_js, helpers) { 'use strict';
12
12
 
13
13
  /**
14
- * @param nodes {Map<string, SankeyNode>}
15
- * @param data {Array<SankeyDataPoint>}
14
+ * @param {Map<string, SankeyNode>} nodes
15
+ * @param {Array<SankeyDataPoint>} data
16
16
  * @return {number}
17
17
  */
18
18
  function calculateX(nodes, data) {
@@ -42,8 +42,8 @@ function calculateX(nodes, data) {
42
42
  }
43
43
 
44
44
  /**
45
- * @param keys {Array<string>}
46
- * @param to {Set<string>}
45
+ * @param {Array<string>} keys
46
+ * @param {Set<string>} to
47
47
  * @return {Array<string>}
48
48
  */
49
49
  function nextColumn(keys, to) {
@@ -58,34 +58,34 @@ function nextColumn(keys, to) {
58
58
  const defined = x => x !== undefined;
59
59
 
60
60
  /**
61
- * @param a {SankeyNode}
62
- * @param b {SankeyNode}
61
+ * @param {SankeyNode} a
62
+ * @param {SankeyNode} b
63
63
  * @return {number}
64
64
  */
65
65
  const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
66
66
 
67
67
  /**
68
- * @param list {Array<FromToElement>}
69
- * @param prop {string}
68
+ * @param {Array<FromToElement>} list
69
+ * @param {string} prop
70
70
  * @return {number}
71
71
  */
72
72
  const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
73
73
 
74
74
  /**
75
- * @param prop {string}
75
+ * @param {string} prop
76
76
  * @return {function(FromToElement, FromToElement): number}
77
77
  */
78
- const flowByNodeCount = (prop) => (a, b) => nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop);
78
+ const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
79
79
 
80
80
  /**
81
- * @param nodeArray {Array<SankeyNode>}
81
+ * @param {Array<SankeyNode>} nodeArray
82
82
  * @return {SankeyNode}
83
83
  */
84
84
  const findLargestNode = (nodeArray) => nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out))[0];
85
85
 
86
86
  /**
87
- * @param node {SankeyNode}
88
- * @param y {number}
87
+ * @param {SankeyNode} node
88
+ * @param {number} y
89
89
  * @return {number}
90
90
  */
91
91
  function processFrom(node, y) {
@@ -93,15 +93,16 @@ function processFrom(node, y) {
93
93
  const n = flow.node;
94
94
  if (!defined(n.y)) {
95
95
  n.y = y;
96
- y = Math.max(y + n.out, processFrom(n, y));
96
+ processFrom(n, y);
97
97
  }
98
+ y = Math.max(n.y + n.out, y);
98
99
  });
99
100
  return y;
100
101
  }
101
102
 
102
103
  /**
103
- * @param node {SankeyNode}
104
- * @param y {number}
104
+ * @param {SankeyNode} node
105
+ * @param {number} y
105
106
  * @return {number}
106
107
  */
107
108
  function processTo(node, y) {
@@ -109,15 +110,16 @@ function processTo(node, y) {
109
110
  const n = flow.node;
110
111
  if (!defined(n.y)) {
111
112
  n.y = y;
112
- y = Math.max(y + n.in, processTo(n, y));
113
+ processTo(n, y);
113
114
  }
115
+ y = Math.max(n.y + n.in, y);
114
116
  });
115
117
  return y;
116
118
  }
117
119
 
118
120
  /**
119
- * @param node {SankeyNode}
120
- * @param value {number}
121
+ * @param {SankeyNode} node
122
+ * @param {number} value
121
123
  * @return {number}
122
124
  */
123
125
  function setOrGetY(node, value) {
@@ -129,8 +131,8 @@ function setOrGetY(node, value) {
129
131
  }
130
132
 
131
133
  /**
132
- * @param nodeArray {Array<SankeyNode>}
133
- * @param maxX {number}
134
+ * @param {Array<SankeyNode>} nodeArray
135
+ * @param {number} maxX
134
136
  * @return {number}
135
137
  */
136
138
  function processRest(nodeArray, maxX) {
@@ -138,9 +140,11 @@ function processRest(nodeArray, maxX) {
138
140
  const rightNodes = nodeArray.filter(node => node.x === maxX);
139
141
  const leftToDo = leftNodes.filter(node => !defined(node.y));
140
142
  const rightToDo = rightNodes.filter(node => !defined(node.y));
143
+ const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
141
144
 
142
145
  let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
143
146
  let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
147
+ let centerY = 0;
144
148
 
145
149
  if (leftY >= rightY) {
146
150
  leftToDo.forEach(node => {
@@ -163,13 +167,21 @@ function processRest(nodeArray, maxX) {
163
167
  leftY = Math.max(leftY + node.out, processTo(node, leftY));
164
168
  });
165
169
  }
170
+ centerToDo.forEach(node => {
171
+ let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
172
+ .reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
173
+ y = setOrGetY(node, y);
174
+ y = Math.max(y + node.in, processFrom(node, y));
175
+ y = Math.max(y + node.out, processTo(node, y));
176
+ centerY = Math.max(centerY, y);
177
+ });
166
178
 
167
- return Math.max(leftY, rightY);
179
+ return Math.max(leftY, rightY, centerY);
168
180
  }
169
181
 
170
182
  /**
171
- * @param nodeArray {Array<SankeyNode>}
172
- * @param maxX {number}
183
+ * @param {Array<SankeyNode>} nodeArray
184
+ * @param {number} maxX
173
185
  * @return {number}
174
186
  */
175
187
  function calculateY(nodeArray, maxX) {
@@ -182,15 +194,17 @@ function calculateY(nodeArray, maxX) {
182
194
  }
183
195
 
184
196
  /**
185
- * @param nodeArray {Array<SankeyNode>}
186
- * @param maxX {number}
197
+ * @param {Array<SankeyNode>} nodeArray
198
+ * @param {number} maxX
187
199
  * @return {number}
188
200
  */
189
201
  function calculateYUsingPriority(nodeArray, maxX) {
190
202
  let maxY = 0;
203
+ let nextYStart = 0;
191
204
  for (let x = 0; x <= maxX; x++) {
192
- let y = 0;
205
+ let y = nextYStart;
193
206
  const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
207
+ nextYStart = nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0;
194
208
  for (const node of nodes) {
195
209
  node.y = y;
196
210
  y += Math.max(node.out, node.in);
@@ -201,8 +215,8 @@ function calculateYUsingPriority(nodeArray, maxX) {
201
215
  }
202
216
 
203
217
  /**
204
- * @param nodeArray {Array<SankeyNode>}
205
- * @param padding {number}
218
+ * @param {Array<SankeyNode>} nodeArray
219
+ * @param {number} padding
206
220
  * @return {number}
207
221
  */
208
222
  function addPadding(nodeArray, padding) {
@@ -237,8 +251,8 @@ function addPadding(nodeArray, padding) {
237
251
  }
238
252
 
239
253
  /**
240
- * @param nodeArray {Array<SankeyNode>}
241
- * @param size {'min' | 'max'}
254
+ * @param {Array<SankeyNode>} nodeArray
255
+ * @param {'min' | 'max'} size
242
256
  */
243
257
  function sortFlows(nodeArray, size) {
244
258
  nodeArray.forEach((node) => {
@@ -269,10 +283,10 @@ function sortFlows(nodeArray, size) {
269
283
  }
270
284
 
271
285
  /**
272
- * @param nodes {Map<string, SankeyNode>}
273
- * @param data {Array<SankeyDataPoint>}
274
- * @param priority {boolean}
275
- * @param size {'min' | 'max'}
286
+ * @param {Map<string, SankeyNode>} nodes
287
+ * @param {Array<SankeyDataPoint>} data
288
+ * @param {boolean} priority
289
+ * @param {'min' | 'max'} size
276
290
  * @return {{maxY: number, maxX: number}}
277
291
  */
278
292
  function layout(nodes, data, priority, size) {
@@ -286,7 +300,7 @@ function layout(nodes, data, priority, size) {
286
300
  }
287
301
 
288
302
  /**
289
- * @param data {Array<SankeyDataPoint>} Array of raw data elements
303
+ * @param {Array<SankeyDataPoint>} data Array of raw data elements
290
304
  * @return {Map<string, SankeyNode>}
291
305
  */
292
306
  function buildNodesFromRawData(data) {
@@ -300,25 +314,25 @@ function buildNodesFromRawData(data) {
300
314
  in: 0,
301
315
  out: flow,
302
316
  from: [],
303
- to: [{key: to, flow: flow}],
317
+ to: [{key: to, flow: flow, index: i}],
304
318
  });
305
319
  } else {
306
320
  const node = nodes.get(from);
307
321
  node.out += flow;
308
- node.to.push({key: to, flow: flow});
322
+ node.to.push({key: to, flow: flow, index: i});
309
323
  }
310
324
  if (!nodes.has(to)) {
311
325
  nodes.set(to, {
312
326
  key: to,
313
327
  in: flow,
314
328
  out: 0,
315
- from: [{key: from, flow: flow}],
329
+ from: [{key: from, flow: flow, index: i}],
316
330
  to: [],
317
331
  });
318
332
  } else {
319
333
  const node = nodes.get(to);
320
334
  node.in += flow;
321
- node.from.push({key: from, flow: flow});
335
+ node.from.push({key: from, flow: flow, index: i});
322
336
  }
323
337
  }
324
338
 
@@ -340,21 +354,22 @@ function buildNodesFromRawData(data) {
340
354
  }
341
355
 
342
356
  /**
343
- * @param arr {Array<FromToElement>}
344
- * @param key {string}
357
+ * @param {Array<FromToElement>} arr
358
+ * @param {string} key
359
+ * @param {number} index
345
360
  * @return {number}
346
361
  */
347
- function getAddY(arr, key) {
348
- for (let i = 0; i < arr.length; i++) {
349
- if (arr[i].key === key) {
350
- return arr[i].addY;
362
+ function getAddY(arr, key, index) {
363
+ for (const item of arr) {
364
+ if (item.key === key && item.index === index) {
365
+ return item.addY;
351
366
  }
352
367
  }
353
368
  return 0;
354
369
  }
355
370
 
356
371
  /**
357
- * @param size {any}
372
+ * @param {any} size
358
373
  * @return {'min' | 'max'}
359
374
  */
360
375
  function validateSizeValue(size) {
@@ -366,10 +381,10 @@ function validateSizeValue(size) {
366
381
 
367
382
  class SankeyController extends chart_js.DatasetController {
368
383
  /**
369
- * @param meta {ChartMeta<Flow, Element>}
370
- * @param data {Array<SankeyDataPoint>} Array of original data elements
371
- * @param start {number}
372
- * @param count {number}
384
+ * @param {ChartMeta<Flow, Element>} meta
385
+ * @param {Array<SankeyDataPoint>} data Array of original data elements
386
+ * @param {number} start
387
+ * @param {number} count
373
388
  * @return {Array<SankeyParsedData>}
374
389
  */
375
390
  parseObjectData(meta, data, start, count) {
@@ -401,8 +416,8 @@ class SankeyController extends chart_js.DatasetController {
401
416
  const dataPoint = data[i]; /* {SankeyDataPoint} */
402
417
  const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
403
418
  const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
404
- const fromY = from.y + getAddY(from.to, dataPoint.to);
405
- const toY = to.y + getAddY(to.from, dataPoint.from);
419
+ const fromY = from.y + getAddY(from.to, dataPoint.to, i);
420
+ const toY = to.y + getAddY(to.from, dataPoint.from, i);
406
421
  parsed.push({
407
422
  x: xScale.parse(from.x, i),
408
423
  y: yScale.parse(fromY, i),
@@ -434,10 +449,10 @@ class SankeyController extends chart_js.DatasetController {
434
449
  }
435
450
 
436
451
  /**
437
- * @param elems {Array<Flow>}
438
- * @param start {number}
439
- * @param count {number}
440
- * @param mode {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"}
452
+ * @param {Array<Flow>} elems
453
+ * @param {number} start
454
+ * @param {number} count
455
+ * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
441
456
  */
442
457
  updateElements(elems, start, count, mode) {
443
458
  const me = this;
@@ -509,11 +524,11 @@ class SankeyController extends chart_js.DatasetController {
509
524
  }
510
525
 
511
526
  /**
512
- * @param label {string}
513
- * @param y {number}
514
- * @param height {number}
515
- * @param ctx {CanvasRenderingContext2D}
516
- * @param textX {number}
527
+ * @param {string} label
528
+ * @param {number} y
529
+ * @param {number} height
530
+ * @param {CanvasRenderingContext2D} ctx
531
+ * @param {number} textX
517
532
  * @private
518
533
  */
519
534
  _drawLabel(label, y, height, ctx, textX) {
@@ -538,7 +553,7 @@ class SankeyController extends chart_js.DatasetController {
538
553
  }
539
554
 
540
555
  /**
541
- * @param inputs {string | Array<string>}
556
+ * @param {string | Array<string>} inputs
542
557
  * @return {Array<string>}
543
558
  * @todo move this in Chart.helpers.toTextLines
544
559
  */
@@ -618,6 +633,7 @@ class SankeyController extends chart_js.DatasetController {
618
633
  }
619
634
 
620
635
  SankeyController.id = 'sankey';
636
+
621
637
  SankeyController.defaults = {
622
638
  dataElementType: 'flow',
623
639
  animations: {
@@ -656,6 +672,7 @@ SankeyController.defaults = {
656
672
  }
657
673
  }
658
674
  };
675
+
659
676
  SankeyController.overrides = {
660
677
  interaction: {
661
678
  mode: 'nearest',
@@ -713,10 +730,10 @@ SankeyController.overrides = {
713
730
  * @typedef {{x: number, y: number}} ControlPoint
714
731
  * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
715
732
  *
716
- * @param x {number}
717
- * @param y {number}
718
- * @param x2 {number}
719
- * @param y2 {number}
733
+ * @param {number} x
734
+ * @param {number} y
735
+ * @param {number} x2
736
+ * @param {number} y2
720
737
  * @return {ControlPoints}
721
738
  */
722
739
  const controlPoints = (x, y, x2, y2) => x < x2
@@ -731,9 +748,9 @@ const controlPoints = (x, y, x2, y2) => x < x2
731
748
 
732
749
  /**
733
750
  *
734
- * @param p1 {ControlPoint}
735
- * @param p2 {ControlPoint}
736
- * @param t {number}
751
+ * @param {ControlPoint} p1
752
+ * @param {ControlPoint} p2
753
+ * @param {number} t
737
754
  * @return {ControlPoint}
738
755
  */
739
756
  const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
@@ -741,7 +758,7 @@ const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t *
741
758
  class Flow extends chart_js.Element {
742
759
 
743
760
  /**
744
- * @param cfg {FlowConfig}
761
+ * @param {FlowConfig} cfg
745
762
  */
746
763
  constructor(cfg) {
747
764
  super();
@@ -759,7 +776,7 @@ class Flow extends chart_js.Element {
759
776
  }
760
777
 
761
778
  /**
762
- * @param ctx {CanvasRenderingContext2D}
779
+ * @param {CanvasRenderingContext2D} ctx
763
780
  */
764
781
  draw(ctx) {
765
782
  const me = this;
@@ -808,9 +825,9 @@ class Flow extends chart_js.Element {
808
825
  }
809
826
 
810
827
  /**
811
- * @param mouseX {number}
812
- * @param mouseY {number}
813
- * @param useFinalPosition {boolean}
828
+ * @param {number} mouseX
829
+ * @param {number} mouseY
830
+ * @param {boolean} useFinalPosition
814
831
  * @return {boolean}
815
832
  */
816
833
  inRange(mouseX, mouseY, useFinalPosition) {
@@ -832,8 +849,8 @@ class Flow extends chart_js.Element {
832
849
  }
833
850
 
834
851
  /**
835
- * @param mouseX {number}
836
- * @param useFinalPosition {boolean}
852
+ * @param {number} mouseX
853
+ * @param {boolean} useFinalPosition
837
854
  * @return {boolean}
838
855
  */
839
856
  inXRange(mouseX, useFinalPosition) {
@@ -842,8 +859,8 @@ class Flow extends chart_js.Element {
842
859
  }
843
860
 
844
861
  /**
845
- * @param mouseY {number}
846
- * @param useFinalPosition {boolean}
862
+ * @param {number} mouseY
863
+ * @param {boolean} useFinalPosition
847
864
  * @return {boolean}
848
865
  */
849
866
  inYRange(mouseY, useFinalPosition) {
@@ -854,7 +871,7 @@ class Flow extends chart_js.Element {
854
871
  }
855
872
 
856
873
  /**
857
- * @param useFinalPosition {boolean}
874
+ * @param {boolean} useFinalPosition
858
875
  * @return {{x: number, y:number}}
859
876
  */
860
877
  getCenterPoint(useFinalPosition) {
@@ -870,7 +887,7 @@ class Flow extends chart_js.Element {
870
887
  }
871
888
 
872
889
  /**
873
- * @param axis {"x" | "y"}
890
+ * @param {"x" | "y"} axis
874
891
  * @return {number}
875
892
  */
876
893
  getRange(axis) {
@@ -887,4 +904,4 @@ Flow.defaults = {
887
904
 
888
905
  chart_js.Chart.register(SankeyController, Flow);
889
906
 
890
- })));
907
+ }));
@@ -1,7 +1,7 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.8.0
2
+ * chartjs-chart-sankey v0.8.1
3
3
  * https://github.com/kurkle/chartjs-chart-sankey#readme
4
- * (c) 2021 Jukka Kurkela
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);function s(t,e){return t.from.sort(i("from")).forEach((t=>{const o=t.node;r(o.y)||(o.y=e,e=Math.max(e+o.out,s(o,e)))})),e}function l(t,e){return t.to.sort(i("to")).forEach((t=>{const o=t.node;r(o.y)||(o.y=e,e=Math.max(e+o.in,l(o,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)));let s=o.reduce(((t,e)=>Math.max(t,e.y+e.out||0)),0),h=a.reduce(((t,e)=>Math.max(t,e.y+e.in||0)),0);return s>=h?(n.forEach((t=>{s=c(t,s),s=Math.max(s+t.out,l(t,s))})),i.forEach((t=>{h=c(t,h),h=Math.max(h+t.in,l(t,h))}))):(i.forEach((t=>{h=c(t,h),h=Math.max(h+t.in,l(t,h))})),n.forEach((t=>{s=c(t,s),s=Math.max(s+t.out,l(t,s))}))),Math.max(s,h)}(t,e);return Math.max(a,n,i)}function f(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;for(let r=0;r<=e;r++){let e=0;const a=t.filter((t=>t.x===r)).sort(((t,e)=>t.priority-e.priority));for(const t of a)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 d(t,e){for(let o=0;o<t.length;o++)if(t[o].key===e)return t[o].addY;return 0}function u(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}class y 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})}else e.set(r,{key:r,in:0,out:n,from:[],to:[{key:a,flow:n}]});if(e.has(a)){const t=e.get(a);t.in+=n,t.from.push({key:r,flow:n})}else e.set(a,{key:a,in:n,out:0,from:[{key:r,flow:n}],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:y,maxY:x}=f(l,e,!!c,u(h));a._maxX=y,a._maxY=x;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+d(r.to,o.to),h=a.y+d(a.from,o.from);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(),f=e.valueOrDefault(h.borderWidth,1)/2+.5,d=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)+d+f,y:l,x2:i.getPixelForValue(r.x)-f,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 f=t.chart.chartArea;for(const t of r.values()){const e=c.getPixelForValue(t.x),r=h.getPixelForValue(t.y),d=Math[n](t.in||t.out,t.out||t.in),u=Math.abs(h.getPixelForValue(t.y+d)-r),y=l&&l[t.key]||t.key;let x=e;o.fillStyle=a.color||"black",o.textBaseline="middle",e<f.width/2?(o.textAlign="left",x+=s+i+4):(o.textAlign="right",x-=i+4),this._drawLabel(y,r,u,o,x)}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,f=s.lineHeight,d=e.valueOrDefault(i.options.padding,f/2);if(a.font=s.string,c>1){const t=h-f*c/2+d;for(let e=0;e<c;e++)a.fillText(l[e],n,t+e*f)}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()}}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 x=(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}=x(o,a,r,n),h=this.options;if(0===s)return;let f;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?f=e.color(h.colorFrom).alpha(.5).rgbString():"to"===h.colorMode?f=e.color(h.colorTo).alpha(.5).rgbString():(f=t.createLinearGradient(o,0,r,0),f.addColorStop(0,e.color(h.colorFrom).alpha(.5).rgbString()),f.addColorStop(1,e.color(h.colorTo).alpha(.5).rgbString())),t.fillStyle=f,t.strokeStyle=f,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}=x(r,a,n,i),h=(t-r)/(n-r),f={x:n,y:i},d=p({x:r,y:a},l,h),u=p(l,c,h),y=p(c,f,h),g=p(d,u,h),m=p(u,y,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(y,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,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)}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chartjs-chart-sankey",
3
- "version": "0.8.0",
3
+ "version": "0.8.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",
@@ -9,6 +9,7 @@
9
9
  "build": "rollup -c",
10
10
  "autobuild": "rollup -c -w",
11
11
  "dev": "karma start --no-single-run --auto-watch --browsers chrome",
12
+ "dev:ff": "karma start --no-single-run --auto-watch --browsers firefox",
12
13
  "lint": "concurrently -r \"npm:lint-*\"",
13
14
  "lint-js": "eslint \"src/**/*.js\" \"test/**/*.js\"",
14
15
  "lint-md": "eslint \"**/*.md\"",