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