chartjs-chart-sankey 0.13.0 → 0.14.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,947 +0,0 @@
1
- /*!
2
- * chartjs-chart-sankey v0.13.0
3
- * https://github.com/kurkle/chartjs-chart-sankey#readme
4
- * (c) 2024 Jukka Kurkela
5
- * Released under the MIT license
6
- */
7
- (function (global, factory) {
8
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('chart.js'), require('chart.js/helpers')) :
9
- typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpers'], factory) :
10
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
11
- })(this, (function (chart_js, helpers) { 'use strict';
12
-
13
- /**
14
- * @param {string | Array<string>} raw
15
- * @return {Array<string>}
16
- */
17
- function toTextLines(raw) {
18
- const lines = [];
19
- const inputs = helpers.isArray(raw) ? raw : helpers.isNullOrUndef(raw) ? [] : [raw];
20
-
21
- while (inputs.length) {
22
- const input = inputs.pop();
23
- if (typeof input === 'string') {
24
- lines.unshift.apply(lines, input.split('\n'));
25
- } else if (Array.isArray(input)) {
26
- inputs.push.apply(inputs, input);
27
- } else if (!helpers.isNullOrUndef(inputs)) {
28
- lines.unshift('' + input);
29
- }
30
- }
31
-
32
- return lines;
33
- }
34
-
35
- /**
36
- * @param {any} size
37
- * @return {'min' | 'max'}
38
- */
39
- function validateSizeValue(size) {
40
- if (!size || ['min', 'max'].indexOf(size) === -1) {
41
- return 'max';
42
- }
43
- return size;
44
- }
45
-
46
- /**
47
- * @param x {any}
48
- * @return {boolean}
49
- */
50
- const defined = x => x !== undefined;
51
-
52
- /**
53
- * @param {Map<string, SankeyNode>} nodes
54
- * @param {Array<SankeyDataPoint>} data
55
- * @return {number}
56
- */
57
- function calculateX(nodes, data) {
58
- const to = new Set(data.map(dataPoint => dataPoint.to));
59
- const from = new Set(data.map(dataPoint => dataPoint.from));
60
- const keys = new Set([...nodes.keys()]);
61
- let x = 0;
62
- while (keys.size) {
63
- const column = nextColumn([...keys], to);
64
- for (const key of column) {
65
- const node = nodes.get(key);
66
- if (!defined(node.x)) {
67
- node.x = x;
68
- }
69
- keys.delete(key);
70
- }
71
- if (keys.size) {
72
- to.clear();
73
- data.filter(flow => keys.has(flow.from)).forEach(flow => to.add(flow.to));
74
- x++;
75
- }
76
- }
77
- [...nodes.keys()]
78
- .filter(key => !from.has(key))
79
- .forEach(key => {
80
- const node = nodes.get(key);
81
- // Only move the node to right edge, if it's column is not defined
82
- if (!node.column) {
83
- node.x = x;
84
- }
85
- });
86
-
87
- return [...nodes.values()]
88
- .reduce((max, node) => Math.max(max, node.x), 0);
89
- }
90
-
91
- /**
92
- * @param {Array<string>} keys
93
- * @param {Set<string>} to
94
- * @return {Array<string>}
95
- */
96
- function nextColumn(keys, to) {
97
- const columnsNotInTo = keys.filter(key => !to.has(key));
98
- return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
99
- }
100
-
101
- /**
102
- * @param {SankeyNode} a
103
- * @param {SankeyNode} b
104
- * @return {number}
105
- */
106
- const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
107
-
108
- let prevCountId = -1;
109
- function getCountId() {
110
- prevCountId = prevCountId < 100 ? prevCountId + 1 : 0;
111
- return prevCountId;
112
- }
113
-
114
- /**
115
- * @param {Array<FromToElement>} list
116
- * @param {string} prop
117
- * @return {number}
118
- */
119
- function nodeCount(list, prop, countId = getCountId()) {
120
- let count = 0;
121
- for (const elem of list) {
122
- if (elem.node._visited === countId) {
123
- continue;
124
- }
125
- elem.node._visited = countId;
126
- count += elem.node[prop].length + nodeCount(elem.node[prop], prop, countId);
127
- }
128
- return count;
129
- }
130
-
131
- /**
132
- * @param {string} prop
133
- * @return {function(FromToElement, FromToElement): number}
134
- */
135
- const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
136
-
137
- /**
138
- * @param {SankeyNode} node
139
- * @param {number} y
140
- * @return {number}
141
- */
142
- function processFrom(node, y) {
143
- node.from.sort(flowByNodeCount('from'));
144
- for (const flow of node.from) {
145
- const n = flow.node;
146
- if (!defined(n.y)) {
147
- n.y = y;
148
- processFrom(n, y);
149
- }
150
- y = Math.max(n.y + n.out, y);
151
- }
152
- return y;
153
- }
154
-
155
- /**
156
- * @param {SankeyNode} node
157
- * @param {number} y
158
- * @return {number}
159
- */
160
- function processTo(node, y) {
161
- node.to.sort(flowByNodeCount('to'));
162
- for (const flow of node.to) {
163
- const n = flow.node;
164
- if (!defined(n.y)) {
165
- n.y = y;
166
- processTo(n, y);
167
- }
168
- const size = Math.max(n.in, n.out);
169
- y = Math.max(n.y + size, y);
170
- }
171
- return y;
172
- }
173
-
174
- /**
175
- * @param {SankeyNode} node
176
- * @param {number} value
177
- * @return {number}
178
- */
179
- function setOrGetY(node, value) {
180
- if (defined(node.y)) {
181
- return node.y;
182
- }
183
- node.y = value;
184
- return value;
185
- }
186
-
187
- /**
188
- * @param {Array<SankeyNode>} nodeArray
189
- * @param {number} maxX
190
- * @return {number}
191
- */
192
- function processRest(nodeArray, maxX) {
193
- const leftNodes = nodeArray.filter(node => node.x === 0);
194
- const rightNodes = nodeArray.filter(node => node.x === maxX);
195
- const leftToDo = leftNodes.filter(node => !defined(node.y));
196
- const rightToDo = rightNodes.filter(node => !defined(node.y));
197
- const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
198
-
199
- let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
200
- let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
201
- let centerY = 0;
202
-
203
- if (leftY >= rightY) {
204
- leftToDo.forEach(node => {
205
- leftY = setOrGetY(node, leftY);
206
- leftY = Math.max(leftY + node.out, processTo(node, leftY));
207
- });
208
-
209
- rightToDo.forEach(node => {
210
- rightY = setOrGetY(node, rightY);
211
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
212
- });
213
- } else {
214
- rightToDo.forEach(node => {
215
- rightY = setOrGetY(node, rightY);
216
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
217
- });
218
-
219
- leftToDo.forEach(node => {
220
- leftY = setOrGetY(node, leftY);
221
- leftY = Math.max(leftY + node.out, processTo(node, leftY));
222
- });
223
- }
224
- centerToDo.forEach(node => {
225
- let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
226
- .reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
227
- y = setOrGetY(node, y);
228
- y = Math.max(y + node.in, processFrom(node, y));
229
- y = Math.max(y + node.out, processTo(node, y));
230
- centerY = Math.max(centerY, y);
231
- });
232
-
233
- return Math.max(leftY, rightY, centerY);
234
- }
235
-
236
- /**
237
- * @param {Array<SankeyNode>} nodeArray
238
- * @param {number} maxX
239
- * @return {number}
240
- */
241
- function calculateY(nodeArray, maxX) {
242
- nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
243
- const start = nodeArray[0];
244
- start.y = 0;
245
- const left = processFrom(start, 0);
246
- const right = processTo(start, 0);
247
- const rest = processRest(nodeArray, maxX);
248
- return Math.max(left, right, rest);
249
- }
250
-
251
- /**
252
- * @param {Array<SankeyNode>} nodeArray
253
- * @param {number} maxX
254
- * @return {number}
255
- */
256
- function calculateYUsingPriority(nodeArray, maxX) {
257
- let maxY = 0;
258
- let nextYStart = 0;
259
- for (let x = 0; x <= maxX; x++) {
260
- let y = nextYStart;
261
- const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
262
- nextYStart = nodes.length ? nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0 : 0;
263
- for (const node of nodes) {
264
- node.y = y;
265
- y += Math.max(node.out, node.in);
266
- }
267
- maxY = Math.max(y, maxY);
268
- }
269
- return maxY;
270
- }
271
-
272
- /**
273
- * @param {Array<SankeyNode>} nodeArray
274
- * @param {number} padding
275
- * @return {number}
276
- */
277
- function addPadding(nodeArray, padding) {
278
- let i = 1;
279
- let x = 0;
280
- let prev = 0;
281
- let maxY = 0;
282
- const rows = [];
283
- nodeArray.sort(nodeByXY);
284
- for (const node of nodeArray) {
285
- if (node.y) {
286
- if (node.x === 0) {
287
- rows.push(node.y);
288
- } else {
289
- if (x !== node.x) {
290
- x = node.x;
291
- prev = 0;
292
- }
293
-
294
- for (i = prev + 1; i < rows.length; i++) {
295
- if (rows[i] > node.y) {
296
- break;
297
- }
298
- }
299
- prev = i;
300
- }
301
- node.y += i * padding;
302
- i++;
303
- }
304
- maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
305
- }
306
- return maxY;
307
- }
308
-
309
- /**
310
- * @param {Array<SankeyNode>} nodeArray
311
- * @param {'min' | 'max'} size
312
- */
313
- function sortFlows(nodeArray, size) {
314
- nodeArray.forEach((node) => {
315
- const nodeSize = Math[size](node.in || node.out, node.out || node.in);
316
- const overlapFrom = nodeSize < node.in;
317
- const overlapTo = nodeSize < node.out;
318
- let addY = 0;
319
- let len = node.from.length;
320
- node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
321
- if (overlapFrom) {
322
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
323
- } else {
324
- flow.addY = addY;
325
- addY += flow.flow;
326
- }
327
- });
328
- addY = 0;
329
- len = node.to.length;
330
- node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
331
- if (overlapTo) {
332
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
333
- } else {
334
- flow.addY = addY;
335
- addY += flow.flow;
336
- }
337
- });
338
- });
339
- }
340
-
341
- /**
342
- * @param {Map<string, SankeyNode>} nodes
343
- * @param {Array<SankeyDataPoint>} data
344
- * @param {boolean} priority
345
- * @param {'min' | 'max'} size
346
- * @return {{maxY: number, maxX: number}}
347
- */
348
- function layout(nodes, data, priority, size) {
349
- const nodeArray = [...nodes.values()];
350
- const maxX = calculateX(nodes, data);
351
- const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
352
- const padding = maxY * 0.03; // rows;
353
- const maxYWithPadding = addPadding(nodeArray, padding);
354
- sortFlows(nodeArray, size);
355
- return {maxX, maxY: maxYWithPadding};
356
- }
357
-
358
- /**
359
- * @param {Array<SankeyDataPoint>} data Array of raw data elements
360
- * @return {Map<string, SankeyNode>}
361
- */
362
- function buildNodesFromRawData(data) {
363
- const nodes = new Map();
364
- for (let i = 0; i < data.length; i++) {
365
- const {from, to, flow} = data[i];
366
-
367
- if (!nodes.has(from)) {
368
- nodes.set(from, {
369
- key: from,
370
- in: 0,
371
- out: flow,
372
- from: [],
373
- to: [{key: to, flow: flow, index: i}],
374
- });
375
- } else {
376
- const node = nodes.get(from);
377
- node.out += flow;
378
- node.to.push({key: to, flow: flow, index: i});
379
- }
380
- if (!nodes.has(to)) {
381
- nodes.set(to, {
382
- key: to,
383
- in: flow,
384
- out: 0,
385
- from: [{key: from, flow: flow, index: i}],
386
- to: [],
387
- });
388
- } else {
389
- const node = nodes.get(to);
390
- node.in += flow;
391
- node.from.push({key: from, flow: flow, index: i});
392
- }
393
- }
394
-
395
- const flowSort = (a, b) => b.flow - a.flow;
396
-
397
- [...nodes.values()].forEach(node => {
398
- node.from = node.from.sort(flowSort);
399
- node.from.forEach(x => {
400
- x.node = nodes.get(x.key);
401
- });
402
-
403
- node.to = node.to.sort(flowSort);
404
- node.to.forEach(x => {
405
- x.node = nodes.get(x.key);
406
- });
407
- });
408
-
409
- return nodes;
410
- }
411
-
412
- /**
413
- * @param {Array<FromToElement>} arr
414
- * @param {string} key
415
- * @param {number} index
416
- * @return {number}
417
- */
418
- function getAddY(arr, key, index) {
419
- for (const item of arr) {
420
- if (item.key === key && item.index === index) {
421
- return item.addY;
422
- }
423
- }
424
- return 0;
425
- }
426
-
427
- class SankeyController extends chart_js.DatasetController {
428
- /**
429
- * @param {ChartMeta<Flow, Element>} meta
430
- * @param {Array<SankeyDataPoint>} data Array of original data elements
431
- * @param {number} start
432
- * @param {number} count
433
- * @return {Array<SankeyParsedData>}
434
- */
435
- parseObjectData(meta, data, start, count) {
436
- const {from: fromKey = 'from', to: toKey = 'to', flow: flowKey = 'flow'} = this.options.parsing;
437
- const sankeyData = data.map(({[fromKey]: from, [toKey]: to, [flowKey]: flow}) => ({from, to, flow}));
438
- const {xScale, yScale} = meta;
439
- const parsed = []; /* Array<SankeyParsedData> */
440
- const nodes = this._nodes = buildNodesFromRawData(sankeyData);
441
- /* getDataset() => SankeyControllerDatasetOptions */
442
- const {column, priority, size} = this.getDataset();
443
- if (priority) {
444
- for (const node of nodes.values()) {
445
- if (node.key in priority) {
446
- node.priority = priority[node.key];
447
- }
448
- }
449
- }
450
- if (column) {
451
- for (const node of nodes.values()) {
452
- if (node.key in column) {
453
- node.column = true;
454
- node.x = column[node.key];
455
- }
456
- }
457
- }
458
-
459
- const {maxX, maxY} = layout(nodes, sankeyData, !!priority, validateSizeValue(size));
460
-
461
- this._maxX = maxX;
462
- this._maxY = maxY;
463
-
464
- for (let i = 0, ilen = sankeyData.length; i < ilen; ++i) {
465
- const dataPoint = sankeyData[i];
466
- const from = nodes.get(dataPoint.from);
467
- const to = nodes.get(dataPoint.to);
468
- const fromY = from.y + getAddY(from.to, dataPoint.to, i);
469
- const toY = to.y + getAddY(to.from, dataPoint.from, i);
470
- parsed.push({
471
- x: xScale.parse(from.x, i),
472
- y: yScale.parse(fromY, i),
473
- _custom: {
474
- from,
475
- to,
476
- x: xScale.parse(to.x, i),
477
- y: yScale.parse(toY, i),
478
- height: yScale.parse(dataPoint.flow, i),
479
- }
480
- });
481
- }
482
- return parsed.slice(start, start + count);
483
- }
484
-
485
- getMinMax(scale) {
486
- return {
487
- min: 0,
488
- max: scale === this._cachedMeta.xScale ? this._maxX : this._maxY
489
- };
490
- }
491
-
492
- update(mode) {
493
- const {data} = this._cachedMeta;
494
-
495
- this.updateElements(data, 0, data.length, mode);
496
- }
497
-
498
- /**
499
- * @param {Array<Flow>} elems
500
- * @param {number} start
501
- * @param {number} count
502
- * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
503
- */
504
- updateElements(elems, start, count, mode) {
505
- const {xScale, yScale} = this._cachedMeta;
506
- const firstOpts = this.resolveDataElementOptions(start, mode);
507
- const sharedOptions = this.getSharedOptions(mode, elems[start], firstOpts);
508
- const dataset = this.getDataset();
509
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1) / 2 + 0.5;
510
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
511
-
512
- for (let i = start; i < start + count; i++) {
513
- /* getParsed(idx: number) => SankeyParsedData */
514
- const parsed = this.getParsed(i);
515
- const custom = parsed._custom;
516
- const y = yScale.getPixelForValue(parsed.y);
517
- this.updateElement(
518
- elems[i],
519
- i,
520
- {
521
- x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderWidth,
522
- y,
523
- x2: xScale.getPixelForValue(custom.x) - borderWidth,
524
- y2: yScale.getPixelForValue(custom.y),
525
- from: custom.from,
526
- to: custom.to,
527
- progress: mode === 'reset' ? 0 : 1,
528
- height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
529
- options: this.resolveDataElementOptions(i, mode)
530
- },
531
- mode);
532
- }
533
-
534
- this.updateSharedOptions(sharedOptions, mode);
535
- }
536
-
537
- _drawLabels() {
538
- const ctx = this._ctx;
539
- const nodes = this._nodes || new Map();
540
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
541
- const size = validateSizeValue(dataset.size);
542
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
543
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
544
- const labels = dataset.labels;
545
- const {xScale, yScale} = this._cachedMeta;
546
-
547
- ctx.save();
548
- const chartArea = this.chart.chartArea;
549
- for (const node of nodes.values()) {
550
- const x = xScale.getPixelForValue(node.x);
551
- const y = yScale.getPixelForValue(node.y);
552
-
553
- const max = Math[size](node.in || node.out, node.out || node.in);
554
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
555
- const label = labels && labels[node.key] || node.key;
556
- let textX = x;
557
- ctx.fillStyle = dataset.color || 'black';
558
- ctx.textBaseline = 'middle';
559
- if (x < chartArea.width / 2) {
560
- ctx.textAlign = 'left';
561
- textX += nodeWidth + borderWidth + 4;
562
- } else {
563
- ctx.textAlign = 'right';
564
- textX -= borderWidth + 4;
565
- }
566
- this._drawLabel(label, y, height, ctx, textX);
567
- }
568
- ctx.restore();
569
- }
570
-
571
- /**
572
- * @param {string} label
573
- * @param {number} y
574
- * @param {number} height
575
- * @param {CanvasRenderingContext2D} ctx
576
- * @param {number} textX
577
- * @private
578
- */
579
- _drawLabel(label, y, height, ctx, textX) {
580
- const font = helpers.toFont(this.options.font, this.chart.options.font);
581
- const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
582
- const linesLength = lines.length;
583
- const middle = y + height / 2;
584
- const textHeight = font.lineHeight;
585
- const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
586
-
587
- ctx.font = font.string;
588
-
589
- if (linesLength > 1) {
590
- const top = middle - (textHeight * linesLength / 2) + padding;
591
- for (let i = 0; i < linesLength; i++) {
592
- ctx.fillText(lines[i], textX, top + (i * textHeight));
593
- }
594
- } else {
595
- ctx.fillText(label, textX, middle);
596
- }
597
- }
598
-
599
- _drawNodes() {
600
- const ctx = this._ctx;
601
- const nodes = this._nodes || new Map();
602
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
603
- const size = validateSizeValue(dataset.size);
604
- const {xScale, yScale} = this._cachedMeta;
605
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
606
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
607
-
608
- ctx.save();
609
- ctx.strokeStyle = dataset.borderColor || 'black';
610
- ctx.lineWidth = borderWidth;
611
-
612
- for (const node of nodes.values()) {
613
- ctx.fillStyle = node.color;
614
- const x = xScale.getPixelForValue(node.x);
615
- const y = yScale.getPixelForValue(node.y);
616
-
617
- const max = Math[size](node.in || node.out, node.out || node.in);
618
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
619
- if (borderWidth) {
620
- ctx.strokeRect(x, y, nodeWidth, height);
621
- }
622
- ctx.fillRect(x, y, nodeWidth, height);
623
- }
624
- ctx.restore();
625
- }
626
-
627
- /**
628
- * That's where the drawing process happens
629
- */
630
- draw() {
631
- const ctx = this._ctx;
632
- const data = this.getMeta().data || []; /* Array<Flow> */
633
-
634
- // Set node colors
635
- const active = [];
636
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
637
- const flow = data[i]; /* Flow at index i */
638
- flow.from.color = flow.options.colorFrom;
639
- flow.to.color = flow.options.colorTo;
640
- if (flow.active) {
641
- active.push(flow);
642
- }
643
- }
644
- // Make sure nodes connected to hovered flows are using hover colors.
645
- for (const flow of active) {
646
- flow.from.color = flow.options.colorFrom;
647
- flow.to.color = flow.options.colorTo;
648
- }
649
-
650
- /* draw SankeyNodes on the canvas */
651
- this._drawNodes();
652
-
653
- /* draw Flow elements on the canvas */
654
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
655
- data[i].draw(ctx);
656
- }
657
-
658
- /* draw labels (for SankeyNodes) on the canvas */
659
- this._drawLabels();
660
- }
661
- }
662
-
663
- SankeyController.id = 'sankey';
664
-
665
- SankeyController.defaults = {
666
- dataElementType: 'flow',
667
- animations: {
668
- numbers: {
669
- type: 'number',
670
- properties: ['x', 'y', 'x2', 'y2', 'height']
671
- },
672
- progress: {
673
- easing: 'linear',
674
- duration: (ctx) => ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
675
- delay: (ctx) => ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined,
676
- },
677
- colors: {
678
- type: 'color',
679
- properties: ['colorFrom', 'colorTo'],
680
- },
681
- },
682
- transitions: {
683
- hide: {
684
- animations: {
685
- colors: {
686
- type: 'color',
687
- properties: ['colorFrom', 'colorTo'],
688
- to: 'transparent'
689
- }
690
- }
691
- },
692
- show: {
693
- animations: {
694
- colors: {
695
- type: 'color',
696
- properties: ['colorFrom', 'colorTo'],
697
- from: 'transparent'
698
- }
699
- }
700
- }
701
- }
702
- };
703
-
704
- SankeyController.overrides = {
705
- interaction: {
706
- mode: 'nearest',
707
- intersect: true
708
- },
709
- datasets: {
710
- clip: false,
711
- parsing: true
712
- },
713
- plugins: {
714
- tooltip: {
715
- callbacks: {
716
- title() {
717
- return '';
718
- },
719
- label(context) {
720
- const item = context.dataset.data[context.dataIndex];
721
- return item.from + ' -> ' + item.to + ': ' + item.flow;
722
- }
723
- },
724
- },
725
- legend: {
726
- display: false,
727
- },
728
- },
729
- scales: {
730
- x: {
731
- type: 'linear',
732
- bounds: 'data',
733
- display: false,
734
- min: 0,
735
- offset: false,
736
- },
737
- y: {
738
- type: 'linear',
739
- bounds: 'data',
740
- display: false,
741
- min: 0,
742
- reverse: true,
743
- offset: false,
744
- },
745
- },
746
- layout: {
747
- padding: {
748
- top: 3,
749
- left: 3,
750
- right: 13,
751
- bottom: 3,
752
- },
753
- },
754
- };
755
-
756
- /**
757
- * @typedef {{x: number, y: number}} ControlPoint
758
- * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
759
- *
760
- * @param {number} x
761
- * @param {number} y
762
- * @param {number} x2
763
- * @param {number} y2
764
- * @return {ControlPoints}
765
- */
766
- const controlPoints = (x, y, x2, y2) => x < x2
767
- ? {
768
- cp1: {x: x + (x2 - x) / 3 * 2, y},
769
- cp2: {x: x + (x2 - x) / 3, y: y2}
770
- }
771
- : {
772
- cp1: {x: x - (x - x2) / 3, y: 0},
773
- cp2: {x: x2 + (x - x2) / 3, y: 0}
774
- };
775
-
776
- /**
777
- *
778
- * @param {ControlPoint} p1
779
- * @param {ControlPoint} p2
780
- * @param {number} t
781
- * @return {ControlPoint}
782
- */
783
- const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
784
-
785
- /**
786
- * @param {CanvasRenderingContext2D} ctx
787
- * @param {Flow} flow
788
- */
789
- function setStyle(ctx, {x, x2, options}) {
790
- let fill;
791
-
792
- if (options.colorMode === 'from') {
793
- /**
794
- * @todo remove the alpha and use tha alpha provided in colorFrom / colorTo in next major version
795
- */
796
- fill = helpers.color(options.colorFrom).alpha(options.alpha).rgbString();
797
- } else if (options.colorMode === 'to') {
798
- fill = helpers.color(options.colorTo).alpha(options.alpha).rgbString();
799
- } else {
800
- fill = ctx.createLinearGradient(x, 0, x2, 0);
801
- fill.addColorStop(0, helpers.color(options.colorFrom).alpha(options.alpha).rgbString());
802
- fill.addColorStop(1, helpers.color(options.colorTo).alpha(options.alpha).rgbString());
803
- }
804
-
805
- ctx.fillStyle = fill;
806
- ctx.strokeStyle = fill;
807
- ctx.lineWidth = 0.5;
808
- }
809
-
810
- class Flow extends chart_js.Element {
811
-
812
- /**
813
- * @param {FlowConfig} cfg
814
- */
815
- constructor(cfg) {
816
- super();
817
-
818
- this.options = undefined;
819
- this.x = undefined;
820
- this.y = undefined;
821
- this.x2 = undefined;
822
- this.y2 = undefined;
823
- this.height = undefined;
824
-
825
- if (cfg) {
826
- Object.assign(this, cfg);
827
- }
828
- }
829
-
830
- /**
831
- * @param {CanvasRenderingContext2D} ctx
832
- */
833
- draw(ctx) {
834
- const me = this;
835
- const {x, x2, y, y2, height, progress} = me;
836
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
837
-
838
- if (progress === 0) {
839
- return;
840
- }
841
- ctx.save();
842
- if (progress < 1) {
843
- ctx.beginPath();
844
- ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
845
- ctx.clip();
846
- }
847
-
848
- setStyle(ctx, me);
849
-
850
- ctx.beginPath();
851
- ctx.moveTo(x, y);
852
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
853
- ctx.lineTo(x2, y2 + height);
854
- ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
855
- ctx.lineTo(x, y);
856
- ctx.stroke();
857
- ctx.closePath();
858
-
859
- ctx.fill();
860
-
861
- ctx.restore();
862
- }
863
-
864
- /**
865
- * @param {number} mouseX
866
- * @param {number} mouseY
867
- * @param {boolean} useFinalPosition
868
- * @return {boolean}
869
- */
870
- inRange(mouseX, mouseY, useFinalPosition) {
871
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
872
- if (mouseX < x || mouseX > x2) {
873
- return false;
874
- }
875
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
876
- const t = (mouseX - x) / (x2 - x);
877
- const p1 = {x, y};
878
- const p2 = {x: x2, y: y2};
879
- const a = pointInLine(p1, cp1, t);
880
- const b = pointInLine(cp1, cp2, t);
881
- const c = pointInLine(cp2, p2, t);
882
- const d = pointInLine(a, b, t);
883
- const e = pointInLine(b, c, t);
884
- const topY = pointInLine(d, e, t).y;
885
- return mouseY >= topY && mouseY <= topY + height;
886
- }
887
-
888
- /**
889
- * @param {number} mouseX
890
- * @param {boolean} useFinalPosition
891
- * @return {boolean}
892
- */
893
- inXRange(mouseX, useFinalPosition) {
894
- const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition);
895
- return mouseX >= x && mouseX <= x2;
896
- }
897
-
898
- /**
899
- * @param {number} mouseY
900
- * @param {boolean} useFinalPosition
901
- * @return {boolean}
902
- */
903
- inYRange(mouseY, useFinalPosition) {
904
- const {y, y2, height} = this.getProps(['y', 'y2', 'height'], useFinalPosition);
905
- const minY = Math.min(y, y2);
906
- const maxY = Math.max(y, y2) + height;
907
- return mouseY >= minY && mouseY <= maxY;
908
- }
909
-
910
- /**
911
- * @param {boolean} useFinalPosition
912
- * @return {{x: number, y:number}}
913
- */
914
- getCenterPoint(useFinalPosition) {
915
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
916
- return {
917
- x: (x + x2) / 2,
918
- y: (y + y2 + height) / 2
919
- };
920
- }
921
-
922
- tooltipPosition(useFinalPosition) {
923
- return this.getCenterPoint(useFinalPosition);
924
- }
925
-
926
- /**
927
- * @param {"x" | "y"} axis
928
- * @return {number}
929
- */
930
- getRange(axis) {
931
- return axis === 'x' ? this.width / 2 : this.height / 2;
932
- }
933
- }
934
-
935
- Flow.id = 'flow';
936
- Flow.defaults = {
937
- colorFrom: 'red',
938
- colorTo: 'green',
939
- colorMode: 'gradient',
940
- alpha: 0.5,
941
- hoverColorFrom: (ctx, options) => helpers.getHoverColor(options.colorFrom),
942
- hoverColorTo: (ctx, options) => helpers.getHoverColor(options.colorTo)
943
- };
944
-
945
- chart_js.Chart.register(SankeyController, Flow);
946
-
947
- }));