chartjs-chart-sankey 0.17.0 → 0.19.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.
- package/dist/chartjs-chart-sankey.cjs +89 -29
- package/dist/chartjs-chart-sankey.esm.js +89 -29
- package/dist/chartjs-chart-sankey.min.js +2 -2
- package/dist/controller.d.cts +2 -0
- package/dist/controller.d.ts +2 -0
- package/dist/lib/layout.d.ts +7 -2
- package/dist/types.d.cts +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.19.0
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -445,10 +445,19 @@ const nodeByXYSize = (a, b)=>{
|
|
|
445
445
|
function createColumnGapState() {
|
|
446
446
|
return {
|
|
447
447
|
lastAfter: 0,
|
|
448
|
+
lastExtraHalf: 0,
|
|
448
449
|
realCount: 0,
|
|
449
450
|
realCumOffset: 0,
|
|
450
451
|
yHistory: []
|
|
451
452
|
};
|
|
453
|
+
}
|
|
454
|
+
function extraHalfFor(node, minSizes, scale) {
|
|
455
|
+
const minSizeUnits = (minSizes.get(node.key) ?? 0) * scale;
|
|
456
|
+
return Math.max(0, minSizeUnits - node.size) / 2;
|
|
457
|
+
}
|
|
458
|
+
function stretchedSize(node, minSizes, scale) {
|
|
459
|
+
const minSizeUnits = (minSizes.get(node.key) ?? 0) * scale;
|
|
460
|
+
return Math.max(node.size, minSizeUnits);
|
|
452
461
|
}
|
|
453
462
|
function countCrossColumnPaddings(grid, colIdx, y, ownPaddings) {
|
|
454
463
|
let paddings = ownPaddings;
|
|
@@ -461,17 +470,50 @@ function countCrossColumnPaddings(grid, colIdx, y, ownPaddings) {
|
|
|
461
470
|
}
|
|
462
471
|
return paddings;
|
|
463
472
|
}
|
|
464
|
-
function offsetForNode(state, gap, paddings) {
|
|
473
|
+
function offsetForNode(state, gap, paddings, scale, extraHalf) {
|
|
465
474
|
const realNodesAbove = state.realCount;
|
|
466
|
-
const
|
|
475
|
+
const before = gap.before * scale;
|
|
476
|
+
const after = gap.after * scale;
|
|
477
|
+
const transitionGap = realNodesAbove > 0 ? Math.max(state.lastAfter, before) + state.lastExtraHalf + extraHalf : 0;
|
|
467
478
|
const realCumOffset = state.realCumOffset + transitionGap;
|
|
468
479
|
const virtualLevels = paddings - realNodesAbove;
|
|
469
480
|
state.realCount = realNodesAbove + 1;
|
|
470
481
|
state.realCumOffset = realCumOffset;
|
|
471
|
-
state.lastAfter =
|
|
472
|
-
|
|
482
|
+
state.lastAfter = after;
|
|
483
|
+
state.lastExtraHalf = extraHalf;
|
|
484
|
+
return realCumOffset + virtualLevels * before;
|
|
485
|
+
}
|
|
486
|
+
function addEvenPadding(nodeArray, gaps, scale, minSizes) {
|
|
487
|
+
let maxY = 0;
|
|
488
|
+
let columnX;
|
|
489
|
+
let prev;
|
|
490
|
+
let prevGap;
|
|
491
|
+
let prevExtraHalf = 0;
|
|
492
|
+
for (const node of nodeArray){
|
|
493
|
+
const x = nodeX$1(node);
|
|
494
|
+
const gap = gaps.get(node.key) ?? {
|
|
495
|
+
after: 0,
|
|
496
|
+
before: 0
|
|
497
|
+
};
|
|
498
|
+
const extraHalf = extraHalfFor(node, minSizes, scale);
|
|
499
|
+
if (x !== columnX) {
|
|
500
|
+
columnX = x;
|
|
501
|
+
} else if (prev && prevGap) {
|
|
502
|
+
node.y = nodeY$1(prev) + prev.size + Math.max(prevGap.after, gap.before) * scale + prevExtraHalf + extraHalf;
|
|
503
|
+
}
|
|
504
|
+
prev = node;
|
|
505
|
+
prevGap = gap;
|
|
506
|
+
prevExtraHalf = extraHalf;
|
|
507
|
+
const H = stretchedSize(node, minSizes, scale);
|
|
508
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out), nodeY$1(node) + (node.size + H) / 2);
|
|
509
|
+
}
|
|
510
|
+
return maxY;
|
|
473
511
|
}
|
|
474
|
-
function addPadding(nodeArray, gaps) {
|
|
512
|
+
function addPadding(nodeArray, gaps, scale = 1, mode = 'auto', minSizes = new Map()) {
|
|
513
|
+
nodeArray.sort(nodeByXYSize);
|
|
514
|
+
if (mode === 'even') {
|
|
515
|
+
return addEvenPadding(nodeArray, gaps, scale, minSizes);
|
|
516
|
+
}
|
|
475
517
|
let maxY = 0;
|
|
476
518
|
const columnXs = new Map();
|
|
477
519
|
const grid = [];
|
|
@@ -484,7 +526,6 @@ function offsetForNode(state, gap, paddings) {
|
|
|
484
526
|
}
|
|
485
527
|
return columnXs.get(x) ?? 0;
|
|
486
528
|
};
|
|
487
|
-
nodeArray.sort(nodeByXYSize);
|
|
488
529
|
for (const node of nodeArray){
|
|
489
530
|
const colIdx = getColIndex(nodeX$1(node));
|
|
490
531
|
const state = grid[colIdx];
|
|
@@ -493,6 +534,7 @@ function offsetForNode(state, gap, paddings) {
|
|
|
493
534
|
before: 0
|
|
494
535
|
};
|
|
495
536
|
const y = nodeY$1(node);
|
|
537
|
+
const extraHalf = extraHalfFor(node, minSizes, scale);
|
|
496
538
|
if (y) {
|
|
497
539
|
state.yHistory.push(y);
|
|
498
540
|
let paddings = state.yHistory.length;
|
|
@@ -500,12 +542,14 @@ function offsetForNode(state, gap, paddings) {
|
|
|
500
542
|
paddings = countCrossColumnPaddings(grid, colIdx, y, paddings);
|
|
501
543
|
while(state.yHistory.length < paddings)state.yHistory.push(y);
|
|
502
544
|
}
|
|
503
|
-
node.y = y + offsetForNode(state, gap, paddings);
|
|
545
|
+
node.y = y + offsetForNode(state, gap, paddings, scale, extraHalf);
|
|
504
546
|
} else {
|
|
505
547
|
state.realCount += 1;
|
|
506
|
-
state.lastAfter = gap.after;
|
|
548
|
+
state.lastAfter = gap.after * scale;
|
|
549
|
+
state.lastExtraHalf = extraHalf;
|
|
507
550
|
}
|
|
508
|
-
|
|
551
|
+
const H = stretchedSize(node, minSizes, scale);
|
|
552
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out), nodeY$1(node) + (node.size + H) / 2);
|
|
509
553
|
}
|
|
510
554
|
return maxY;
|
|
511
555
|
}
|
|
@@ -536,21 +580,14 @@ function sortFlows(nodeArray) {
|
|
|
536
580
|
});
|
|
537
581
|
});
|
|
538
582
|
}
|
|
539
|
-
function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
583
|
+
function layout(nodes, data, { priority, height, nodePadding, nodePaddingMode, nodeMinSize, modeX }) {
|
|
540
584
|
const nodeArray = [
|
|
541
585
|
...nodes.values()
|
|
542
586
|
];
|
|
543
587
|
const maxX = calculateX(nodes, data, modeX ?? 'edge');
|
|
544
588
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
545
589
|
const scale = maxY / height;
|
|
546
|
-
const
|
|
547
|
-
for (const [key, gap] of nodePadding){
|
|
548
|
-
scaledGaps.set(key, {
|
|
549
|
-
after: gap.after * scale,
|
|
550
|
-
before: gap.before * scale
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
const maxYWithPadding = addPadding(nodeArray, scaledGaps);
|
|
590
|
+
const maxYWithPadding = addPadding(nodeArray, nodePadding, scale, nodePaddingMode ?? 'auto', nodeMinSize ?? new Map());
|
|
554
591
|
sortFlows(nodeArray);
|
|
555
592
|
return {
|
|
556
593
|
maxX,
|
|
@@ -646,22 +683,30 @@ function getColumnPadding(nodeWidth, orientation, chart) {
|
|
|
646
683
|
const trailingSpace = orientation === 'vertical' ? chart.height - chart.chartArea.bottom : chart.width - chart.chartArea.right;
|
|
647
684
|
return Math.max(0, nodeWidth + 3 - trailingSpace);
|
|
648
685
|
}
|
|
649
|
-
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation) {
|
|
686
|
+
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation, minSize) {
|
|
650
687
|
if (orientation === 'vertical') {
|
|
651
|
-
const
|
|
688
|
+
const x1 = xScale.getPixelForValue(nodeY(node));
|
|
689
|
+
const x2 = xScale.getPixelForValue(nodeY(node) + size);
|
|
690
|
+
const left = Math.min(x1, x2);
|
|
691
|
+
const naturalWidth = Math.abs(x2 - x1);
|
|
692
|
+
const width = Math.max(naturalWidth, minSize);
|
|
652
693
|
return {
|
|
653
694
|
height: nodeWidth,
|
|
654
|
-
width
|
|
655
|
-
x,
|
|
695
|
+
width,
|
|
696
|
+
x: left - (width - naturalWidth) / 2,
|
|
656
697
|
y: getColumnPixel(yScale, nodeX(node), maxColumn, columnPadding)
|
|
657
698
|
};
|
|
658
699
|
}
|
|
659
|
-
const
|
|
700
|
+
const y1 = yScale.getPixelForValue(nodeY(node));
|
|
701
|
+
const y2 = yScale.getPixelForValue(nodeY(node) + size);
|
|
702
|
+
const top = Math.min(y1, y2);
|
|
703
|
+
const naturalHeight = Math.abs(y2 - y1);
|
|
704
|
+
const height = Math.max(naturalHeight, minSize);
|
|
660
705
|
return {
|
|
661
|
-
height
|
|
706
|
+
height,
|
|
662
707
|
width: nodeWidth,
|
|
663
708
|
x: getColumnPixel(xScale, nodeX(node), maxColumn, columnPadding),
|
|
664
|
-
y
|
|
709
|
+
y: top - (height - naturalHeight) / 2
|
|
665
710
|
};
|
|
666
711
|
}
|
|
667
712
|
function resolveNodeGap(option, node) {
|
|
@@ -677,6 +722,9 @@ function resolveNodeGap(option, node) {
|
|
|
677
722
|
before: resolved.before ?? 10
|
|
678
723
|
};
|
|
679
724
|
}
|
|
725
|
+
function resolveNodeMinSize(option, node) {
|
|
726
|
+
return resolveNodeOption(option ?? 0, node) ?? 0;
|
|
727
|
+
}
|
|
680
728
|
function resolveNodeLabelStyle(options, node) {
|
|
681
729
|
const { backgroundColor, borderRadius = 0, color, display, font, padding = 4, position } = options.nodeLabels ?? {};
|
|
682
730
|
return {
|
|
@@ -689,6 +737,10 @@ function resolveNodeLabelStyle(options, node) {
|
|
|
689
737
|
position: resolveNodeOption(position, node) ?? 'auto'
|
|
690
738
|
};
|
|
691
739
|
}
|
|
740
|
+
const NODE_SCOPED_OPTIONS = new Set([
|
|
741
|
+
'nodeMinSize',
|
|
742
|
+
'nodePadding'
|
|
743
|
+
]);
|
|
692
744
|
class SankeyController extends chart_js.DatasetController {
|
|
693
745
|
parseObjectData(meta, data, start, count) {
|
|
694
746
|
const sankeyData = getParsedData(data, this.options.parsing);
|
|
@@ -698,13 +750,17 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
698
750
|
const orientation = this.options.orientation ?? 'horizontal';
|
|
699
751
|
this._nodes = nodes;
|
|
700
752
|
const nodeGaps = new Map();
|
|
753
|
+
const nodeMinSizes = new Map();
|
|
701
754
|
for (const node of nodes.values()){
|
|
702
755
|
nodeGaps.set(node.key, resolveNodeGap(this.options.nodePadding, node));
|
|
756
|
+
nodeMinSizes.set(node.key, resolveNodeMinSize(this.options.nodeMinSize, node));
|
|
703
757
|
}
|
|
704
758
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
705
759
|
height: orientation === 'vertical' ? this.chart.width : this.chart.height,
|
|
706
760
|
modeX: this.options.modeX,
|
|
761
|
+
nodeMinSize: nodeMinSizes,
|
|
707
762
|
nodePadding: nodeGaps,
|
|
763
|
+
nodePaddingMode: this.options.nodePaddingMode,
|
|
708
764
|
priority: !!this.options.priority
|
|
709
765
|
});
|
|
710
766
|
this._maxX = maxX;
|
|
@@ -768,7 +824,8 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
768
824
|
const chartArea = this.chart.chartArea;
|
|
769
825
|
for (const node of nodes.values()){
|
|
770
826
|
const max = getNodeSize(node, size);
|
|
771
|
-
const
|
|
827
|
+
const minSize = resolveNodeMinSize(options.nodeMinSize, node);
|
|
828
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation, minSize);
|
|
772
829
|
const label = labels?.[node.key] ?? node.key;
|
|
773
830
|
const labelStyle = resolveNodeLabelStyle(options, node);
|
|
774
831
|
if (labelStyle.display) {
|
|
@@ -808,7 +865,8 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
808
865
|
ctx.fillStyle = node.color ?? 'black';
|
|
809
866
|
if (!xScale || !yScale) return;
|
|
810
867
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
811
|
-
const
|
|
868
|
+
const minSize = resolveNodeMinSize(this.options.nodeMinSize, node);
|
|
869
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation, minSize);
|
|
812
870
|
if (borderWidth) {
|
|
813
871
|
ctx.strokeRect(x, y, width, height);
|
|
814
872
|
}
|
|
@@ -851,7 +909,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
851
909
|
SankeyController.id = 'sankey';
|
|
852
910
|
SankeyController.descriptors = {
|
|
853
911
|
_indexable: false,
|
|
854
|
-
_scriptable: (name)
|
|
912
|
+
_scriptable: (name)=>!NODE_SCOPED_OPTIONS.has(name),
|
|
855
913
|
nodeLabels: {
|
|
856
914
|
_indexable: false,
|
|
857
915
|
_scriptable: false
|
|
@@ -888,7 +946,9 @@ SankeyController.defaults = {
|
|
|
888
946
|
color: 'black',
|
|
889
947
|
dataElementType: 'flow',
|
|
890
948
|
modeX: 'edge',
|
|
949
|
+
nodeMinSize: 0,
|
|
891
950
|
nodePadding: 10,
|
|
951
|
+
nodePaddingMode: 'auto',
|
|
892
952
|
nodeWidth: 10,
|
|
893
953
|
orientation: 'horizontal',
|
|
894
954
|
transitions: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.19.0
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -442,10 +442,19 @@ const nodeByXYSize = (a, b)=>{
|
|
|
442
442
|
function createColumnGapState() {
|
|
443
443
|
return {
|
|
444
444
|
lastAfter: 0,
|
|
445
|
+
lastExtraHalf: 0,
|
|
445
446
|
realCount: 0,
|
|
446
447
|
realCumOffset: 0,
|
|
447
448
|
yHistory: []
|
|
448
449
|
};
|
|
450
|
+
}
|
|
451
|
+
function extraHalfFor(node, minSizes, scale) {
|
|
452
|
+
const minSizeUnits = (minSizes.get(node.key) ?? 0) * scale;
|
|
453
|
+
return Math.max(0, minSizeUnits - node.size) / 2;
|
|
454
|
+
}
|
|
455
|
+
function stretchedSize(node, minSizes, scale) {
|
|
456
|
+
const minSizeUnits = (minSizes.get(node.key) ?? 0) * scale;
|
|
457
|
+
return Math.max(node.size, minSizeUnits);
|
|
449
458
|
}
|
|
450
459
|
function countCrossColumnPaddings(grid, colIdx, y, ownPaddings) {
|
|
451
460
|
let paddings = ownPaddings;
|
|
@@ -458,17 +467,50 @@ function countCrossColumnPaddings(grid, colIdx, y, ownPaddings) {
|
|
|
458
467
|
}
|
|
459
468
|
return paddings;
|
|
460
469
|
}
|
|
461
|
-
function offsetForNode(state, gap, paddings) {
|
|
470
|
+
function offsetForNode(state, gap, paddings, scale, extraHalf) {
|
|
462
471
|
const realNodesAbove = state.realCount;
|
|
463
|
-
const
|
|
472
|
+
const before = gap.before * scale;
|
|
473
|
+
const after = gap.after * scale;
|
|
474
|
+
const transitionGap = realNodesAbove > 0 ? Math.max(state.lastAfter, before) + state.lastExtraHalf + extraHalf : 0;
|
|
464
475
|
const realCumOffset = state.realCumOffset + transitionGap;
|
|
465
476
|
const virtualLevels = paddings - realNodesAbove;
|
|
466
477
|
state.realCount = realNodesAbove + 1;
|
|
467
478
|
state.realCumOffset = realCumOffset;
|
|
468
|
-
state.lastAfter =
|
|
469
|
-
|
|
479
|
+
state.lastAfter = after;
|
|
480
|
+
state.lastExtraHalf = extraHalf;
|
|
481
|
+
return realCumOffset + virtualLevels * before;
|
|
482
|
+
}
|
|
483
|
+
function addEvenPadding(nodeArray, gaps, scale, minSizes) {
|
|
484
|
+
let maxY = 0;
|
|
485
|
+
let columnX;
|
|
486
|
+
let prev;
|
|
487
|
+
let prevGap;
|
|
488
|
+
let prevExtraHalf = 0;
|
|
489
|
+
for (const node of nodeArray){
|
|
490
|
+
const x = nodeX$1(node);
|
|
491
|
+
const gap = gaps.get(node.key) ?? {
|
|
492
|
+
after: 0,
|
|
493
|
+
before: 0
|
|
494
|
+
};
|
|
495
|
+
const extraHalf = extraHalfFor(node, minSizes, scale);
|
|
496
|
+
if (x !== columnX) {
|
|
497
|
+
columnX = x;
|
|
498
|
+
} else if (prev && prevGap) {
|
|
499
|
+
node.y = nodeY$1(prev) + prev.size + Math.max(prevGap.after, gap.before) * scale + prevExtraHalf + extraHalf;
|
|
500
|
+
}
|
|
501
|
+
prev = node;
|
|
502
|
+
prevGap = gap;
|
|
503
|
+
prevExtraHalf = extraHalf;
|
|
504
|
+
const H = stretchedSize(node, minSizes, scale);
|
|
505
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out), nodeY$1(node) + (node.size + H) / 2);
|
|
506
|
+
}
|
|
507
|
+
return maxY;
|
|
470
508
|
}
|
|
471
|
-
function addPadding(nodeArray, gaps) {
|
|
509
|
+
function addPadding(nodeArray, gaps, scale = 1, mode = 'auto', minSizes = new Map()) {
|
|
510
|
+
nodeArray.sort(nodeByXYSize);
|
|
511
|
+
if (mode === 'even') {
|
|
512
|
+
return addEvenPadding(nodeArray, gaps, scale, minSizes);
|
|
513
|
+
}
|
|
472
514
|
let maxY = 0;
|
|
473
515
|
const columnXs = new Map();
|
|
474
516
|
const grid = [];
|
|
@@ -481,7 +523,6 @@ function offsetForNode(state, gap, paddings) {
|
|
|
481
523
|
}
|
|
482
524
|
return columnXs.get(x) ?? 0;
|
|
483
525
|
};
|
|
484
|
-
nodeArray.sort(nodeByXYSize);
|
|
485
526
|
for (const node of nodeArray){
|
|
486
527
|
const colIdx = getColIndex(nodeX$1(node));
|
|
487
528
|
const state = grid[colIdx];
|
|
@@ -490,6 +531,7 @@ function offsetForNode(state, gap, paddings) {
|
|
|
490
531
|
before: 0
|
|
491
532
|
};
|
|
492
533
|
const y = nodeY$1(node);
|
|
534
|
+
const extraHalf = extraHalfFor(node, minSizes, scale);
|
|
493
535
|
if (y) {
|
|
494
536
|
state.yHistory.push(y);
|
|
495
537
|
let paddings = state.yHistory.length;
|
|
@@ -497,12 +539,14 @@ function offsetForNode(state, gap, paddings) {
|
|
|
497
539
|
paddings = countCrossColumnPaddings(grid, colIdx, y, paddings);
|
|
498
540
|
while(state.yHistory.length < paddings)state.yHistory.push(y);
|
|
499
541
|
}
|
|
500
|
-
node.y = y + offsetForNode(state, gap, paddings);
|
|
542
|
+
node.y = y + offsetForNode(state, gap, paddings, scale, extraHalf);
|
|
501
543
|
} else {
|
|
502
544
|
state.realCount += 1;
|
|
503
|
-
state.lastAfter = gap.after;
|
|
545
|
+
state.lastAfter = gap.after * scale;
|
|
546
|
+
state.lastExtraHalf = extraHalf;
|
|
504
547
|
}
|
|
505
|
-
|
|
548
|
+
const H = stretchedSize(node, minSizes, scale);
|
|
549
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out), nodeY$1(node) + (node.size + H) / 2);
|
|
506
550
|
}
|
|
507
551
|
return maxY;
|
|
508
552
|
}
|
|
@@ -533,21 +577,14 @@ function sortFlows(nodeArray) {
|
|
|
533
577
|
});
|
|
534
578
|
});
|
|
535
579
|
}
|
|
536
|
-
function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
580
|
+
function layout(nodes, data, { priority, height, nodePadding, nodePaddingMode, nodeMinSize, modeX }) {
|
|
537
581
|
const nodeArray = [
|
|
538
582
|
...nodes.values()
|
|
539
583
|
];
|
|
540
584
|
const maxX = calculateX(nodes, data, modeX ?? 'edge');
|
|
541
585
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
542
586
|
const scale = maxY / height;
|
|
543
|
-
const
|
|
544
|
-
for (const [key, gap] of nodePadding){
|
|
545
|
-
scaledGaps.set(key, {
|
|
546
|
-
after: gap.after * scale,
|
|
547
|
-
before: gap.before * scale
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
const maxYWithPadding = addPadding(nodeArray, scaledGaps);
|
|
587
|
+
const maxYWithPadding = addPadding(nodeArray, nodePadding, scale, nodePaddingMode ?? 'auto', nodeMinSize ?? new Map());
|
|
551
588
|
sortFlows(nodeArray);
|
|
552
589
|
return {
|
|
553
590
|
maxX,
|
|
@@ -643,22 +680,30 @@ function getColumnPadding(nodeWidth, orientation, chart) {
|
|
|
643
680
|
const trailingSpace = orientation === 'vertical' ? chart.height - chart.chartArea.bottom : chart.width - chart.chartArea.right;
|
|
644
681
|
return Math.max(0, nodeWidth + 3 - trailingSpace);
|
|
645
682
|
}
|
|
646
|
-
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation) {
|
|
683
|
+
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation, minSize) {
|
|
647
684
|
if (orientation === 'vertical') {
|
|
648
|
-
const
|
|
685
|
+
const x1 = xScale.getPixelForValue(nodeY(node));
|
|
686
|
+
const x2 = xScale.getPixelForValue(nodeY(node) + size);
|
|
687
|
+
const left = Math.min(x1, x2);
|
|
688
|
+
const naturalWidth = Math.abs(x2 - x1);
|
|
689
|
+
const width = Math.max(naturalWidth, minSize);
|
|
649
690
|
return {
|
|
650
691
|
height: nodeWidth,
|
|
651
|
-
width
|
|
652
|
-
x,
|
|
692
|
+
width,
|
|
693
|
+
x: left - (width - naturalWidth) / 2,
|
|
653
694
|
y: getColumnPixel(yScale, nodeX(node), maxColumn, columnPadding)
|
|
654
695
|
};
|
|
655
696
|
}
|
|
656
|
-
const
|
|
697
|
+
const y1 = yScale.getPixelForValue(nodeY(node));
|
|
698
|
+
const y2 = yScale.getPixelForValue(nodeY(node) + size);
|
|
699
|
+
const top = Math.min(y1, y2);
|
|
700
|
+
const naturalHeight = Math.abs(y2 - y1);
|
|
701
|
+
const height = Math.max(naturalHeight, minSize);
|
|
657
702
|
return {
|
|
658
|
-
height
|
|
703
|
+
height,
|
|
659
704
|
width: nodeWidth,
|
|
660
705
|
x: getColumnPixel(xScale, nodeX(node), maxColumn, columnPadding),
|
|
661
|
-
y
|
|
706
|
+
y: top - (height - naturalHeight) / 2
|
|
662
707
|
};
|
|
663
708
|
}
|
|
664
709
|
function resolveNodeGap(option, node) {
|
|
@@ -674,6 +719,9 @@ function resolveNodeGap(option, node) {
|
|
|
674
719
|
before: resolved.before ?? 10
|
|
675
720
|
};
|
|
676
721
|
}
|
|
722
|
+
function resolveNodeMinSize(option, node) {
|
|
723
|
+
return resolveNodeOption(option ?? 0, node) ?? 0;
|
|
724
|
+
}
|
|
677
725
|
function resolveNodeLabelStyle(options, node) {
|
|
678
726
|
const { backgroundColor, borderRadius = 0, color, display, font, padding = 4, position } = options.nodeLabels ?? {};
|
|
679
727
|
return {
|
|
@@ -686,6 +734,10 @@ function resolveNodeLabelStyle(options, node) {
|
|
|
686
734
|
position: resolveNodeOption(position, node) ?? 'auto'
|
|
687
735
|
};
|
|
688
736
|
}
|
|
737
|
+
const NODE_SCOPED_OPTIONS = new Set([
|
|
738
|
+
'nodeMinSize',
|
|
739
|
+
'nodePadding'
|
|
740
|
+
]);
|
|
689
741
|
class SankeyController extends DatasetController {
|
|
690
742
|
parseObjectData(meta, data, start, count) {
|
|
691
743
|
const sankeyData = getParsedData(data, this.options.parsing);
|
|
@@ -695,13 +747,17 @@ class SankeyController extends DatasetController {
|
|
|
695
747
|
const orientation = this.options.orientation ?? 'horizontal';
|
|
696
748
|
this._nodes = nodes;
|
|
697
749
|
const nodeGaps = new Map();
|
|
750
|
+
const nodeMinSizes = new Map();
|
|
698
751
|
for (const node of nodes.values()){
|
|
699
752
|
nodeGaps.set(node.key, resolveNodeGap(this.options.nodePadding, node));
|
|
753
|
+
nodeMinSizes.set(node.key, resolveNodeMinSize(this.options.nodeMinSize, node));
|
|
700
754
|
}
|
|
701
755
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
702
756
|
height: orientation === 'vertical' ? this.chart.width : this.chart.height,
|
|
703
757
|
modeX: this.options.modeX,
|
|
758
|
+
nodeMinSize: nodeMinSizes,
|
|
704
759
|
nodePadding: nodeGaps,
|
|
760
|
+
nodePaddingMode: this.options.nodePaddingMode,
|
|
705
761
|
priority: !!this.options.priority
|
|
706
762
|
});
|
|
707
763
|
this._maxX = maxX;
|
|
@@ -765,7 +821,8 @@ class SankeyController extends DatasetController {
|
|
|
765
821
|
const chartArea = this.chart.chartArea;
|
|
766
822
|
for (const node of nodes.values()){
|
|
767
823
|
const max = getNodeSize(node, size);
|
|
768
|
-
const
|
|
824
|
+
const minSize = resolveNodeMinSize(options.nodeMinSize, node);
|
|
825
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation, minSize);
|
|
769
826
|
const label = labels?.[node.key] ?? node.key;
|
|
770
827
|
const labelStyle = resolveNodeLabelStyle(options, node);
|
|
771
828
|
if (labelStyle.display) {
|
|
@@ -805,7 +862,8 @@ class SankeyController extends DatasetController {
|
|
|
805
862
|
ctx.fillStyle = node.color ?? 'black';
|
|
806
863
|
if (!xScale || !yScale) return;
|
|
807
864
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
808
|
-
const
|
|
865
|
+
const minSize = resolveNodeMinSize(this.options.nodeMinSize, node);
|
|
866
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation, minSize);
|
|
809
867
|
if (borderWidth) {
|
|
810
868
|
ctx.strokeRect(x, y, width, height);
|
|
811
869
|
}
|
|
@@ -848,7 +906,7 @@ class SankeyController extends DatasetController {
|
|
|
848
906
|
SankeyController.id = 'sankey';
|
|
849
907
|
SankeyController.descriptors = {
|
|
850
908
|
_indexable: false,
|
|
851
|
-
_scriptable: (name)
|
|
909
|
+
_scriptable: (name)=>!NODE_SCOPED_OPTIONS.has(name),
|
|
852
910
|
nodeLabels: {
|
|
853
911
|
_indexable: false,
|
|
854
912
|
_scriptable: false
|
|
@@ -885,7 +943,9 @@ SankeyController.defaults = {
|
|
|
885
943
|
color: 'black',
|
|
886
944
|
dataElementType: 'flow',
|
|
887
945
|
modeX: 'edge',
|
|
946
|
+
nodeMinSize: 0,
|
|
888
947
|
nodePadding: 10,
|
|
948
|
+
nodePaddingMode: 'auto',
|
|
889
949
|
nodeWidth: 10,
|
|
890
950
|
orientation: 'horizontal',
|
|
891
951
|
transitions: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.19.0
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
|
-
!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports,require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["exports","chart.js","chart.js/helpers"],o):o((t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-chart-sankey"]={},t.Chart,t.Chart.helpers)}(this,function(t,o,e){"use strict";const r=t=>void 0!==t;function n(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}function i(t,o){return"function"==typeof t?t(o):t&&"object"==typeof t&&!function(t){const o=Object.prototype.toString.call(t);return"[object CanvasPattern]"===o||"[object CanvasGradient]"===o}(t)?t[o.key]:t}function a(t,o,e,r,n,i,a){t.save(),t.fillStyle=o,a>0?(!function(t,o,e,r,n,i){const a=Math.max(0,Math.min(i,r/2,n/2));t.beginPath(),t.moveTo(o+a,e),t.lineTo(o+r-a,e),t.quadraticCurveTo(o+r,e,o+r,e+a),t.lineTo(o+r,e+n-a),t.quadraticCurveTo(o+r,e+n,o+r-a,e+n),t.lineTo(o+a,e+n),t.quadraticCurveTo(o,e+n,o,e+n-a),t.lineTo(o,e+a),t.quadraticCurveTo(o,e,o+a,e),t.closePath()}(t,e,r,n,i,a),t.fill()):t.fillRect(e,r,n,i),t.restore()}function s(t,o,e){const r=function(t){if(!t)return[];const o=[],e=Array.isArray(t)?t:[t];for(;e.length;){const t=e.pop();"string"==typeof t?o.unshift(...t.split("\n")):Array.isArray(t)?e.push(...t):t&&o.unshift(`${t}`)}return o}(o);if(!r.length)return;const{backgroundColor:n,borderRadius:i,color:s,font:l,lineOffset:h,padding:c}=e,f=function(t,o){return"auto"===t?o:t}(e.position,e.autoPosition),d=Number(l.lineHeight);t.font=l.string;const u=Math.max(...r.map(o=>t.measureText(o).width)),p=d*r.length,y=function(t,o,e){const{borderWidth:r,height:n,padding:i,width:a,x:s,y:l}=o,h={align:"center",x:s+a/2,y:l+n/2};return"left"===t?(h.align="right",h.x=s-r-i):"right"===t?(h.align="left",h.x=s+a+r+i):"top"===t?h.y=l-i-e/2:"bottom"===t&&(h.y=l+n+i+e/2),h}(f,e,p);t.textAlign=y.align,t.textBaseline="middle";const g=u+2*c,x=p+2*c,m=function(t,o,e,r,n){return"left"===t?o-n:"right"===t?o-e-n:o-r/2}(y.align,y.x,u,g,c),b=1===r.length?y.y:y.y-p/2+h,w=b+(r.length-1)*d/2;void 0!==n&&a(t,n,m,w-x/2,g,x,i),t.fillStyle=s;for(let o=0;o<r.length;o++)t.fillText(r[o],y.x,b+o*d)}const l=(t,o)=>o.flow===t.flow?t.index-o.index:o.flow-t.flow;function h(t,{size:o,priority:e,column:r}){const i=new Map;for(let o=0;o<t.length;o++){const{from:e,to:r,flow:n}=t[o],a=i.get(e)??{from:[],in:0,key:e,out:0,size:0,to:[]},s=(e===r?a:i.get(r))??{from:[],in:0,key:r,out:0,size:0,to:[]};a.out+=n,a.to.push({addY:0,flow:n,index:o,key:r,node:s}),1===a.to.length&&i.set(e,a),s.in+=n,s.from.push({addY:0,flow:n,index:o,key:e,node:a}),1===s.from.length&&i.set(r,s)}return((t,o)=>{const e=n(o);for(const o of t.values())o.from.sort(l),o.to.sort(l),o.size=Math[e](o.in||o.out,o.out||o.in)})(i,o),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.priority=o[e.key])})(i,e),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.column=!0,e.x=o[e.key])})(i,r),i}const c=1e-6;function f(t){return t.x??0}function d(t){return t.y??0}const u=(t,o=new Set)=>{const e=[];for(const r of t)o.has(r.key)||(o.add(r.key),e.push(r.key,...u(r.to.map(t=>t.node),o)));return e},p=(t,o)=>{const e=o.filter(t=>0===t.from.length),r=e.map(t=>t.key),n=u(e),i=new Set(n);for(const o of t)i.has(o.from)||i.has(o.to)||(r.push(o.from),i.add(o.from)),i.add(o.to);return r},y=(t,o)=>{const e=new Set(t.filter(t=>o.has(t.from)).map(t=>t.to)),r=[...o],n=r.filter(t=>!e.has(t));return n.length?n:r.slice(0,1)};function g(t,o,e=new Set){let r=0;for(const n of t)e.has(n.node)||(e.add(n.node),r+=n.node[o].length+g(n.node[o],o,e));return r}const x=t=>(o,e)=>g(o.node[t],t)-g(e.node[t],t)||o.node[t].length-e.node[t].length;function m(t,o){if(!t.from.length)return o;t.from.sort(x("from"));for(const e of t.from){const t=e.node;r(t.y)||(t.y=o,m(t,o?o+c:0)),o=Math.max(t.y+t.out,o)}return d(t)+t.size}const b=(t,o)=>Boolean(o&&f(o)<f(t));function w(t,o){if(!t.to.length)return o;t.to.sort(x("to"));for(let e=0;e<t.to.length;e++){const n=t.to[e],i=n.node;r(i.y)||(i.y=o,w(i,o?o+c:0)),b(i,t.to[e+1]?.node)?o+=n.flow:o=Math.max(i.y+Math.max(i.in,i.out),o)}return d(t)+t.size}function M(t,o){return r(t.y)?t.y:(t.y=o,o)}function v(t,o){if(!t.length)return 0;const e=((t,o)=>{const e=[...t].sort((t,o)=>t.size-o.size),r=e[e.length-1].size,n=t.filter(t=>t.size===r),i=n[0];if(1===n.length)return i;if(n.sort((t,o)=>f(t)-f(o)),0===f(i))return i;const a=n[n.length-1];return f(a)===o?a:n[Math.floor(n.length/2)]})(t,o);return e.y=0,m(e,0),w(e,0),function(t,o){const e=t.filter(t=>0===t.x),n=t.filter(t=>t.x===o),i=e.filter(t=>!r(t.y)),a=n.filter(t=>!r(t.y)),s=t.filter(t=>f(t)>0&&f(t)<o&&!r(t.y));let l=e.reduce((t,o)=>Math.max(t,d(o)+o.out||0),0)+c,h=n.reduce((t,o)=>Math.max(t,d(o)+o.in||0),0)+c,u=0;l>=h?(i.forEach(t=>{l=M(t,l),l=Math.max(l+t.out,w(t,l))}),a.forEach(t=>{h=M(t,h),h=Math.max(h+t.in,m(t,h))})):(i.forEach(t=>{l=M(t,l)}),a.forEach(t=>{h=M(t,h),h=Math.max(h+t.in,m(t,h))})),s.forEach(o=>{let e=t.filter(t=>f(t)===f(o)&&r(t.y)).reduce((t,o)=>Math.max(t,d(o)+Math.max(o.in,o.out)),0);e=M(o,e),e=Math.max(e+o.in,m(o,e)),e=Math.max(e+o.out,w(o,e)),u=Math.max(u,e)}),Math.max(l,h,u)}(t,o),((t,o)=>{let e=0;for(let r=0;r<=o;r++){const o=t.filter(t=>f(t)===r).sort((t,o)=>d(t)-d(o));let n=0;for(const t of o)d(t)<n&&(t.y=n),n=d(t)+t.size;e=Math.max(e,n)}return e})(t,o)}const k=(t,o)=>f(t)!==f(o)?f(t)-f(o):d(t)===d(o)?t.size-o.size:d(t)-d(o);function C(t,o,e,r){let n=r;for(let r=0;r<o;r++){const o=t[r].yHistory;for(let t=0;t<o.length&&!(o[t]>e);t++)n=Math.max(t+1,n)}return n}function P(t,o,e){const r=t.realCount,n=r>0?Math.max(t.lastAfter,o.before):0,i=t.realCumOffset+n,a=e-r;return t.realCount=r+1,t.realCumOffset=i,t.lastAfter=o.after,i+a*o.before}function _(t,o,{priority:e,height:n,nodePadding:i,modeX:a}){const s=[...t.values()],l=function(t,o,e){const n=o.filter(t=>t.from!==t.to),i=[...t.keys()],a=[...t.values()],s=new Set(i);let l=0;for(;s.size;){const e=0===l?p(o,a):y(n,s);if(!e.length)throw new Error("Fatal error: Unable to place nodes to columns. Please report this issue.");for(const o of e){const e=t.get(o);e&&!r(e.x)&&(e.x=l),s.delete(o)}s.size&&l++}const h=a.reduce((t,o)=>Math.max(t,f(o)),0);if("edge"===e){const e=new Set(o.map(t=>t.from));i.filter(t=>!e.has(t)).forEach(o=>{const e=t.get(o);e&&!e.column&&(e.x=h)})}return h}(t,o,a??"edge"),h=e?function(t,o){let e=0,r=0;for(let n=0;n<=o;n++){let o=r;const i=t.filter(t=>f(t)===n).sort((t,o)=>(t.priority??0)-(o.priority??0));if(i.length){const o=t.reduce((t,o)=>f(o)>n?Math.min(t,f(o)):t,1/0);r=i[0].to.filter(t=>f(t.node)>o).reduce((t,o)=>t+o.flow,0)||0}for(const t of i)t.y=o,o+=Math.max(t.out,t.in);e=Math.max(o,e)}return e}(s,l):v(s,l),c=h/n,u=new Map;for(const[t,o]of i)u.set(t,{after:o.after*c,before:o.before*c});const g=function(t,o){let e=0;const r=new Map,n=[],i=t=>{if(!r.has(t)){const o=n.length;return r.set(t,o),n.push({lastAfter:0,realCount:0,realCumOffset:0,yHistory:[]}),o}return r.get(t)??0};t.sort(k);for(const r of t){const t=i(f(r)),a=n[t],s=o.get(r.key)??{after:0,before:0},l=d(r);if(l){a.yHistory.push(l);let o=a.yHistory.length;if(r.in)for(o=C(n,t,l,o);a.yHistory.length<o;)a.yHistory.push(l);r.y=l+P(a,s,o)}else a.realCount+=1,a.lastAfter=s.after;e=Math.max(e,d(r)+Math.max(r.in,r.out))}return e}(s,u);return function(t){t.forEach(t=>{const o=t.size,e=o<t.in,r=o<t.out;let n=0,i=t.from.length;t.from.sort((t,o)=>d(t.node)+t.node.out/2-(d(o.node)+o.node.out/2)).forEach((t,r)=>{e?t.addY=r*(o-t.flow)/(i-1):(t.addY=n,n+=t.flow)}),n=0,i=t.to.length,t.to.sort((t,o)=>d(t.node)+t.node.in/2-(d(o.node)+o.node.in/2)).forEach((t,e)=>{r?t.addY=e*(o-t.flow)/(i-1):(t.addY=n,n+=t.flow)})})}(s),{maxX:l,maxY:g}}function T(t){return t.x??0}function S(t){return t.y??0}function z(t,o){return Math[o](t.in||t.out,t.out||t.in)}function F(t,o,e,r){return"vertical"===r?o<(e.top+e.bottom)/2?"bottom":"top":t<(e.left+e.right)/2?"right":"left"}function E(t,o,e){for(const r of t)if(r.key===o&&r.index===e)return r.addY;return 0}function O(t,o,e,r,n,i,a,s,l){return"vertical"===l?{_custom:{flow:n,from:t,height:a.parse(n,i),to:o,x:a.parse(r,i),y:s.parse(T(o),i)},x:a.parse(e,i),y:s.parse(T(t),i)}:{_custom:{flow:n,from:t,height:s.parse(n,i),to:o,x:a.parse(T(o),i),y:s.parse(r,i)},x:a.parse(T(t),i),y:s.parse(e,i)}}function R(t,o,e,r,n,i,a,s){const l=t._custom,h=o.getPixelForValue(t.x),c=e.getPixelForValue(t.y);return"vertical"===s?{flow:l.flow,from:l.from,height:0,to:l.to,width:Math.abs(o.getPixelForValue(t.x+l.height)-h),x:h,x2:o.getPixelForValue(l.x),y:W(e,t.y,r,i)+n+a,y2:W(e,l.y,r,i)-a}:{flow:l.flow,from:l.from,height:Math.abs(e.getPixelForValue(t.y+l.height)-c),to:l.to,width:0,x:W(o,t.x,r,i)+n+a,x2:W(o,l.x,r,i)-a,y:c,y2:e.getPixelForValue(l.y)}}function W(t,o,e,r){const n=t.getPixelForValue(o);return e?n-o/e*r:n}function X(t,o,e){const r="vertical"===o?e.height-e.chartArea.bottom:e.width-e.chartArea.right;return Math.max(0,t+3-r)}function Y(t,o,e,r,n,i,a,s){if("vertical"===s){const s=e.getPixelForValue(S(t));return{height:i,width:Math.abs(e.getPixelForValue(S(t)+o)-s),x:s,y:W(r,T(t),n,a)}}const l=r.getPixelForValue(S(t));return{height:Math.abs(r.getPixelForValue(S(t)+o)-l),width:i,x:W(e,T(t),n,a),y:l}}function j(t,o){const e=i(t??10,o)??10;return"number"==typeof e?{after:e,before:e}:{after:e.after??10,before:e.before??10}}function A(t,o){const{backgroundColor:e,borderRadius:r=0,color:n,display:a,font:s,padding:l=4,position:h}=t.nodeLabels??{};return{backgroundColor:i(e,o),borderRadius:r,color:i(n,o)??t.color??"black",display:i(a,o)??!0,font:s,padding:l,position:i(h,o)??"auto"}}class V extends o.DatasetController{parseObjectData(t,o,e,r){const n=((t,o)=>{const{from:e="from",to:r="to",flow:n="flow"}=o;return t.map(({[e]:t,[r]:o,[n]:i})=>({flow:i,from:t,to:o}))})(o,this.options.parsing),{xScale:i,yScale:a}=t,s=[],l=h(n,this.options),c=this.options.orientation??"horizontal";this._nodes=l;const f=new Map;for(const t of l.values())f.set(t.key,j(this.options.nodePadding,t));const{maxX:d,maxY:u}=_(l,n,{height:"vertical"===c?this.chart.width:this.chart.height,modeX:this.options.modeX,nodePadding:f,priority:!!this.options.priority});if(this._maxX=d,this._maxY=u,!i||!a)return[];for(let t=0,o=n.length;t<o;++t){const o=n[t],e=l.get(o.from),r=l.get(o.to);if(!e||!r)continue;const h=S(e)+E(e.to,o.to,t),f=S(r)+E(r.from,o.from,t);s.push(O(e,r,h,f,o.flow,t,i,a,c))}return s.slice(e,e+r)}getMinMax(t){return{max:t===("vertical"===this.options.orientation?this._cachedMeta.yScale:this._cachedMeta.xScale)?this._maxX:this._maxY,min:0}}update(t){const{data:o}=this._cachedMeta;this.updateElements(o,0,o.length,t)}updateElements(t,o,e,r){const{xScale:n,yScale:i}=this._cachedMeta;if(!n||!i)return;const a=this.resolveDataElementOptions(o,r),s=this.getSharedOptions(a),{borderWidth:l,nodeWidth:h=10,orientation:c="horizontal"}=this.options,f=X(h,c,this.chart),d=l?l/2+.5:0;for(let a=o;a<o+e;a++){const o=this.getParsed(a);this.updateElement(t[a],a,{options:this.resolveDataElementOptions(a,r),progress:"reset"===r?0:1,...R(o,n,i,this._maxX,h,f,d,c)},r)}s&&this.updateSharedOptions(s,r,a)}_drawLabels(){const t=this.chart.ctx,r=this.options,i=this._nodes||new Map,a=n(r.size),l=r.labels,{borderWidth:h=1,nodeWidth:c=10,orientation:f="horizontal"}=r,d=X(c,f,this.chart),u=r.font??this.chart.options.font??o.Chart.defaults.font,{xScale:p,yScale:y}=this._cachedMeta;if(!p||!y)return;t.save();const g=this.chart.chartArea;for(const o of i.values()){const n=z(o,a),{height:i,width:x,x:m,y:b}=Y(o,n,p,y,this._maxX,c,d,f),w=l?.[o.key]??o.key,M=A(r,o);if(M.display){const o=e.toFont(M.font??u);s(t,w,{autoPosition:F(m,b,g,f),backgroundColor:M.backgroundColor,borderRadius:M.borderRadius,borderWidth:h,color:M.color,font:o,height:i,lineOffset:e.valueOrDefault(r.padding,o.lineHeight/2),padding:M.padding,position:M.position,width:x,x:m,y:b})}}t.restore()}_drawNodes(){const t=this.chart.ctx,o=this._nodes||new Map,{borderColor:e,borderWidth:r=0,nodeWidth:i=10,orientation:a="horizontal",size:s}=this.options,l=X(i,a,this.chart),h=n(s),{xScale:c,yScale:f}=this._cachedMeta;t.save(),e&&r&&(t.strokeStyle=e,t.lineWidth=r);for(const e of o.values()){if(t.fillStyle=e.color??"black",!c||!f)return;const o=Math[h](e.in||e.out,e.out||e.in),{height:n,width:s,x:d,y:u}=Y(e,o,c,f,this._maxX,i,l,a);r&&t.strokeRect(d,u,s,n),t.fillRect(d,u,s,n)}t.restore()}draw(){const t=this.chart.ctx,o=this.getMeta().data??[],e=[];for(let t=0,r=o.length;t<r;++t){const r=o[t];r.from&&r.to&&(r.from.color=r.options.colorFrom,r.to.color=r.options.colorTo,r.active&&e.push(r))}for(const t of e)t.from&&t.to&&(t.from.color=t.options.colorFrom,t.to.color=t.options.colorTo);this._drawNodes();for(let e=0,r=o.length;e<r;++e)o[e].draw(t);this._drawLabels()}constructor(...t){super(...t),this._nodes=new Map,this._maxX=0,this._maxY=0}}V.id="sankey",V.descriptors={_indexable:!1,_scriptable:t=>"nodePadding"!==t,nodeLabels:{_indexable:!1,_scriptable:!1}},V.defaults={animations:{colors:{properties:["colorFrom","colorTo"],type:"color"},numbers:{properties:["x","y","x2","y2","height","width"],type:"number"},progress:{delay:t=>"data"===t.type?500*t.parsed["vertical"===t.dataset.orientation?"y":"x"]+20*t.dataIndex:void 0,duration:t=>"data"===t.type?200*(t.parsed._custom["vertical"===t.dataset.orientation?"y":"x"]-t.parsed["vertical"===t.dataset.orientation?"y":"x"]):void 0,easing:"linear"}},borderColor:"black",borderWidth:1,color:"black",dataElementType:"flow",modeX:"edge",nodePadding:10,nodeWidth:10,orientation:"horizontal",transitions:{hide:{animations:{colors:{properties:["colorFrom","colorTo"],to:"transparent",type:"color"}}},resize:{animations:{progress:{delay:0,duration:0}}},show:{animations:{colors:{from:"transparent",properties:["colorFrom","colorTo"],type:"color"}}}}},V.overrides={datasets:{clip:!1,parsing:{flow:"flow",from:"from",to:"to"}},interaction:{intersect:!0,mode:"nearest"},layout:{padding:{bottom:3,left:3,right:13,top:3}},plugins:{legend:{display:!1},tooltip:{callbacks:{label(t){const o=t.parsed._custom;return`${o.from.key} -> ${o.to.key}: ${o.flow}`},title:()=>""}}},scales:{x:{bounds:"data",display:!1,min:0,offset:!1,type:"linear"},y:{bounds:"data",display:!1,min:0,offset:!1,reverse:!0,type:"linear"}}};const H=(t,o,e,r,n)=>"vertical"===n?o<r?{cp1:{x:t,y:o+(r-o)/3*2},cp2:{x:e,y:o+(r-o)/3}}:{cp1:{x:0,y:o-(o-r)/3},cp2:{x:0,y:r+(o-r)/3}}:t<e?{cp1:{x:t+(e-t)/3*2,y:o},cp2:{x:t+(e-t)/3,y:r}}:{cp1:{x:t-(t-e)/3,y:0},cp2:{x:e+(t-e)/3,y:0}},L=(t,o,e)=>({x:t.x+e*(o.x-t.x),y:t.y+e*(o.y-t.y)}),q=(t,o)=>e.color(t).alpha(o).rgbString(),D=(t,o)=>"string"==typeof t?q(t,o):t,$=t=>"string"==typeof t?e.getHoverColor(t):t;class G extends o.Element{draw(t){const{x:r,x2:n,y:i,y2:a,height:l,progress:h,width:c}=this,f=this.options.orientation,d=H(r,i,n,a,f),u={height:l,width:c,x:r,x2:n,y:i,y2:a};if(0===h)return;t.save(),h<1&&function(t,{height:o,width:e,x:r,x2:n,y:i,y2:a},s,l){t.beginPath(),"vertical"===l?t.rect(Math.min(r,n),i,Math.abs(n-r)+e+1,(a-i)*s+1):t.rect(r,Math.min(i,a),(n-r)*s+1,Math.abs(a-i)+o+1),t.clip()}(t,u,h,f),function(t,{x:o,x2:e,y:r,y2:n,options:i}){let a="black";null!==i.flowColor?a=i.flowColor:"from"===i.colorMode?a=D(i.colorFrom,i.alpha):"to"===i.colorMode?a=D(i.colorTo,i.alpha):"string"==typeof i.colorFrom&&"string"==typeof i.colorTo&&(a="vertical"===i.orientation?t.createLinearGradient(0,r,0,n):t.createLinearGradient(o,0,e,0),a.addColorStop(0,q(i.colorFrom,i.alpha)),a.addColorStop(1,q(i.colorTo,i.alpha))),t.fillStyle=a,t.strokeStyle=a,t.lineWidth=.5}(t,this),function(t,{height:o,width:e,x:r,x2:n,y:i,y2:a},{cp1:s,cp2:l},h){t.beginPath(),t.moveTo(r,i),t.bezierCurveTo(s.x,s.y,l.x,l.y,n,a),"vertical"===h?(t.lineTo(n+e,a),t.bezierCurveTo(l.x+e,l.y,s.x+e,s.y,r+e,i)):(t.lineTo(n,a+o),t.bezierCurveTo(l.x,l.y+o,s.x,s.y+o,r,i+o)),t.lineTo(r,i),t.stroke(),t.closePath(),t.fill()}(t,u,d,f);const p=this.options.flowLabels;if(p.display){const r=e.toFont(p.font??o.Chart.defaults.font),n=function({height:t,width:o,x:e,x2:r,y:n,y2:i},a){return"vertical"===a?{height:i-n,width:Math.abs(r-e)+o,x:Math.min(e,r),y:n}:{height:Math.abs(i-n)+t,width:r-e,x:e,y:Math.min(n,i)}}(u,f);s(t,`${this.flow}`,{autoPosition:"center",backgroundColor:p.backgroundColor,borderRadius:p.borderRadius,borderWidth:0,color:p.color,font:r,height:n.height,lineOffset:r.lineHeight/2,padding:p.padding,position:p.position,width:n.width,x:n.x,y:n.y})}t.restore()}inRange(t,o,e){const{x:r,y:n,x2:i,y2:a,height:s,width:l}=this.getProps(["x","y","x2","y2","height","width"],e),h="vertical"===this.options.orientation;if(h?o<n||o>a:t<r||t>i)return!1;const{cp1:c,cp2:f}=H(r,n,i,a,this.options.orientation),d=h?(o-n)/(a-n):(t-r)/(i-r),u={x:i,y:a},p=L({x:r,y:n},c,d),y=L(c,f,d),g=L(f,u,d),x=L(p,y,d),m=L(y,g,d),b=L(x,m,d);return h?t>=b.x&&t<=b.x+l:o>=b.y&&o<=b.y+s}inXRange(t,o){const{x:e,x2:r,width:n}=this.getProps(["x","x2","width"],o),i=Math.min(e,r),a=Math.max(e,r)+("vertical"===this.options.orientation?n:0);return t>=i&&t<=a}inYRange(t,o){const{y:e,y2:r,height:n}=this.getProps(["y","y2","height"],o),i=Math.min(e,r),a=Math.max(e,r)+("vertical"===this.options.orientation?0:n);return t>=i&&t<=a}getCenterPoint(t){const{x:o,y:e,x2:r,y2:n,height:i,width:a}=this.getProps(["x","y","x2","y2","height","width"],t),s="vertical"===this.options.orientation;return{x:(o+r+(s?a:0))/2,y:(e+n+(s?0:i))/2}}tooltipPosition(t=!1){return this.getCenterPoint(t)}getRange(t){const o="vertical"===this.options.orientation;return"x"===t?o?this.width/2:0:o?0:this.height/2}constructor(t){super(),this.flow=0,this.x2=0,this.y2=0,this.width=0,this.height=0,this.progress=1,t&&Object.assign(this,t)}}G.id="flow",G.defaults={alpha:.5,colorFrom:"red",colorMode:"gradient",colorTo:"green",flowColor:null,flowLabels:{borderRadius:0,color:"black",display:!1,padding:4,position:"center"},hoverColorFrom:(t,o)=>$(o.colorFrom),hoverColorTo:(t,o)=>$(o.colorTo),orientation:"horizontal"},G.descriptors={_scriptable:!0,flowLabels:{_scriptable:!0}},o.Chart.register(V,G),t.Flow=G,t.SankeyController=V});
|
|
7
|
+
!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports,require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["exports","chart.js","chart.js/helpers"],o):o((t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-chart-sankey"]={},t.Chart,t.Chart.helpers)}(this,function(t,o,e){"use strict";const n=t=>void 0!==t;function r(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}function i(t,o){return"function"==typeof t?t(o):t&&"object"==typeof t&&!function(t){const o=Object.prototype.toString.call(t);return"[object CanvasPattern]"===o||"[object CanvasGradient]"===o}(t)?t[o.key]:t}function a(t,o,e,n,r,i,a){t.save(),t.fillStyle=o,a>0?(!function(t,o,e,n,r,i){const a=Math.max(0,Math.min(i,n/2,r/2));t.beginPath(),t.moveTo(o+a,e),t.lineTo(o+n-a,e),t.quadraticCurveTo(o+n,e,o+n,e+a),t.lineTo(o+n,e+r-a),t.quadraticCurveTo(o+n,e+r,o+n-a,e+r),t.lineTo(o+a,e+r),t.quadraticCurveTo(o,e+r,o,e+r-a),t.lineTo(o,e+a),t.quadraticCurveTo(o,e,o+a,e),t.closePath()}(t,e,n,r,i,a),t.fill()):t.fillRect(e,n,r,i),t.restore()}function s(t,o,e){const n=function(t){if(!t)return[];const o=[],e=Array.isArray(t)?t:[t];for(;e.length;){const t=e.pop();"string"==typeof t?o.unshift(...t.split("\n")):Array.isArray(t)?e.push(...t):t&&o.unshift(`${t}`)}return o}(o);if(!n.length)return;const{backgroundColor:r,borderRadius:i,color:s,font:l,lineOffset:h,padding:c}=e,f=function(t,o){return"auto"===t?o:t}(e.position,e.autoPosition),d=Number(l.lineHeight);t.font=l.string;const u=Math.max(...n.map(o=>t.measureText(o).width)),p=d*n.length,y=function(t,o,e){const{borderWidth:n,height:r,padding:i,width:a,x:s,y:l}=o,h={align:"center",x:s+a/2,y:l+r/2};return"left"===t?(h.align="right",h.x=s-n-i):"right"===t?(h.align="left",h.x=s+a+n+i):"top"===t?h.y=l-i-e/2:"bottom"===t&&(h.y=l+r+i+e/2),h}(f,e,p);t.textAlign=y.align,t.textBaseline="middle";const x=u+2*c,g=p+2*c,m=function(t,o,e,n,r){return"left"===t?o-r:"right"===t?o-e-r:o-n/2}(y.align,y.x,u,x,c),w=1===n.length?y.y:y.y-p/2+h,b=w+(n.length-1)*d/2;void 0!==r&&a(t,r,m,b-g/2,x,g,i),t.fillStyle=s;for(let o=0;o<n.length;o++)t.fillText(n[o],y.x,w+o*d)}const l=(t,o)=>o.flow===t.flow?t.index-o.index:o.flow-t.flow;function h(t,{size:o,priority:e,column:n}){const i=new Map;for(let o=0;o<t.length;o++){const{from:e,to:n,flow:r}=t[o],a=i.get(e)??{from:[],in:0,key:e,out:0,size:0,to:[]},s=(e===n?a:i.get(n))??{from:[],in:0,key:n,out:0,size:0,to:[]};a.out+=r,a.to.push({addY:0,flow:r,index:o,key:n,node:s}),1===a.to.length&&i.set(e,a),s.in+=r,s.from.push({addY:0,flow:r,index:o,key:e,node:a}),1===s.from.length&&i.set(n,s)}return((t,o)=>{const e=r(o);for(const o of t.values())o.from.sort(l),o.to.sort(l),o.size=Math[e](o.in||o.out,o.out||o.in)})(i,o),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.priority=o[e.key])})(i,e),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.column=!0,e.x=o[e.key])})(i,n),i}const c=1e-6;function f(t){return t.x??0}function d(t){return t.y??0}const u=(t,o=new Set)=>{const e=[];for(const n of t)o.has(n.key)||(o.add(n.key),e.push(n.key,...u(n.to.map(t=>t.node),o)));return e},p=(t,o)=>{const e=o.filter(t=>0===t.from.length),n=e.map(t=>t.key),r=u(e),i=new Set(r);for(const o of t)i.has(o.from)||i.has(o.to)||(n.push(o.from),i.add(o.from)),i.add(o.to);return n},y=(t,o)=>{const e=new Set(t.filter(t=>o.has(t.from)).map(t=>t.to)),n=[...o],r=n.filter(t=>!e.has(t));return r.length?r:n.slice(0,1)};function x(t,o,e=new Set){let n=0;for(const r of t)e.has(r.node)||(e.add(r.node),n+=r.node[o].length+x(r.node[o],o,e));return n}const g=t=>(o,e)=>x(o.node[t],t)-x(e.node[t],t)||o.node[t].length-e.node[t].length;function m(t,o){if(!t.from.length)return o;t.from.sort(g("from"));for(const e of t.from){const t=e.node;n(t.y)||(t.y=o,m(t,o?o+c:0)),o=Math.max(t.y+t.out,o)}return d(t)+t.size}const w=(t,o)=>Boolean(o&&f(o)<f(t));function b(t,o){if(!t.to.length)return o;t.to.sort(g("to"));for(let e=0;e<t.to.length;e++){const r=t.to[e],i=r.node;n(i.y)||(i.y=o,b(i,o?o+c:0)),w(i,t.to[e+1]?.node)?o+=r.flow:o=Math.max(i.y+Math.max(i.in,i.out),o)}return d(t)+t.size}function M(t,o){return n(t.y)?t.y:(t.y=o,o)}function v(t,o){if(!t.length)return 0;const e=((t,o)=>{const e=[...t].sort((t,o)=>t.size-o.size),n=e[e.length-1].size,r=t.filter(t=>t.size===n),i=r[0];if(1===r.length)return i;if(r.sort((t,o)=>f(t)-f(o)),0===f(i))return i;const a=r[r.length-1];return f(a)===o?a:r[Math.floor(r.length/2)]})(t,o);return e.y=0,m(e,0),b(e,0),function(t,o){const e=t.filter(t=>0===t.x),r=t.filter(t=>t.x===o),i=e.filter(t=>!n(t.y)),a=r.filter(t=>!n(t.y)),s=t.filter(t=>f(t)>0&&f(t)<o&&!n(t.y));let l=e.reduce((t,o)=>Math.max(t,d(o)+o.out||0),0)+c,h=r.reduce((t,o)=>Math.max(t,d(o)+o.in||0),0)+c,u=0;l>=h?(i.forEach(t=>{l=M(t,l),l=Math.max(l+t.out,b(t,l))}),a.forEach(t=>{h=M(t,h),h=Math.max(h+t.in,m(t,h))})):(i.forEach(t=>{l=M(t,l)}),a.forEach(t=>{h=M(t,h),h=Math.max(h+t.in,m(t,h))})),s.forEach(o=>{let e=t.filter(t=>f(t)===f(o)&&n(t.y)).reduce((t,o)=>Math.max(t,d(o)+Math.max(o.in,o.out)),0);e=M(o,e),e=Math.max(e+o.in,m(o,e)),e=Math.max(e+o.out,b(o,e)),u=Math.max(u,e)}),Math.max(l,h,u)}(t,o),((t,o)=>{let e=0;for(let n=0;n<=o;n++){const o=t.filter(t=>f(t)===n).sort((t,o)=>d(t)-d(o));let r=0;for(const t of o)d(t)<r&&(t.y=r),r=d(t)+t.size;e=Math.max(e,r)}return e})(t,o)}const k=(t,o)=>f(t)!==f(o)?f(t)-f(o):d(t)===d(o)?t.size-o.size:d(t)-d(o);function C(t,o,e){const n=(o.get(t.key)??0)*e;return Math.max(0,n-t.size)/2}function z(t,o,e){const n=(o.get(t.key)??0)*e;return Math.max(t.size,n)}function P(t,o,e,n){let r=n;for(let n=0;n<o;n++){const o=t[n].yHistory;for(let t=0;t<o.length&&!(o[t]>e);t++)r=Math.max(t+1,r)}return r}function S(t,o,e,n,r){const i=t.realCount,a=o.before*n,s=o.after*n,l=i>0?Math.max(t.lastAfter,a)+t.lastExtraHalf+r:0,h=t.realCumOffset+l,c=e-i;return t.realCount=i+1,t.realCumOffset=h,t.lastAfter=s,t.lastExtraHalf=r,h+c*a}function _(t,o,e=1,n="auto",r=new Map){if(t.sort(k),"even"===n)return function(t,o,e,n){let r,i,a,s=0,l=0;for(const h of t){const t=f(h),c=o.get(h.key)??{after:0,before:0},u=C(h,n,e);t!==r?r=t:i&&a&&(h.y=d(i)+i.size+Math.max(a.after,c.before)*e+l+u),i=h,a=c,l=u;const p=z(h,n,e);s=Math.max(s,d(h)+Math.max(h.in,h.out),d(h)+(h.size+p)/2)}return s}(t,o,e,r);let i=0;const a=new Map,s=[],l=t=>{if(!a.has(t)){const o=s.length;return a.set(t,o),s.push({lastAfter:0,lastExtraHalf:0,realCount:0,realCumOffset:0,yHistory:[]}),o}return a.get(t)??0};for(const n of t){const t=l(f(n)),a=s[t],h=o.get(n.key)??{after:0,before:0},c=d(n),u=C(n,r,e);if(c){a.yHistory.push(c);let o=a.yHistory.length;if(n.in)for(o=P(s,t,c,o);a.yHistory.length<o;)a.yHistory.push(c);n.y=c+S(a,h,o,e,u)}else a.realCount+=1,a.lastAfter=h.after*e,a.lastExtraHalf=u;const p=z(n,r,e);i=Math.max(i,d(n)+Math.max(n.in,n.out),d(n)+(n.size+p)/2)}return i}function T(t,o,{priority:e,height:r,nodePadding:i,nodePaddingMode:a,nodeMinSize:s,modeX:l}){const h=[...t.values()],c=function(t,o,e){const r=o.filter(t=>t.from!==t.to),i=[...t.keys()],a=[...t.values()],s=new Set(i);let l=0;for(;s.size;){const e=0===l?p(o,a):y(r,s);if(!e.length)throw new Error("Fatal error: Unable to place nodes to columns. Please report this issue.");for(const o of e){const e=t.get(o);e&&!n(e.x)&&(e.x=l),s.delete(o)}s.size&&l++}const h=a.reduce((t,o)=>Math.max(t,f(o)),0);if("edge"===e){const e=new Set(o.map(t=>t.from));i.filter(t=>!e.has(t)).forEach(o=>{const e=t.get(o);e&&!e.column&&(e.x=h)})}return h}(t,o,l??"edge"),u=e?function(t,o){let e=0,n=0;for(let r=0;r<=o;r++){let o=n;const i=t.filter(t=>f(t)===r).sort((t,o)=>(t.priority??0)-(o.priority??0));if(i.length){const o=t.reduce((t,o)=>f(o)>r?Math.min(t,f(o)):t,1/0);n=i[0].to.filter(t=>f(t.node)>o).reduce((t,o)=>t+o.flow,0)||0}for(const t of i)t.y=o,o+=Math.max(t.out,t.in);e=Math.max(o,e)}return e}(h,c):v(h,c),x=_(h,i,u/r,a??"auto",s??new Map);return function(t){t.forEach(t=>{const o=t.size,e=o<t.in,n=o<t.out;let r=0,i=t.from.length;t.from.sort((t,o)=>d(t.node)+t.node.out/2-(d(o.node)+o.node.out/2)).forEach((t,n)=>{e?t.addY=n*(o-t.flow)/(i-1):(t.addY=r,r+=t.flow)}),r=0,i=t.to.length,t.to.sort((t,o)=>d(t.node)+t.node.in/2-(d(o.node)+o.node.in/2)).forEach((t,e)=>{n?t.addY=e*(o-t.flow)/(i-1):(t.addY=r,r+=t.flow)})})}(h),{maxX:c,maxY:x}}function F(t){return t.x??0}function E(t){return t.y??0}function O(t,o){return Math[o](t.in||t.out,t.out||t.in)}function R(t,o,e,n){return"vertical"===n?o<(e.top+e.bottom)/2?"bottom":"top":t<(e.left+e.right)/2?"right":"left"}function H(t,o,e){for(const n of t)if(n.key===o&&n.index===e)return n.addY;return 0}function W(t,o,e,n,r,i,a,s,l){return"vertical"===l?{_custom:{flow:r,from:t,height:a.parse(r,i),to:o,x:a.parse(n,i),y:s.parse(F(o),i)},x:a.parse(e,i),y:s.parse(F(t),i)}:{_custom:{flow:r,from:t,height:s.parse(r,i),to:o,x:a.parse(F(o),i),y:s.parse(n,i)},x:a.parse(F(t),i),y:s.parse(e,i)}}function X(t,o,e,n,r,i,a,s){const l=t._custom,h=o.getPixelForValue(t.x),c=e.getPixelForValue(t.y);return"vertical"===s?{flow:l.flow,from:l.from,height:0,to:l.to,width:Math.abs(o.getPixelForValue(t.x+l.height)-h),x:h,x2:o.getPixelForValue(l.x),y:Y(e,t.y,n,i)+r+a,y2:Y(e,l.y,n,i)-a}:{flow:l.flow,from:l.from,height:Math.abs(e.getPixelForValue(t.y+l.height)-c),to:l.to,width:0,x:Y(o,t.x,n,i)+r+a,x2:Y(o,l.x,n,i)-a,y:c,y2:e.getPixelForValue(l.y)}}function Y(t,o,e,n){const r=t.getPixelForValue(o);return e?r-o/e*n:r}function j(t,o,e){const n="vertical"===o?e.height-e.chartArea.bottom:e.width-e.chartArea.right;return Math.max(0,t+3-n)}function A(t,o,e,n,r,i,a,s,l){if("vertical"===s){const s=e.getPixelForValue(E(t)),h=e.getPixelForValue(E(t)+o),c=Math.min(s,h),f=Math.abs(h-s),d=Math.max(f,l);return{height:i,width:d,x:c-(d-f)/2,y:Y(n,F(t),r,a)}}const h=n.getPixelForValue(E(t)),c=n.getPixelForValue(E(t)+o),f=Math.min(h,c),d=Math.abs(c-h),u=Math.max(d,l);return{height:u,width:i,x:Y(e,F(t),r,a),y:f-(u-d)/2}}function V(t,o){const e=i(t??10,o)??10;return"number"==typeof e?{after:e,before:e}:{after:e.after??10,before:e.before??10}}function L(t,o){return i(t??0,o)??0}function q(t,o){const{backgroundColor:e,borderRadius:n=0,color:r,display:a,font:s,padding:l=4,position:h}=t.nodeLabels??{};return{backgroundColor:i(e,o),borderRadius:n,color:i(r,o)??t.color??"black",display:i(a,o)??!0,font:s,padding:l,position:i(h,o)??"auto"}}const D=new Set(["nodeMinSize","nodePadding"]);class $ extends o.DatasetController{parseObjectData(t,o,e,n){const r=((t,o)=>{const{from:e="from",to:n="to",flow:r="flow"}=o;return t.map(({[e]:t,[n]:o,[r]:i})=>({flow:i,from:t,to:o}))})(o,this.options.parsing),{xScale:i,yScale:a}=t,s=[],l=h(r,this.options),c=this.options.orientation??"horizontal";this._nodes=l;const f=new Map,d=new Map;for(const t of l.values())f.set(t.key,V(this.options.nodePadding,t)),d.set(t.key,L(this.options.nodeMinSize,t));const{maxX:u,maxY:p}=T(l,r,{height:"vertical"===c?this.chart.width:this.chart.height,modeX:this.options.modeX,nodeMinSize:d,nodePadding:f,nodePaddingMode:this.options.nodePaddingMode,priority:!!this.options.priority});if(this._maxX=u,this._maxY=p,!i||!a)return[];for(let t=0,o=r.length;t<o;++t){const o=r[t],e=l.get(o.from),n=l.get(o.to);if(!e||!n)continue;const h=E(e)+H(e.to,o.to,t),f=E(n)+H(n.from,o.from,t);s.push(W(e,n,h,f,o.flow,t,i,a,c))}return s.slice(e,e+n)}getMinMax(t){return{max:t===("vertical"===this.options.orientation?this._cachedMeta.yScale:this._cachedMeta.xScale)?this._maxX:this._maxY,min:0}}update(t){const{data:o}=this._cachedMeta;this.updateElements(o,0,o.length,t)}updateElements(t,o,e,n){const{xScale:r,yScale:i}=this._cachedMeta;if(!r||!i)return;const a=this.resolveDataElementOptions(o,n),s=this.getSharedOptions(a),{borderWidth:l,nodeWidth:h=10,orientation:c="horizontal"}=this.options,f=j(h,c,this.chart),d=l?l/2+.5:0;for(let a=o;a<o+e;a++){const o=this.getParsed(a);this.updateElement(t[a],a,{options:this.resolveDataElementOptions(a,n),progress:"reset"===n?0:1,...X(o,r,i,this._maxX,h,f,d,c)},n)}s&&this.updateSharedOptions(s,n,a)}_drawLabels(){const t=this.chart.ctx,n=this.options,i=this._nodes||new Map,a=r(n.size),l=n.labels,{borderWidth:h=1,nodeWidth:c=10,orientation:f="horizontal"}=n,d=j(c,f,this.chart),u=n.font??this.chart.options.font??o.Chart.defaults.font,{xScale:p,yScale:y}=this._cachedMeta;if(!p||!y)return;t.save();const x=this.chart.chartArea;for(const o of i.values()){const r=O(o,a),i=L(n.nodeMinSize,o),{height:g,width:m,x:w,y:b}=A(o,r,p,y,this._maxX,c,d,f,i),M=l?.[o.key]??o.key,v=q(n,o);if(v.display){const o=e.toFont(v.font??u);s(t,M,{autoPosition:R(w,b,x,f),backgroundColor:v.backgroundColor,borderRadius:v.borderRadius,borderWidth:h,color:v.color,font:o,height:g,lineOffset:e.valueOrDefault(n.padding,o.lineHeight/2),padding:v.padding,position:v.position,width:m,x:w,y:b})}}t.restore()}_drawNodes(){const t=this.chart.ctx,o=this._nodes||new Map,{borderColor:e,borderWidth:n=0,nodeWidth:i=10,orientation:a="horizontal",size:s}=this.options,l=j(i,a,this.chart),h=r(s),{xScale:c,yScale:f}=this._cachedMeta;t.save(),e&&n&&(t.strokeStyle=e,t.lineWidth=n);for(const e of o.values()){if(t.fillStyle=e.color??"black",!c||!f)return;const o=Math[h](e.in||e.out,e.out||e.in),r=L(this.options.nodeMinSize,e),{height:s,width:d,x:u,y:p}=A(e,o,c,f,this._maxX,i,l,a,r);n&&t.strokeRect(u,p,d,s),t.fillRect(u,p,d,s)}t.restore()}draw(){const t=this.chart.ctx,o=this.getMeta().data??[],e=[];for(let t=0,n=o.length;t<n;++t){const n=o[t];n.from&&n.to&&(n.from.color=n.options.colorFrom,n.to.color=n.options.colorTo,n.active&&e.push(n))}for(const t of e)t.from&&t.to&&(t.from.color=t.options.colorFrom,t.to.color=t.options.colorTo);this._drawNodes();for(let e=0,n=o.length;e<n;++e)o[e].draw(t);this._drawLabels()}constructor(...t){super(...t),this._nodes=new Map,this._maxX=0,this._maxY=0}}$.id="sankey",$.descriptors={_indexable:!1,_scriptable:t=>!D.has(t),nodeLabels:{_indexable:!1,_scriptable:!1}},$.defaults={animations:{colors:{properties:["colorFrom","colorTo"],type:"color"},numbers:{properties:["x","y","x2","y2","height","width"],type:"number"},progress:{delay:t=>"data"===t.type?500*t.parsed["vertical"===t.dataset.orientation?"y":"x"]+20*t.dataIndex:void 0,duration:t=>"data"===t.type?200*(t.parsed._custom["vertical"===t.dataset.orientation?"y":"x"]-t.parsed["vertical"===t.dataset.orientation?"y":"x"]):void 0,easing:"linear"}},borderColor:"black",borderWidth:1,color:"black",dataElementType:"flow",modeX:"edge",nodeMinSize:0,nodePadding:10,nodePaddingMode:"auto",nodeWidth:10,orientation:"horizontal",transitions:{hide:{animations:{colors:{properties:["colorFrom","colorTo"],to:"transparent",type:"color"}}},resize:{animations:{progress:{delay:0,duration:0}}},show:{animations:{colors:{from:"transparent",properties:["colorFrom","colorTo"],type:"color"}}}}},$.overrides={datasets:{clip:!1,parsing:{flow:"flow",from:"from",to:"to"}},interaction:{intersect:!0,mode:"nearest"},layout:{padding:{bottom:3,left:3,right:13,top:3}},plugins:{legend:{display:!1},tooltip:{callbacks:{label(t){const o=t.parsed._custom;return`${o.from.key} -> ${o.to.key}: ${o.flow}`},title:()=>""}}},scales:{x:{bounds:"data",display:!1,min:0,offset:!1,type:"linear"},y:{bounds:"data",display:!1,min:0,offset:!1,reverse:!0,type:"linear"}}};const G=(t,o,e,n,r)=>"vertical"===r?o<n?{cp1:{x:t,y:o+(n-o)/3*2},cp2:{x:e,y:o+(n-o)/3}}:{cp1:{x:0,y:o-(o-n)/3},cp2:{x:0,y:n+(o-n)/3}}:t<e?{cp1:{x:t+(e-t)/3*2,y:o},cp2:{x:t+(e-t)/3,y:n}}:{cp1:{x:t-(t-e)/3,y:0},cp2:{x:e+(t-e)/3,y:0}},N=(t,o,e)=>({x:t.x+e*(o.x-t.x),y:t.y+e*(o.y-t.y)}),B=(t,o)=>e.color(t).alpha(o).rgbString(),I=(t,o)=>"string"==typeof t?B(t,o):t,U=t=>"string"==typeof t?e.getHoverColor(t):t;class J extends o.Element{draw(t){const{x:n,x2:r,y:i,y2:a,height:l,progress:h,width:c}=this,f=this.options.orientation,d=G(n,i,r,a,f),u={height:l,width:c,x:n,x2:r,y:i,y2:a};if(0===h)return;t.save(),h<1&&function(t,{height:o,width:e,x:n,x2:r,y:i,y2:a},s,l){t.beginPath(),"vertical"===l?t.rect(Math.min(n,r),i,Math.abs(r-n)+e+1,(a-i)*s+1):t.rect(n,Math.min(i,a),(r-n)*s+1,Math.abs(a-i)+o+1),t.clip()}(t,u,h,f),function(t,{x:o,x2:e,y:n,y2:r,options:i}){let a="black";null!==i.flowColor?a=i.flowColor:"from"===i.colorMode?a=I(i.colorFrom,i.alpha):"to"===i.colorMode?a=I(i.colorTo,i.alpha):"string"==typeof i.colorFrom&&"string"==typeof i.colorTo&&(a="vertical"===i.orientation?t.createLinearGradient(0,n,0,r):t.createLinearGradient(o,0,e,0),a.addColorStop(0,B(i.colorFrom,i.alpha)),a.addColorStop(1,B(i.colorTo,i.alpha))),t.fillStyle=a,t.strokeStyle=a,t.lineWidth=.5}(t,this),function(t,{height:o,width:e,x:n,x2:r,y:i,y2:a},{cp1:s,cp2:l},h){t.beginPath(),t.moveTo(n,i),t.bezierCurveTo(s.x,s.y,l.x,l.y,r,a),"vertical"===h?(t.lineTo(r+e,a),t.bezierCurveTo(l.x+e,l.y,s.x+e,s.y,n+e,i)):(t.lineTo(r,a+o),t.bezierCurveTo(l.x,l.y+o,s.x,s.y+o,n,i+o)),t.lineTo(n,i),t.stroke(),t.closePath(),t.fill()}(t,u,d,f);const p=this.options.flowLabels;if(p.display){const n=e.toFont(p.font??o.Chart.defaults.font),r=function({height:t,width:o,x:e,x2:n,y:r,y2:i},a){return"vertical"===a?{height:i-r,width:Math.abs(n-e)+o,x:Math.min(e,n),y:r}:{height:Math.abs(i-r)+t,width:n-e,x:e,y:Math.min(r,i)}}(u,f);s(t,`${this.flow}`,{autoPosition:"center",backgroundColor:p.backgroundColor,borderRadius:p.borderRadius,borderWidth:0,color:p.color,font:n,height:r.height,lineOffset:n.lineHeight/2,padding:p.padding,position:p.position,width:r.width,x:r.x,y:r.y})}t.restore()}inRange(t,o,e){const{x:n,y:r,x2:i,y2:a,height:s,width:l}=this.getProps(["x","y","x2","y2","height","width"],e),h="vertical"===this.options.orientation;if(h?o<r||o>a:t<n||t>i)return!1;const{cp1:c,cp2:f}=G(n,r,i,a,this.options.orientation),d=h?(o-r)/(a-r):(t-n)/(i-n),u={x:i,y:a},p=N({x:n,y:r},c,d),y=N(c,f,d),x=N(f,u,d),g=N(p,y,d),m=N(y,x,d),w=N(g,m,d);return h?t>=w.x&&t<=w.x+l:o>=w.y&&o<=w.y+s}inXRange(t,o){const{x:e,x2:n,width:r}=this.getProps(["x","x2","width"],o),i=Math.min(e,n),a=Math.max(e,n)+("vertical"===this.options.orientation?r:0);return t>=i&&t<=a}inYRange(t,o){const{y:e,y2:n,height:r}=this.getProps(["y","y2","height"],o),i=Math.min(e,n),a=Math.max(e,n)+("vertical"===this.options.orientation?0:r);return t>=i&&t<=a}getCenterPoint(t){const{x:o,y:e,x2:n,y2:r,height:i,width:a}=this.getProps(["x","y","x2","y2","height","width"],t),s="vertical"===this.options.orientation;return{x:(o+n+(s?a:0))/2,y:(e+r+(s?0:i))/2}}tooltipPosition(t=!1){return this.getCenterPoint(t)}getRange(t){const o="vertical"===this.options.orientation;return"x"===t?o?this.width/2:0:o?0:this.height/2}constructor(t){super(),this.flow=0,this.x2=0,this.y2=0,this.width=0,this.height=0,this.progress=1,t&&Object.assign(this,t)}}J.id="flow",J.defaults={alpha:.5,colorFrom:"red",colorMode:"gradient",colorTo:"green",flowColor:null,flowLabels:{borderRadius:0,color:"black",display:!1,padding:4,position:"center"},hoverColorFrom:(t,o)=>U(o.colorFrom),hoverColorTo:(t,o)=>U(o.colorTo),orientation:"horizontal"},J.descriptors={_scriptable:!0,flowLabels:{_scriptable:!0}},o.Chart.register($,J),t.Flow=J,t.SankeyController=$});
|
package/dist/controller.d.cts
CHANGED
|
@@ -33,7 +33,9 @@ export default class SankeyController extends DatasetController {
|
|
|
33
33
|
color: string;
|
|
34
34
|
dataElementType: string;
|
|
35
35
|
modeX: string;
|
|
36
|
+
nodeMinSize: number;
|
|
36
37
|
nodePadding: number;
|
|
38
|
+
nodePaddingMode: string;
|
|
37
39
|
nodeWidth: number;
|
|
38
40
|
orientation: string;
|
|
39
41
|
transitions: {
|
package/dist/controller.d.ts
CHANGED
|
@@ -33,7 +33,9 @@ export default class SankeyController extends DatasetController {
|
|
|
33
33
|
color: string;
|
|
34
34
|
dataElementType: string;
|
|
35
35
|
modeX: string;
|
|
36
|
+
nodeMinSize: number;
|
|
36
37
|
nodePadding: number;
|
|
38
|
+
nodePaddingMode: string;
|
|
37
39
|
nodeWidth: number;
|
|
38
40
|
orientation: string;
|
|
39
41
|
transitions: {
|
package/dist/lib/layout.d.ts
CHANGED
|
@@ -17,10 +17,11 @@ export declare function calculateY(nodeArray: SankeyNode[], maxX: number): numbe
|
|
|
17
17
|
export declare function calculateYUsingPriority(nodeArray: SankeyNode[], maxX: number): number;
|
|
18
18
|
type PaddableNode = Pick<SankeyNode, 'in' | 'key' | 'out' | 'size' | 'x' | 'y'>;
|
|
19
19
|
type NodeGap = Required<SankeyNodeGap>;
|
|
20
|
+
export type NodePaddingMode = 'auto' | 'even';
|
|
20
21
|
/**
|
|
21
22
|
* @return {number} maxY
|
|
22
23
|
*/
|
|
23
|
-
export declare function addPadding(nodeArray: PaddableNode[], gaps: Map<string, NodeGap>): number;
|
|
24
|
+
export declare function addPadding(nodeArray: PaddableNode[], gaps: Map<string, NodeGap>, scale?: number, mode?: NodePaddingMode, minSizes?: Map<string, number>): number;
|
|
24
25
|
export declare function sortFlows(nodeArray: SankeyNode[]): void;
|
|
25
26
|
interface LayoutOptions {
|
|
26
27
|
/** use node priority when sorting nodes vertically */
|
|
@@ -29,10 +30,14 @@ interface LayoutOptions {
|
|
|
29
30
|
height: number;
|
|
30
31
|
/** vertical before/after gap per node, in CSS pixels */
|
|
31
32
|
nodePadding: Map<string, NodeGap>;
|
|
33
|
+
/** how nodePadding gaps are distributed within a column, defaults to 'auto' */
|
|
34
|
+
nodePaddingMode: SankeyControllerDatasetOptions['nodePaddingMode'];
|
|
35
|
+
/** minimum drawn node size per node, in CSS pixels */
|
|
36
|
+
nodeMinSize?: Map<string, number>;
|
|
32
37
|
/** layout mode in x-direction */
|
|
33
38
|
modeX: SankeyControllerDatasetOptions['modeX'];
|
|
34
39
|
}
|
|
35
|
-
export declare function layout(nodes: Map<string, SankeyNode>, data: SankeyDataPoint[], { priority, height, nodePadding, modeX }: LayoutOptions): {
|
|
40
|
+
export declare function layout(nodes: Map<string, SankeyNode>, data: SankeyDataPoint[], { priority, height, nodePadding, nodePaddingMode, nodeMinSize, modeX }: LayoutOptions): {
|
|
36
41
|
maxY: number;
|
|
37
42
|
maxX: number;
|
|
38
43
|
};
|
package/dist/types.d.cts
CHANGED
|
@@ -61,7 +61,9 @@ export interface SankeyControllerDatasetOptions extends Omit<ControllerDatasetOp
|
|
|
61
61
|
flowColor?: ScriptableAndArray<Color, SankeyScriptableContext>;
|
|
62
62
|
modeX?: 'edge' | 'even';
|
|
63
63
|
nodeLabels?: SankeyControllerDatasetNodeLabelsOptions;
|
|
64
|
+
nodeMinSize?: SankeyNodeOption<number>;
|
|
64
65
|
nodePadding?: SankeyNodeOption<number | SankeyNodeGap>;
|
|
66
|
+
nodePaddingMode?: 'auto' | 'even';
|
|
65
67
|
nodeWidth?: number;
|
|
66
68
|
orientation?: SankeyOrientation;
|
|
67
69
|
padding?: number;
|
package/dist/types.d.ts
CHANGED
|
@@ -60,7 +60,9 @@ export interface SankeyControllerDatasetOptions extends Omit<ControllerDatasetOp
|
|
|
60
60
|
flowColor?: ScriptableAndArray<Color, SankeyScriptableContext>;
|
|
61
61
|
modeX?: 'edge' | 'even';
|
|
62
62
|
nodeLabels?: SankeyControllerDatasetNodeLabelsOptions;
|
|
63
|
+
nodeMinSize?: SankeyNodeOption<number>;
|
|
63
64
|
nodePadding?: SankeyNodeOption<number | SankeyNodeGap>;
|
|
65
|
+
nodePaddingMode?: 'auto' | 'even';
|
|
64
66
|
nodeWidth?: number;
|
|
65
67
|
orientation?: SankeyOrientation;
|
|
66
68
|
padding?: number;
|
package/package.json
CHANGED