chartjs-chart-sankey 0.6.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,12 +1,17 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.6.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
- import { valueOrDefault, color } from 'chart.js/helpers';
8
+ import { valueOrDefault, toFont, isNullOrUndef, color } from 'chart.js/helpers';
9
9
 
10
+ /**
11
+ * @param {Map<string, SankeyNode>} nodes
12
+ * @param {Array<SankeyDataPoint>} data
13
+ * @return {number}
14
+ */
10
15
  function calculateX(nodes, data) {
11
16
  const to = new Set(data.map(x => x.to));
12
17
  const from = new Set(data.map(x => x.from));
@@ -33,42 +38,87 @@ function calculateX(nodes, data) {
33
38
  return x;
34
39
  }
35
40
 
41
+ /**
42
+ * @param {Array<string>} keys
43
+ * @param {Set<string>} to
44
+ * @return {Array<string>}
45
+ */
36
46
  function nextColumn(keys, to) {
37
47
  const columnsNotInTo = keys.filter(key => !to.has(key));
38
48
  return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
39
49
  }
40
50
 
51
+ /**
52
+ * @param x {any}
53
+ * @return {boolean}
54
+ */
41
55
  const defined = x => x !== undefined;
56
+
57
+ /**
58
+ * @param {SankeyNode} a
59
+ * @param {SankeyNode} b
60
+ * @return {number}
61
+ */
42
62
  const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
63
+
64
+ /**
65
+ * @param {Array<FromToElement>} list
66
+ * @param {string} prop
67
+ * @return {number}
68
+ */
43
69
  const nodeCount = (list, prop) => list.reduce((acc, cur) => acc + cur.node[prop].length + nodeCount(cur.node[prop], prop), 0);
44
- const flowByNodeCount = (prop) => (a, b) => nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop);
45
70
 
46
- function findLargestNode(nodeArray) {
47
- return nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out))[0];
48
- }
71
+ /**
72
+ * @param {string} prop
73
+ * @return {function(FromToElement, FromToElement): number}
74
+ */
75
+ const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
49
76
 
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
+ /**
84
+ * @param {SankeyNode} node
85
+ * @param {number} y
86
+ * @return {number}
87
+ */
50
88
  function processFrom(node, y) {
51
89
  node.from.sort(flowByNodeCount('from')).forEach(flow => {
52
90
  const n = flow.node;
53
91
  if (!defined(n.y)) {
54
92
  n.y = y;
55
- y = Math.max(y + n.out, processFrom(n, y));
93
+ processFrom(n, y);
56
94
  }
95
+ y = Math.max(n.y + n.out, y);
57
96
  });
58
97
  return y;
59
98
  }
60
99
 
100
+ /**
101
+ * @param {SankeyNode} node
102
+ * @param {number} y
103
+ * @return {number}
104
+ */
61
105
  function processTo(node, y) {
62
106
  node.to.sort(flowByNodeCount('to')).forEach(flow => {
63
107
  const n = flow.node;
64
108
  if (!defined(n.y)) {
65
109
  n.y = y;
66
- y = Math.max(y + n.in, processTo(n, y));
110
+ processTo(n, y);
67
111
  }
112
+ y = Math.max(n.y + n.in, y);
68
113
  });
69
114
  return y;
70
115
  }
71
116
 
117
+ /**
118
+ * @param {SankeyNode} node
119
+ * @param {number} value
120
+ * @return {number}
121
+ */
72
122
  function setOrGetY(node, value) {
73
123
  if (defined(node.y)) {
74
124
  return node.y;
@@ -77,14 +127,21 @@ function setOrGetY(node, value) {
77
127
  return value;
78
128
  }
79
129
 
130
+ /**
131
+ * @param {Array<SankeyNode>} nodeArray
132
+ * @param {number} maxX
133
+ * @return {number}
134
+ */
80
135
  function processRest(nodeArray, maxX) {
81
136
  const leftNodes = nodeArray.filter(node => node.x === 0);
82
137
  const rightNodes = nodeArray.filter(node => node.x === maxX);
83
138
  const leftToDo = leftNodes.filter(node => !defined(node.y));
84
139
  const rightToDo = rightNodes.filter(node => !defined(node.y));
140
+ const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
85
141
 
86
142
  let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
87
143
  let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
144
+ let centerY = 0;
88
145
 
89
146
  if (leftY >= rightY) {
90
147
  leftToDo.forEach(node => {
@@ -107,10 +164,23 @@ function processRest(nodeArray, maxX) {
107
164
  leftY = Math.max(leftY + node.out, processTo(node, leftY));
108
165
  });
109
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
+ });
110
175
 
111
- return Math.max(leftY, rightY);
176
+ return Math.max(leftY, rightY, centerY);
112
177
  }
113
178
 
179
+ /**
180
+ * @param {Array<SankeyNode>} nodeArray
181
+ * @param {number} maxX
182
+ * @return {number}
183
+ */
114
184
  function calculateY(nodeArray, maxX) {
115
185
  const start = findLargestNode(nodeArray);
116
186
  start.y = 0;
@@ -120,11 +190,18 @@ function calculateY(nodeArray, maxX) {
120
190
  return Math.max(left, right, rest);
121
191
  }
122
192
 
193
+ /**
194
+ * @param {Array<SankeyNode>} nodeArray
195
+ * @param {number} maxX
196
+ * @return {number}
197
+ */
123
198
  function calculateYUsingPriority(nodeArray, maxX) {
124
199
  let maxY = 0;
200
+ let nextYStart = 0;
125
201
  for (let x = 0; x <= maxX; x++) {
126
- let y = 0;
202
+ let y = nextYStart;
127
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;
128
205
  for (const node of nodes) {
129
206
  node.y = y;
130
207
  y += Math.max(node.out, node.in);
@@ -134,6 +211,11 @@ function calculateYUsingPriority(nodeArray, maxX) {
134
211
  return maxY;
135
212
  }
136
213
 
214
+ /**
215
+ * @param {Array<SankeyNode>} nodeArray
216
+ * @param {number} padding
217
+ * @return {number}
218
+ */
137
219
  function addPadding(nodeArray, padding) {
138
220
  let i = 1;
139
221
  let x = 0;
@@ -158,95 +240,150 @@ function addPadding(nodeArray, padding) {
158
240
  prev = i;
159
241
  }
160
242
  node.y += i * padding;
161
- maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
162
243
  i++;
163
244
  }
245
+ maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
164
246
  });
165
247
  return maxY;
166
248
  }
167
249
 
168
- function sortFlows(nodeArray) {
169
- nodeArray.forEach(node => {
250
+ /**
251
+ * @param {Array<SankeyNode>} nodeArray
252
+ * @param {'min' | 'max'} size
253
+ */
254
+ function sortFlows(nodeArray, size) {
255
+ nodeArray.forEach((node) => {
256
+ const nodeSize = Math[size](node.in || node.out, node.out || node.in);
257
+ const overlapFrom = nodeSize < node.in;
258
+ const overlapTo = nodeSize < node.out;
170
259
  let addY = 0;
171
- node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach(flow => {
172
- flow.addY = addY;
173
- addY += flow.flow;
260
+ let len = node.from.length;
261
+ node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
262
+ if (overlapFrom) {
263
+ flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
264
+ } else {
265
+ flow.addY = addY;
266
+ addY += flow.flow;
267
+ }
174
268
  });
175
269
  addY = 0;
176
- node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach(flow => {
177
- flow.addY = addY;
178
- addY += flow.flow;
270
+ len = node.to.length;
271
+ node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
272
+ if (overlapTo) {
273
+ flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
274
+ } else {
275
+ flow.addY = addY;
276
+ addY += flow.flow;
277
+ }
179
278
  });
180
279
  });
181
280
  }
182
281
 
183
- function layout(nodes, data, priority) {
282
+ /**
283
+ * @param {Map<string, SankeyNode>} nodes
284
+ * @param {Array<SankeyDataPoint>} data
285
+ * @param {boolean} priority
286
+ * @param {'min' | 'max'} size
287
+ * @return {{maxY: number, maxX: number}}
288
+ */
289
+ function layout(nodes, data, priority, size) {
184
290
  const nodeArray = [...nodes.values()];
185
291
  const maxX = calculateX(nodes, data);
186
292
  const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
187
293
  const padding = maxY * 0.03; // rows;
188
-
189
294
  const maxYWithPadding = addPadding(nodeArray, padding);
190
- sortFlows(nodeArray);
191
-
295
+ sortFlows(nodeArray, size);
192
296
  return {maxX, maxY: maxYWithPadding};
193
297
  }
194
298
 
195
- function buildNodesFromFlows(data) {
299
+ /**
300
+ * @param {Array<SankeyDataPoint>} data Array of raw data elements
301
+ * @return {Map<string, SankeyNode>}
302
+ */
303
+ function buildNodesFromRawData(data) {
196
304
  const nodes = new Map();
197
305
  for (let i = 0; i < data.length; i++) {
198
- const d = data[i];
199
- if (!nodes.has(d.from)) {
200
- nodes.set(d.from, {key: d.from, in: 0, out: d.flow, from: [], to: [{key: d.to, flow: d.flow}]});
306
+ const {from, to, flow} = data[i];
307
+
308
+ if (!nodes.has(from)) {
309
+ nodes.set(from, {
310
+ key: from,
311
+ in: 0,
312
+ out: flow,
313
+ from: [],
314
+ to: [{key: to, flow: flow, index: i}],
315
+ });
201
316
  } else {
202
- const node = nodes.get(d.from);
203
- node.out += d.flow;
204
- node.to.push({key: d.to, flow: d.flow});
317
+ const node = nodes.get(from);
318
+ node.out += flow;
319
+ node.to.push({key: to, flow: flow, index: i});
205
320
  }
206
- if (!nodes.has(d.to)) {
207
- nodes.set(d.to, {key: d.to, in: d.flow, out: 0, from: [{key: d.from, flow: d.flow}], to: []});
321
+ if (!nodes.has(to)) {
322
+ nodes.set(to, {
323
+ key: to,
324
+ in: flow,
325
+ out: 0,
326
+ from: [{key: from, flow: flow, index: i}],
327
+ to: [],
328
+ });
208
329
  } else {
209
- const node = nodes.get(d.to);
210
- node.in += d.flow;
211
- node.from.push({key: d.from, flow: d.flow});
330
+ const node = nodes.get(to);
331
+ node.in += flow;
332
+ node.from.push({key: from, flow: flow, index: i});
212
333
  }
213
334
  }
214
335
 
215
336
  const flowSort = (a, b) => b.flow - a.flow;
216
337
 
217
338
  [...nodes.values()].forEach(node => {
218
- let tmp = 0;
219
339
  node.from = node.from.sort(flowSort);
220
340
  node.from.forEach(x => {
221
341
  x.node = nodes.get(x.key);
222
- x.addY = tmp;
223
- tmp += x.flow;
224
342
  });
225
343
 
226
- tmp = 0;
227
344
  node.to = node.to.sort(flowSort);
228
345
  node.to.forEach(x => {
229
346
  x.node = nodes.get(x.key);
230
- x.addY = tmp;
231
- tmp += x.flow;
232
347
  });
233
348
  });
234
349
 
235
350
  return nodes;
236
351
  }
237
352
 
238
-
239
- function getAddY(arr, key) {
240
- for (let i = 0; i < arr.length; i++) {
241
- if (arr[i].key === key) {
242
- return arr[i].addY;
353
+ /**
354
+ * @param {Array<FromToElement>} arr
355
+ * @param {string} key
356
+ * @param {number} index
357
+ * @return {number}
358
+ */
359
+ function getAddY(arr, key, index) {
360
+ for (const item of arr) {
361
+ if (item.key === key && item.index === index) {
362
+ return item.addY;
243
363
  }
244
364
  }
245
365
  return 0;
246
366
  }
247
367
 
248
- class SankeyController extends DatasetController {
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
+ }
249
378
 
379
+ class SankeyController extends DatasetController {
380
+ /**
381
+ * @param {ChartMeta<Flow, Element>} meta
382
+ * @param {Array<SankeyDataPoint>} data Array of original data elements
383
+ * @param {number} start
384
+ * @param {number} count
385
+ * @return {Array<SankeyParsedData>}
386
+ */
250
387
  parseObjectData(meta, data, start, count) {
251
388
  // https://github.com/chartjs/Chart.js/pull/8379
252
389
  if (count === 0) {
@@ -254,9 +391,10 @@ class SankeyController extends DatasetController {
254
391
  }
255
392
  const me = this;
256
393
  const {xScale, yScale} = meta;
257
- const parsed = [];
258
- const nodes = me._nodes = buildNodesFromFlows(data);
259
- const priority = me.getDataset().priority;
394
+ const parsed = []; /* Array<SankeyParsedData> */
395
+ const nodes = me._nodes = buildNodesFromRawData(data);
396
+ /* getDataset() => SankeyControllerDatasetOptions */
397
+ const {priority, size} = me.getDataset();
260
398
  if (priority) {
261
399
  for (const node of nodes.values()) {
262
400
  if (node.key in priority) {
@@ -265,17 +403,18 @@ class SankeyController extends DatasetController {
265
403
  }
266
404
  }
267
405
 
268
- const {maxX, maxY} = layout(nodes, data, !!priority);
406
+ const {maxX, maxY} = layout(nodes, data, !!priority, validateSizeValue(size));
269
407
 
270
408
  me._maxX = maxX;
271
409
  me._maxY = maxY;
272
410
 
411
+ /* loop over raw data elements {SankeyDataPoint} */
273
412
  for (let i = 0, ilen = data.length; i < ilen; ++i) {
274
- const flow = data[i];
275
- const from = nodes.get(flow.from);
276
- const to = nodes.get(flow.to);
277
- const fromY = from.y + getAddY(from.to, flow.to);
278
- const toY = to.y + getAddY(to.from, flow.from);
413
+ const dataPoint = data[i]; /* {SankeyDataPoint} */
414
+ const from = nodes.get(dataPoint.from); /* from {SankeyNode} */
415
+ const to = nodes.get(dataPoint.to); /* to {SankeyNode} */
416
+ const fromY = from.y + getAddY(from.to, dataPoint.to, i);
417
+ const toY = to.y + getAddY(to.from, dataPoint.from, i);
279
418
  parsed.push({
280
419
  x: xScale.parse(from.x, i),
281
420
  y: yScale.parse(fromY, i),
@@ -284,7 +423,7 @@ class SankeyController extends DatasetController {
284
423
  to,
285
424
  x: xScale.parse(to.x, i),
286
425
  y: yScale.parse(toY, i),
287
- height: yScale.parse(flow.flow, i),
426
+ height: yScale.parse(dataPoint.flow, i),
288
427
  }
289
428
  });
290
429
  }
@@ -306,6 +445,12 @@ class SankeyController extends DatasetController {
306
445
  me.updateElements(meta.data, 0, meta.data.length, mode);
307
446
  }
308
447
 
448
+ /**
449
+ * @param {Array<Flow>} elems
450
+ * @param {number} start
451
+ * @param {number} count
452
+ * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
453
+ */
309
454
  updateElements(elems, start, count, mode) {
310
455
  const me = this;
311
456
  const {xScale, yScale} = me._cachedMeta;
@@ -316,6 +461,7 @@ class SankeyController extends DatasetController {
316
461
  const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
317
462
 
318
463
  for (let i = start; i < start + count; i++) {
464
+ /* getParsed(idx: number) => SankeyParsedData */
319
465
  const parsed = me.getParsed(i);
320
466
  const custom = parsed._custom;
321
467
  const y = yScale.getPixelForValue(parsed.y);
@@ -343,7 +489,8 @@ class SankeyController extends DatasetController {
343
489
  const me = this;
344
490
  const ctx = me._ctx;
345
491
  const nodes = me._nodes || new Map();
346
- const dataset = me.getDataset();
492
+ const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
493
+ const size = validateSizeValue(dataset.size);
347
494
  const borderWidth = valueOrDefault(dataset.borderWidth, 1);
348
495
  const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
349
496
  const labels = dataset.labels;
@@ -351,11 +498,11 @@ class SankeyController extends DatasetController {
351
498
 
352
499
  ctx.save();
353
500
  const chartArea = me.chart.chartArea;
354
-
355
501
  for (const node of nodes.values()) {
356
502
  const x = xScale.getPixelForValue(node.x);
357
503
  const y = yScale.getPixelForValue(node.y);
358
- const max = Math.max(node.in, node.out);
504
+
505
+ const max = Math[size](node.in || node.out, node.out || node.in);
359
506
  const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
360
507
  const label = labels && labels[node.key] || node.key;
361
508
  let textX = x;
@@ -368,16 +515,70 @@ class SankeyController extends DatasetController {
368
515
  ctx.textAlign = 'right';
369
516
  textX -= borderWidth + 4;
370
517
  }
371
- ctx.fillText(label, textX, y + height / 2);
518
+ this._drawLabel(label, y, height, ctx, textX);
372
519
  }
373
520
  ctx.restore();
374
521
  }
375
522
 
523
+ /**
524
+ * @param {string} label
525
+ * @param {number} y
526
+ * @param {number} height
527
+ * @param {CanvasRenderingContext2D} ctx
528
+ * @param {number} textX
529
+ * @private
530
+ */
531
+ _drawLabel(label, y, height, ctx, textX) {
532
+ const me = this;
533
+ const font = toFont(me.options.font, me.chart.options.font);
534
+ const lines = isNullOrUndef(label) ? [] : me.toTextLines(label);
535
+ const linesLength = lines.length;
536
+ const middle = y + height / 2;
537
+ const textHeight = font.lineHeight;
538
+ const padding = valueOrDefault(me.options.padding, textHeight / 2);
539
+
540
+ ctx.font = font.string;
541
+
542
+ if (linesLength > 1) {
543
+ const top = middle - (textHeight * linesLength / 2) + padding;
544
+ for (let i = 0; i < linesLength; i++) {
545
+ ctx.fillText(lines[i], textX, top + (i * textHeight));
546
+ }
547
+ } else {
548
+ ctx.fillText(label, textX, middle);
549
+ }
550
+ }
551
+
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
+
376
576
  _drawNodes() {
377
577
  const me = this;
378
578
  const ctx = me._ctx;
379
579
  const nodes = me._nodes || new Map();
380
- const dataset = me.getDataset();
580
+ const dataset = me.getDataset(); /* SankeyControllerDatasetOptions */
581
+ const size = validateSizeValue(dataset.size);
381
582
  const {xScale, yScale} = me._cachedMeta;
382
583
  const borderWidth = valueOrDefault(dataset.borderWidth, 1);
383
584
  const nodeWidth = valueOrDefault(dataset.nodeWidth, 10);
@@ -390,7 +591,8 @@ class SankeyController extends DatasetController {
390
591
  ctx.fillStyle = node.color;
391
592
  const x = xScale.getPixelForValue(node.x);
392
593
  const y = yScale.getPixelForValue(node.y);
393
- const max = Math.max(node.in, node.out);
594
+
595
+ const max = Math[size](node.in || node.out, node.out || node.in);
394
596
  const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
395
597
  if (borderWidth) {
396
598
  ctx.strokeRect(x, y, nodeWidth, height);
@@ -400,28 +602,35 @@ class SankeyController extends DatasetController {
400
602
  ctx.restore();
401
603
  }
402
604
 
605
+ /**
606
+ * That's where the drawing process happens
607
+ */
403
608
  draw() {
404
609
  const me = this;
405
610
  const ctx = me._ctx;
406
- const data = me.getMeta().data || [];
611
+ const data = me.getMeta().data || []; /* Array<Flow> */
407
612
 
408
613
  for (let i = 0, ilen = data.length; i < ilen; ++i) {
409
- const flow = data[i];
614
+ const flow = data[i]; /* Flow at index i */
410
615
  flow.from.color = flow.options.colorFrom;
411
616
  flow.to.color = flow.options.colorTo;
412
617
  }
413
618
 
619
+ /* draw SankeyNodes on the canvas */
414
620
  me._drawNodes();
415
621
 
622
+ /* draw Flow elements on the canvas */
416
623
  for (let i = 0, ilen = data.length; i < ilen; ++i) {
417
624
  data[i].draw(ctx);
418
625
  }
419
626
 
627
+ /* draw labels (for SankeyNodes) on the canvas */
420
628
  me._drawLabels();
421
629
  }
422
630
  }
423
631
 
424
632
  SankeyController.id = 'sankey';
633
+
425
634
  SankeyController.defaults = {
426
635
  dataElementType: 'flow',
427
636
  animations: {
@@ -457,9 +666,10 @@ SankeyController.defaults = {
457
666
  from: 'transparent'
458
667
  }
459
668
  }
460
- },
669
+ }
461
670
  }
462
671
  };
672
+
463
673
  SankeyController.overrides = {
464
674
  interaction: {
465
675
  mode: 'nearest',
@@ -480,10 +690,10 @@ SankeyController.overrides = {
480
690
  const item = context.dataset.data[context.dataIndex];
481
691
  return item.from + ' -> ' + item.to + ': ' + item.flow;
482
692
  }
483
- }
693
+ },
484
694
  },
485
695
  legend: {
486
- display: false
696
+ display: false,
487
697
  },
488
698
  },
489
699
  scales: {
@@ -492,7 +702,7 @@ SankeyController.overrides = {
492
702
  bounds: 'data',
493
703
  display: false,
494
704
  min: 0,
495
- offset: false
705
+ offset: false,
496
706
  },
497
707
  y: {
498
708
  type: 'linear',
@@ -500,19 +710,29 @@ SankeyController.overrides = {
500
710
  display: false,
501
711
  min: 0,
502
712
  reverse: true,
503
- offset: false
504
- }
713
+ offset: false,
714
+ },
505
715
  },
506
716
  layout: {
507
717
  padding: {
508
718
  top: 3,
509
719
  left: 3,
510
720
  right: 13,
511
- bottom: 3
512
- }
513
- }
721
+ bottom: 3,
722
+ },
723
+ },
514
724
  };
515
725
 
726
+ /**
727
+ * @typedef {{x: number, y: number}} ControlPoint
728
+ * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
729
+ *
730
+ * @param {number} x
731
+ * @param {number} y
732
+ * @param {number} x2
733
+ * @param {number} y2
734
+ * @return {ControlPoints}
735
+ */
516
736
  const controlPoints = (x, y, x2, y2) => x < x2
517
737
  ? {
518
738
  cp1: {x: x + (x2 - x) / 3 * 2, y},
@@ -523,9 +743,20 @@ const controlPoints = (x, y, x2, y2) => x < x2
523
743
  cp2: {x: x2 + (x - x2) / 3, y: 0}
524
744
  };
525
745
 
746
+ /**
747
+ *
748
+ * @param {ControlPoint} p1
749
+ * @param {ControlPoint} p2
750
+ * @param {number} t
751
+ * @return {ControlPoint}
752
+ */
526
753
  const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
527
754
 
528
755
  class Flow extends Element {
756
+
757
+ /**
758
+ * @param {FlowConfig} cfg
759
+ */
529
760
  constructor(cfg) {
530
761
  super();
531
762
 
@@ -541,6 +772,9 @@ class Flow extends Element {
541
772
  }
542
773
  }
543
774
 
775
+ /**
776
+ * @param {CanvasRenderingContext2D} ctx
777
+ */
544
778
  draw(ctx) {
545
779
  const me = this;
546
780
  const {x, x2, y, y2, height, progress} = me;
@@ -587,6 +821,12 @@ class Flow extends Element {
587
821
  ctx.restore();
588
822
  }
589
823
 
824
+ /**
825
+ * @param {number} mouseX
826
+ * @param {number} mouseY
827
+ * @param {boolean} useFinalPosition
828
+ * @return {boolean}
829
+ */
590
830
  inRange(mouseX, mouseY, useFinalPosition) {
591
831
  const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
592
832
  if (mouseX < x || mouseX > x2) {
@@ -605,11 +845,21 @@ class Flow extends Element {
605
845
  return mouseY >= topY && mouseY <= topY + height;
606
846
  }
607
847
 
848
+ /**
849
+ * @param {number} mouseX
850
+ * @param {boolean} useFinalPosition
851
+ * @return {boolean}
852
+ */
608
853
  inXRange(mouseX, useFinalPosition) {
609
854
  const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition);
610
855
  return mouseX >= x && mouseX <= x2;
611
856
  }
612
857
 
858
+ /**
859
+ * @param {number} mouseY
860
+ * @param {boolean} useFinalPosition
861
+ * @return {boolean}
862
+ */
613
863
  inYRange(mouseY, useFinalPosition) {
614
864
  const {y, y2, height} = this.getProps(['y', 'y2', 'height'], useFinalPosition);
615
865
  const minY = Math.min(y, y2);
@@ -617,6 +867,10 @@ class Flow extends Element {
617
867
  return mouseY >= minY && mouseY <= maxY;
618
868
  }
619
869
 
870
+ /**
871
+ * @param {boolean} useFinalPosition
872
+ * @return {{x: number, y:number}}
873
+ */
620
874
  getCenterPoint(useFinalPosition) {
621
875
  const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
622
876
  return {
@@ -625,10 +879,14 @@ class Flow extends Element {
625
879
  };
626
880
  }
627
881
 
628
- tooltipPosition() {
629
- return this.getCenterPoint();
882
+ tooltipPosition(useFinalPosition) {
883
+ return this.getCenterPoint(useFinalPosition);
630
884
  }
631
885
 
886
+ /**
887
+ * @param {"x" | "y"} axis
888
+ * @return {number}
889
+ */
632
890
  getRange(axis) {
633
891
  return axis === 'x' ? this.width / 2 : this.height / 2;
634
892
  }