chartjs-chart-sankey 0.7.1 → 0.9.0

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