chartjs-chart-sankey 0.14.4 → 0.15.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/README.md +5 -0
- package/dist/chartjs-chart-sankey.cjs +398 -109
- package/dist/chartjs-chart-sankey.esm.js +398 -109
- package/dist/chartjs-chart-sankey.min.js +2 -2
- package/dist/controller.d.ts +11 -3
- package/dist/flow.d.ts +15 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.d.ts +1 -1
- package/dist/labels.d.ts +22 -0
- package/dist/lib/core.d.ts +1 -2
- package/dist/lib/layout.d.ts +2 -1
- package/dist/types.d.ts +110 -66
- package/package.json +3 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.15.0
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -39,6 +39,99 @@ function validateSizeValue(size) {
|
|
|
39
39
|
return size;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function isPatternOrGradient(value) {
|
|
43
|
+
const type = Object.prototype.toString.call(value);
|
|
44
|
+
return type === '[object CanvasPattern]' || type === '[object CanvasGradient]';
|
|
45
|
+
}
|
|
46
|
+
function resolveNodeLabelOption(option, node) {
|
|
47
|
+
if (typeof option === 'function') {
|
|
48
|
+
return option(node);
|
|
49
|
+
}
|
|
50
|
+
if (option && typeof option === 'object' && !isPatternOrGradient(option)) {
|
|
51
|
+
return option[node.key];
|
|
52
|
+
}
|
|
53
|
+
return option;
|
|
54
|
+
}
|
|
55
|
+
function addRoundedRectPath(ctx, x, y, width, height, radius) {
|
|
56
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.moveTo(x + r, y);
|
|
59
|
+
ctx.lineTo(x + width - r, y);
|
|
60
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
61
|
+
ctx.lineTo(x + width, y + height - r);
|
|
62
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
63
|
+
ctx.lineTo(x + r, y + height);
|
|
64
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
65
|
+
ctx.lineTo(x, y + r);
|
|
66
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
}
|
|
69
|
+
function resolvePosition(position, autoPosition) {
|
|
70
|
+
return position === 'auto' ? autoPosition : position;
|
|
71
|
+
}
|
|
72
|
+
function getTextPosition(position, options, totalTextHeight) {
|
|
73
|
+
const { borderWidth, height, padding, width, x, y } = options;
|
|
74
|
+
const text = {
|
|
75
|
+
align: 'center',
|
|
76
|
+
x: x + width / 2,
|
|
77
|
+
y: y + height / 2
|
|
78
|
+
};
|
|
79
|
+
if (position === 'left') {
|
|
80
|
+
text.align = 'right';
|
|
81
|
+
text.x = x - borderWidth - padding;
|
|
82
|
+
} else if (position === 'right') {
|
|
83
|
+
text.align = 'left';
|
|
84
|
+
text.x = x + width + borderWidth + padding;
|
|
85
|
+
} else if (position === 'top') {
|
|
86
|
+
text.y = y - padding - totalTextHeight / 2;
|
|
87
|
+
} else if (position === 'bottom') {
|
|
88
|
+
text.y = y + height + padding + totalTextHeight / 2;
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
function getBackgroundX(textAlign, textX, textWidth, backgroundWidth, padding) {
|
|
93
|
+
if (textAlign === 'left') return textX - padding;
|
|
94
|
+
if (textAlign === 'right') return textX - textWidth - padding;
|
|
95
|
+
return textX - backgroundWidth / 2;
|
|
96
|
+
}
|
|
97
|
+
function drawBackground(ctx, color, x, y, width, height, borderRadius) {
|
|
98
|
+
ctx.save();
|
|
99
|
+
ctx.fillStyle = color;
|
|
100
|
+
if (borderRadius > 0) {
|
|
101
|
+
addRoundedRectPath(ctx, x, y, width, height, borderRadius);
|
|
102
|
+
ctx.fill();
|
|
103
|
+
} else {
|
|
104
|
+
ctx.fillRect(x, y, width, height);
|
|
105
|
+
}
|
|
106
|
+
ctx.restore();
|
|
107
|
+
}
|
|
108
|
+
function drawLabel(ctx, label, options) {
|
|
109
|
+
const lines = toTextLines(label);
|
|
110
|
+
if (!lines.length) return;
|
|
111
|
+
const { backgroundColor, borderRadius, color, font, lineOffset, padding } = options;
|
|
112
|
+
const position = resolvePosition(options.position, options.autoPosition);
|
|
113
|
+
const textHeight = Number(font.lineHeight);
|
|
114
|
+
ctx.font = font.string;
|
|
115
|
+
const textWidth = Math.max(...lines.map((line)=>ctx.measureText(line).width));
|
|
116
|
+
const totalTextHeight = textHeight * lines.length;
|
|
117
|
+
const text = getTextPosition(position, options, totalTextHeight);
|
|
118
|
+
ctx.textAlign = text.align;
|
|
119
|
+
ctx.textBaseline = 'middle';
|
|
120
|
+
const backgroundWidth = textWidth + padding * 2;
|
|
121
|
+
const backgroundHeight = totalTextHeight + padding * 2;
|
|
122
|
+
const backgroundX = getBackgroundX(text.align, text.x, textWidth, backgroundWidth, padding);
|
|
123
|
+
const firstLineY = lines.length === 1 ? text.y : text.y - totalTextHeight / 2 + lineOffset;
|
|
124
|
+
const textCenterY = firstLineY + (lines.length - 1) * textHeight / 2;
|
|
125
|
+
const backgroundY = textCenterY - backgroundHeight / 2;
|
|
126
|
+
if (backgroundColor !== undefined) {
|
|
127
|
+
drawBackground(ctx, backgroundColor, backgroundX, backgroundY, backgroundWidth, backgroundHeight, borderRadius);
|
|
128
|
+
}
|
|
129
|
+
ctx.fillStyle = color;
|
|
130
|
+
for(let i = 0; i < lines.length; i++){
|
|
131
|
+
ctx.fillText(lines[i], text.x, firstLineY + i * textHeight);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
42
135
|
const flowSort = (a, b)=>{
|
|
43
136
|
if (b.flow === a.flow) return a.index - b.index;
|
|
44
137
|
return b.flow - a.flow;
|
|
@@ -231,16 +324,22 @@ function processFrom(node, y) {
|
|
|
231
324
|
}
|
|
232
325
|
return nodeY$1(node) + node.size;
|
|
233
326
|
}
|
|
327
|
+
const returnsToNearerColumn = (current, next)=>Boolean(next && nodeX$1(next) < nodeX$1(current));
|
|
234
328
|
function processTo(node, y) {
|
|
235
329
|
if (!node.to.length) return y;
|
|
236
330
|
node.to.sort(flowByNodeCount('to'));
|
|
237
|
-
for
|
|
331
|
+
for(let i = 0; i < node.to.length; i++){
|
|
332
|
+
const flow = node.to[i];
|
|
238
333
|
const n = flow.node;
|
|
239
334
|
if (!defined(n.y)) {
|
|
240
335
|
n.y = y;
|
|
241
336
|
processTo(n, y ? y + SMALL_VALUE : 0);
|
|
242
337
|
}
|
|
243
|
-
|
|
338
|
+
if (returnsToNearerColumn(n, node.to[i + 1]?.node)) {
|
|
339
|
+
y += flow.flow;
|
|
340
|
+
} else {
|
|
341
|
+
y = Math.max(n.y + Math.max(n.in, n.out), y);
|
|
342
|
+
}
|
|
244
343
|
}
|
|
245
344
|
return nodeY$1(node) + node.size;
|
|
246
345
|
}
|
|
@@ -331,7 +430,10 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
331
430
|
for(let x = 0; x <= maxX; x++){
|
|
332
431
|
let y = nextYStart;
|
|
333
432
|
const nodes = nodeArray.filter((node)=>nodeX$1(node) === x).sort((a, b)=>(a.priority ?? 0) - (b.priority ?? 0));
|
|
334
|
-
|
|
433
|
+
if (nodes.length) {
|
|
434
|
+
const nextX = nodeArray.reduce((next, node)=>nodeX$1(node) > x ? Math.min(next, nodeX$1(node)) : next, Infinity);
|
|
435
|
+
nextYStart = nodes[0].to.filter((to)=>nodeX$1(to.node) > nextX).reduce((acc, cur)=>acc + cur.flow, 0) || 0;
|
|
436
|
+
}
|
|
335
437
|
for (const node of nodes){
|
|
336
438
|
node.y = y;
|
|
337
439
|
y += Math.max(node.out, node.in);
|
|
@@ -429,6 +531,15 @@ function nodeX(node) {
|
|
|
429
531
|
function nodeY(node) {
|
|
430
532
|
return node.y ?? 0;
|
|
431
533
|
}
|
|
534
|
+
function getNodeSize(node, size) {
|
|
535
|
+
return Math[size](node.in || node.out, node.out || node.in);
|
|
536
|
+
}
|
|
537
|
+
function getAutoLabelPosition(x, y, chartArea, orientation) {
|
|
538
|
+
if (orientation === 'vertical') {
|
|
539
|
+
return y < (chartArea.top + chartArea.bottom) / 2 ? 'bottom' : 'top';
|
|
540
|
+
}
|
|
541
|
+
return x < (chartArea.left + chartArea.right) / 2 ? 'right' : 'left';
|
|
542
|
+
}
|
|
432
543
|
function getAddY(arr, key, index) {
|
|
433
544
|
for (const item of arr){
|
|
434
545
|
if (item.key === key && item.index === index) {
|
|
@@ -437,15 +548,111 @@ function getAddY(arr, key, index) {
|
|
|
437
548
|
}
|
|
438
549
|
return 0;
|
|
439
550
|
}
|
|
551
|
+
function parseFlow(from, to, fromY, toY, flow, index, xScale, yScale, orientation) {
|
|
552
|
+
if (orientation === 'vertical') {
|
|
553
|
+
return {
|
|
554
|
+
_custom: {
|
|
555
|
+
flow,
|
|
556
|
+
from,
|
|
557
|
+
height: xScale.parse(flow, index),
|
|
558
|
+
to,
|
|
559
|
+
x: xScale.parse(toY, index),
|
|
560
|
+
y: yScale.parse(nodeX(to), index)
|
|
561
|
+
},
|
|
562
|
+
x: xScale.parse(fromY, index),
|
|
563
|
+
y: yScale.parse(nodeX(from), index)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
_custom: {
|
|
568
|
+
flow,
|
|
569
|
+
from,
|
|
570
|
+
height: yScale.parse(flow, index),
|
|
571
|
+
to,
|
|
572
|
+
x: xScale.parse(nodeX(to), index),
|
|
573
|
+
y: yScale.parse(toY, index)
|
|
574
|
+
},
|
|
575
|
+
x: xScale.parse(nodeX(from), index),
|
|
576
|
+
y: yScale.parse(fromY, index)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function getFlowElementProperties(parsed, xScale, yScale, maxColumn, nodeWidth, columnPadding, borderSpace, orientation) {
|
|
580
|
+
const custom = parsed._custom;
|
|
581
|
+
const x = xScale.getPixelForValue(parsed.x);
|
|
582
|
+
const y = yScale.getPixelForValue(parsed.y);
|
|
583
|
+
if (orientation === 'vertical') {
|
|
584
|
+
return {
|
|
585
|
+
flow: custom.flow,
|
|
586
|
+
from: custom.from,
|
|
587
|
+
height: 0,
|
|
588
|
+
to: custom.to,
|
|
589
|
+
width: Math.abs(xScale.getPixelForValue(parsed.x + custom.height) - x),
|
|
590
|
+
x,
|
|
591
|
+
x2: xScale.getPixelForValue(custom.x),
|
|
592
|
+
y: getColumnPixel(yScale, parsed.y, maxColumn, columnPadding) + nodeWidth + borderSpace,
|
|
593
|
+
y2: getColumnPixel(yScale, custom.y, maxColumn, columnPadding) - borderSpace
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
flow: custom.flow,
|
|
598
|
+
from: custom.from,
|
|
599
|
+
height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
|
|
600
|
+
to: custom.to,
|
|
601
|
+
width: 0,
|
|
602
|
+
x: getColumnPixel(xScale, parsed.x, maxColumn, columnPadding) + nodeWidth + borderSpace,
|
|
603
|
+
x2: getColumnPixel(xScale, custom.x, maxColumn, columnPadding) - borderSpace,
|
|
604
|
+
y,
|
|
605
|
+
y2: yScale.getPixelForValue(custom.y)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function getColumnPixel(scale, value, maxColumn, padding) {
|
|
609
|
+
const pixel = scale.getPixelForValue(value);
|
|
610
|
+
return maxColumn ? pixel - value / maxColumn * padding : pixel;
|
|
611
|
+
}
|
|
612
|
+
function getColumnPadding(nodeWidth, orientation, chart) {
|
|
613
|
+
const trailingSpace = orientation === 'vertical' ? chart.height - chart.chartArea.bottom : chart.width - chart.chartArea.right;
|
|
614
|
+
return Math.max(0, nodeWidth + 3 - trailingSpace);
|
|
615
|
+
}
|
|
616
|
+
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation) {
|
|
617
|
+
if (orientation === 'vertical') {
|
|
618
|
+
const x = xScale.getPixelForValue(nodeY(node));
|
|
619
|
+
return {
|
|
620
|
+
height: nodeWidth,
|
|
621
|
+
width: Math.abs(xScale.getPixelForValue(nodeY(node) + size) - x),
|
|
622
|
+
x,
|
|
623
|
+
y: getColumnPixel(yScale, nodeX(node), maxColumn, columnPadding)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
627
|
+
return {
|
|
628
|
+
height: Math.abs(yScale.getPixelForValue(nodeY(node) + size) - y),
|
|
629
|
+
width: nodeWidth,
|
|
630
|
+
x: getColumnPixel(xScale, nodeX(node), maxColumn, columnPadding),
|
|
631
|
+
y
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function resolveNodeLabelStyle(options, node) {
|
|
635
|
+
const { backgroundColor, borderRadius = 0, color, display, font, padding = 4, position } = options.nodeLabels ?? {};
|
|
636
|
+
return {
|
|
637
|
+
backgroundColor: resolveNodeLabelOption(backgroundColor, node),
|
|
638
|
+
borderRadius,
|
|
639
|
+
color: resolveNodeLabelOption(color, node) ?? options.color ?? 'black',
|
|
640
|
+
display: resolveNodeLabelOption(display, node) ?? true,
|
|
641
|
+
font,
|
|
642
|
+
padding,
|
|
643
|
+
position: resolveNodeLabelOption(position, node) ?? 'auto'
|
|
644
|
+
};
|
|
645
|
+
}
|
|
440
646
|
class SankeyController extends chart_js.DatasetController {
|
|
441
647
|
parseObjectData(meta, data, start, count) {
|
|
442
648
|
const sankeyData = getParsedData(data, this.options.parsing);
|
|
443
649
|
const { xScale, yScale } = meta;
|
|
444
650
|
const parsed = [];
|
|
445
651
|
const nodes = buildNodesFromData(sankeyData, this.options);
|
|
652
|
+
const orientation = this.options.orientation ?? 'horizontal';
|
|
446
653
|
this._nodes = nodes;
|
|
447
654
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
448
|
-
height: this.chart.canvas.height,
|
|
655
|
+
height: orientation === 'vertical' ? this.chart.canvas.width : this.chart.canvas.height,
|
|
449
656
|
modeX: this.options.modeX,
|
|
450
657
|
nodePadding: this.options.nodePadding ?? 10,
|
|
451
658
|
priority: !!this.options.priority
|
|
@@ -460,24 +667,15 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
460
667
|
if (!from || !to) continue;
|
|
461
668
|
const fromY = nodeY(from) + getAddY(from.to, dataPoint.to, i);
|
|
462
669
|
const toY = nodeY(to) + getAddY(to.from, dataPoint.from, i);
|
|
463
|
-
parsed.push(
|
|
464
|
-
_custom: {
|
|
465
|
-
flow: dataPoint.flow,
|
|
466
|
-
from,
|
|
467
|
-
height: yScale.parse(dataPoint.flow, i),
|
|
468
|
-
to,
|
|
469
|
-
x: xScale.parse(nodeX(to), i),
|
|
470
|
-
y: yScale.parse(toY, i)
|
|
471
|
-
},
|
|
472
|
-
x: xScale.parse(nodeX(from), i),
|
|
473
|
-
y: yScale.parse(fromY, i)
|
|
474
|
-
});
|
|
670
|
+
parsed.push(parseFlow(from, to, fromY, toY, dataPoint.flow, i, xScale, yScale, orientation));
|
|
475
671
|
}
|
|
476
672
|
return parsed.slice(start, start + count);
|
|
477
673
|
}
|
|
478
674
|
getMinMax(scale) {
|
|
675
|
+
const vertical = this.options.orientation === 'vertical';
|
|
676
|
+
const columnScale = vertical ? this._cachedMeta.yScale : this._cachedMeta.xScale;
|
|
479
677
|
return {
|
|
480
|
-
max: scale ===
|
|
678
|
+
max: scale === columnScale ? this._maxX : this._maxY,
|
|
481
679
|
min: 0
|
|
482
680
|
};
|
|
483
681
|
}
|
|
@@ -490,22 +688,15 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
490
688
|
if (!xScale || !yScale) return;
|
|
491
689
|
const firstOpts = this.resolveDataElementOptions(start, mode);
|
|
492
690
|
const sharedOptions = this.getSharedOptions(firstOpts);
|
|
493
|
-
const { borderWidth, nodeWidth = 10 } = this.options;
|
|
691
|
+
const { borderWidth, nodeWidth = 10, orientation = 'horizontal' } = this.options;
|
|
692
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
494
693
|
const borderSpace = borderWidth ? borderWidth / 2 + 0.5 : 0;
|
|
495
694
|
for(let i = start; i < start + count; i++){
|
|
496
695
|
const parsed = this.getParsed(i);
|
|
497
|
-
const custom = parsed._custom;
|
|
498
|
-
const y = yScale.getPixelForValue(parsed.y);
|
|
499
696
|
this.updateElement(elems[i], i, {
|
|
500
|
-
from: custom.from,
|
|
501
|
-
height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
|
|
502
697
|
options: this.resolveDataElementOptions(i, mode),
|
|
503
698
|
progress: mode === 'reset' ? 0 : 1,
|
|
504
|
-
|
|
505
|
-
x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderSpace,
|
|
506
|
-
x2: xScale.getPixelForValue(custom.x) - borderSpace,
|
|
507
|
-
y,
|
|
508
|
-
y2: yScale.getPixelForValue(custom.y)
|
|
699
|
+
...getFlowElementProperties(parsed, xScale, yScale, this._maxX, nodeWidth, columnPadding, borderSpace, orientation)
|
|
509
700
|
}, mode);
|
|
510
701
|
}
|
|
511
702
|
if (sharedOptions) {
|
|
@@ -517,54 +708,45 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
517
708
|
const options = this.options;
|
|
518
709
|
const nodes = this._nodes || new Map();
|
|
519
710
|
const size = validateSizeValue(options.size);
|
|
520
|
-
const borderWidth = options.borderWidth ?? 1;
|
|
521
|
-
const nodeWidth = options.nodeWidth ?? 10;
|
|
522
711
|
const labels = options.labels;
|
|
712
|
+
const { borderWidth = 1, nodeWidth = 10, orientation = 'horizontal' } = options;
|
|
713
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
714
|
+
const defaultFont = options.font ?? this.chart.options.font ?? chart_js.Chart.defaults.font;
|
|
523
715
|
const { xScale, yScale } = this._cachedMeta;
|
|
524
716
|
if (!xScale || !yScale) return;
|
|
525
717
|
ctx.save();
|
|
526
718
|
const chartArea = this.chart.chartArea;
|
|
527
719
|
for (const node of nodes.values()){
|
|
528
|
-
const
|
|
529
|
-
const y = yScale.
|
|
530
|
-
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
531
|
-
const height = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
|
|
720
|
+
const max = getNodeSize(node, size);
|
|
721
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation);
|
|
532
722
|
const label = labels?.[node.key] ?? node.key;
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
723
|
+
const labelStyle = resolveNodeLabelStyle(options, node);
|
|
724
|
+
if (labelStyle.display) {
|
|
725
|
+
const font = helpers.toFont(labelStyle.font ?? defaultFont);
|
|
726
|
+
drawLabel(ctx, label, {
|
|
727
|
+
autoPosition: getAutoLabelPosition(x, y, chartArea, orientation),
|
|
728
|
+
backgroundColor: labelStyle.backgroundColor,
|
|
729
|
+
borderRadius: labelStyle.borderRadius,
|
|
730
|
+
borderWidth,
|
|
731
|
+
color: labelStyle.color,
|
|
732
|
+
font,
|
|
733
|
+
height,
|
|
734
|
+
lineOffset: helpers.valueOrDefault(options.padding, font.lineHeight / 2),
|
|
735
|
+
padding: labelStyle.padding,
|
|
736
|
+
position: labelStyle.position,
|
|
737
|
+
width,
|
|
738
|
+
x,
|
|
739
|
+
y
|
|
740
|
+
});
|
|
542
741
|
}
|
|
543
|
-
this._drawLabel(label, y, height, ctx, textX);
|
|
544
742
|
}
|
|
545
743
|
ctx.restore();
|
|
546
744
|
}
|
|
547
|
-
_drawLabel(label, y, height, ctx, textX) {
|
|
548
|
-
const font = helpers.toFont(this.options.font ?? this.chart.options.font ?? chart_js.Chart.defaults.font);
|
|
549
|
-
const lines = toTextLines(label);
|
|
550
|
-
const lineCount = lines.length;
|
|
551
|
-
const middle = y + height / 2;
|
|
552
|
-
const textHeight = font.lineHeight;
|
|
553
|
-
const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
|
|
554
|
-
ctx.font = font.string;
|
|
555
|
-
if (lineCount > 1) {
|
|
556
|
-
const top = middle - textHeight * lineCount / 2 + padding;
|
|
557
|
-
for(let i = 0; i < lineCount; i++){
|
|
558
|
-
ctx.fillText(lines[i], textX, top + i * textHeight);
|
|
559
|
-
}
|
|
560
|
-
} else {
|
|
561
|
-
ctx.fillText(label, textX, middle);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
745
|
_drawNodes() {
|
|
565
746
|
const ctx = this.chart.ctx;
|
|
566
747
|
const nodes = this._nodes || new Map();
|
|
567
|
-
const { borderColor, borderWidth = 0, nodeWidth = 10, size } = this.options;
|
|
748
|
+
const { borderColor, borderWidth = 0, nodeWidth = 10, orientation = 'horizontal', size } = this.options;
|
|
749
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
568
750
|
const sizeMethod = validateSizeValue(size);
|
|
569
751
|
const { xScale, yScale } = this._cachedMeta;
|
|
570
752
|
ctx.save();
|
|
@@ -575,14 +757,12 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
575
757
|
for (const node of nodes.values()){
|
|
576
758
|
ctx.fillStyle = node.color ?? 'black';
|
|
577
759
|
if (!xScale || !yScale) return;
|
|
578
|
-
const x = xScale.getPixelForValue(nodeX(node));
|
|
579
|
-
const y = yScale.getPixelForValue(nodeY(node));
|
|
580
760
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
581
|
-
const height =
|
|
761
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation);
|
|
582
762
|
if (borderWidth) {
|
|
583
|
-
ctx.strokeRect(x, y,
|
|
763
|
+
ctx.strokeRect(x, y, width, height);
|
|
584
764
|
}
|
|
585
|
-
ctx.fillRect(x, y,
|
|
765
|
+
ctx.fillRect(x, y, width, height);
|
|
586
766
|
}
|
|
587
767
|
ctx.restore();
|
|
588
768
|
}
|
|
@@ -619,6 +799,14 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
619
799
|
}
|
|
620
800
|
}
|
|
621
801
|
SankeyController.id = 'sankey';
|
|
802
|
+
SankeyController.descriptors = {
|
|
803
|
+
_indexable: false,
|
|
804
|
+
_scriptable: true,
|
|
805
|
+
nodeLabels: {
|
|
806
|
+
_indexable: false,
|
|
807
|
+
_scriptable: false
|
|
808
|
+
}
|
|
809
|
+
};
|
|
622
810
|
SankeyController.defaults = {
|
|
623
811
|
animations: {
|
|
624
812
|
colors: {
|
|
@@ -634,13 +822,14 @@ SankeyController.defaults = {
|
|
|
634
822
|
'y',
|
|
635
823
|
'x2',
|
|
636
824
|
'y2',
|
|
637
|
-
'height'
|
|
825
|
+
'height',
|
|
826
|
+
'width'
|
|
638
827
|
],
|
|
639
828
|
type: 'number'
|
|
640
829
|
},
|
|
641
830
|
progress: {
|
|
642
|
-
delay: (ctx)=>ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined,
|
|
643
|
-
duration: (ctx)=>ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
|
|
831
|
+
delay: (ctx)=>ctx.type === 'data' ? ctx.parsed[ctx.dataset.orientation === 'vertical' ? 'y' : 'x'] * 500 + ctx.dataIndex * 20 : undefined,
|
|
832
|
+
duration: (ctx)=>ctx.type === 'data' ? (ctx.parsed._custom[ctx.dataset.orientation === 'vertical' ? 'y' : 'x'] - ctx.parsed[ctx.dataset.orientation === 'vertical' ? 'y' : 'x']) * 200 : undefined,
|
|
644
833
|
easing: 'linear'
|
|
645
834
|
}
|
|
646
835
|
},
|
|
@@ -651,6 +840,7 @@ SankeyController.defaults = {
|
|
|
651
840
|
modeX: 'edge',
|
|
652
841
|
nodePadding: 10,
|
|
653
842
|
nodeWidth: 10,
|
|
843
|
+
orientation: 'horizontal',
|
|
654
844
|
transitions: {
|
|
655
845
|
hide: {
|
|
656
846
|
animations: {
|
|
@@ -734,7 +924,25 @@ SankeyController.overrides = {
|
|
|
734
924
|
}
|
|
735
925
|
};
|
|
736
926
|
|
|
737
|
-
const controlPoints = (x, y, x2, y2)=>
|
|
927
|
+
const controlPoints = (x, y, x2, y2, orientation)=>orientation === 'vertical' ? y < y2 ? {
|
|
928
|
+
cp1: {
|
|
929
|
+
x,
|
|
930
|
+
y: y + (y2 - y) / 3 * 2
|
|
931
|
+
},
|
|
932
|
+
cp2: {
|
|
933
|
+
x: x2,
|
|
934
|
+
y: y + (y2 - y) / 3
|
|
935
|
+
}
|
|
936
|
+
} : {
|
|
937
|
+
cp1: {
|
|
938
|
+
x: 0,
|
|
939
|
+
y: y - (y - y2) / 3
|
|
940
|
+
},
|
|
941
|
+
cp2: {
|
|
942
|
+
x: 0,
|
|
943
|
+
y: y2 + (y - y2) / 3
|
|
944
|
+
}
|
|
945
|
+
} : x < x2 ? {
|
|
738
946
|
cp1: {
|
|
739
947
|
x: x + (x2 - x) / 3 * 2,
|
|
740
948
|
y
|
|
@@ -760,14 +968,16 @@ const pointInLine = (p1, p2, t)=>({
|
|
|
760
968
|
const applyAlpha = (original, alpha)=>helpers.color(original).alpha(alpha).rgbString();
|
|
761
969
|
const getColorOption = (option, alpha)=>typeof option === 'string' ? applyAlpha(option, alpha) : option;
|
|
762
970
|
const getHoverColorOption = (option)=>typeof option === 'string' ? helpers.getHoverColor(option) : option;
|
|
763
|
-
function setStyle(ctx, { x, x2, options }) {
|
|
971
|
+
function setStyle(ctx, { x, x2, y, y2, options }) {
|
|
764
972
|
let fill = 'black';
|
|
765
|
-
if (options.
|
|
973
|
+
if (options.flowColor !== null) {
|
|
974
|
+
fill = options.flowColor;
|
|
975
|
+
} else if (options.colorMode === 'from') {
|
|
766
976
|
fill = getColorOption(options.colorFrom, options.alpha);
|
|
767
977
|
} else if (options.colorMode === 'to') {
|
|
768
978
|
fill = getColorOption(options.colorTo, options.alpha);
|
|
769
979
|
} else if (typeof options.colorFrom === 'string' && typeof options.colorTo === 'string') {
|
|
770
|
-
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
980
|
+
fill = options.orientation === 'vertical' ? ctx.createLinearGradient(0, y, 0, y2) : ctx.createLinearGradient(x, 0, x2, 0);
|
|
771
981
|
fill.addColorStop(0, applyAlpha(options.colorFrom, options.alpha));
|
|
772
982
|
fill.addColorStop(1, applyAlpha(options.colorTo, options.alpha));
|
|
773
983
|
}
|
|
@@ -775,44 +985,104 @@ function setStyle(ctx, { x, x2, options }) {
|
|
|
775
985
|
ctx.strokeStyle = fill;
|
|
776
986
|
ctx.lineWidth = 0.5;
|
|
777
987
|
}
|
|
988
|
+
function clipFlow(ctx, { height, width, x, x2, y, y2 }, progress, orientation) {
|
|
989
|
+
ctx.beginPath();
|
|
990
|
+
if (orientation === 'vertical') {
|
|
991
|
+
ctx.rect(Math.min(x, x2), y, Math.abs(x2 - x) + width + 1, (y2 - y) * progress + 1);
|
|
992
|
+
} else {
|
|
993
|
+
ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
|
|
994
|
+
}
|
|
995
|
+
ctx.clip();
|
|
996
|
+
}
|
|
997
|
+
function drawFlowPath(ctx, { height, width, x, x2, y, y2 }, { cp1, cp2 }, orientation) {
|
|
998
|
+
ctx.beginPath();
|
|
999
|
+
ctx.moveTo(x, y);
|
|
1000
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
|
|
1001
|
+
if (orientation === 'vertical') {
|
|
1002
|
+
ctx.lineTo(x2 + width, y2);
|
|
1003
|
+
ctx.bezierCurveTo(cp2.x + width, cp2.y, cp1.x + width, cp1.y, x + width, y);
|
|
1004
|
+
} else {
|
|
1005
|
+
ctx.lineTo(x2, y2 + height);
|
|
1006
|
+
ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
|
|
1007
|
+
}
|
|
1008
|
+
ctx.lineTo(x, y);
|
|
1009
|
+
ctx.stroke();
|
|
1010
|
+
ctx.closePath();
|
|
1011
|
+
ctx.fill();
|
|
1012
|
+
}
|
|
1013
|
+
function getLabelRect({ height, width, x, x2, y, y2 }, orientation) {
|
|
1014
|
+
if (orientation === 'vertical') {
|
|
1015
|
+
return {
|
|
1016
|
+
height: y2 - y,
|
|
1017
|
+
width: Math.abs(x2 - x) + width,
|
|
1018
|
+
x: Math.min(x, x2),
|
|
1019
|
+
y
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
height: Math.abs(y2 - y) + height,
|
|
1024
|
+
width: x2 - x,
|
|
1025
|
+
x,
|
|
1026
|
+
y: Math.min(y, y2)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
778
1029
|
class Flow extends chart_js.Element {
|
|
779
1030
|
draw(ctx) {
|
|
780
|
-
const { x, x2, y, y2, height, progress } = this;
|
|
781
|
-
const
|
|
1031
|
+
const { x, x2, y, y2, height, progress, width } = this;
|
|
1032
|
+
const orientation = this.options.orientation;
|
|
1033
|
+
const controls = controlPoints(x, y, x2, y2, orientation);
|
|
1034
|
+
const geometry = {
|
|
1035
|
+
height,
|
|
1036
|
+
width,
|
|
1037
|
+
x,
|
|
1038
|
+
x2,
|
|
1039
|
+
y,
|
|
1040
|
+
y2
|
|
1041
|
+
};
|
|
782
1042
|
if (progress === 0) {
|
|
783
1043
|
return;
|
|
784
1044
|
}
|
|
785
1045
|
ctx.save();
|
|
786
1046
|
if (progress < 1) {
|
|
787
|
-
ctx
|
|
788
|
-
ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
|
|
789
|
-
ctx.clip();
|
|
1047
|
+
clipFlow(ctx, geometry, progress, orientation);
|
|
790
1048
|
}
|
|
791
1049
|
setStyle(ctx, this);
|
|
792
|
-
ctx
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1050
|
+
drawFlowPath(ctx, geometry, controls, orientation);
|
|
1051
|
+
const labels = this.options.flowLabels;
|
|
1052
|
+
if (labels.display) {
|
|
1053
|
+
const font = helpers.toFont(labels.font ?? chart_js.Chart.defaults.font);
|
|
1054
|
+
const labelRect = getLabelRect(geometry, orientation);
|
|
1055
|
+
drawLabel(ctx, `${this.flow}`, {
|
|
1056
|
+
autoPosition: 'center',
|
|
1057
|
+
backgroundColor: labels.backgroundColor,
|
|
1058
|
+
borderRadius: labels.borderRadius,
|
|
1059
|
+
borderWidth: 0,
|
|
1060
|
+
color: labels.color,
|
|
1061
|
+
font,
|
|
1062
|
+
height: labelRect.height,
|
|
1063
|
+
lineOffset: font.lineHeight / 2,
|
|
1064
|
+
padding: labels.padding,
|
|
1065
|
+
position: labels.position,
|
|
1066
|
+
width: labelRect.width,
|
|
1067
|
+
x: labelRect.x,
|
|
1068
|
+
y: labelRect.y
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
801
1071
|
ctx.restore();
|
|
802
1072
|
}
|
|
803
1073
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
804
|
-
const { x, y, x2, y2, height } = this.getProps([
|
|
1074
|
+
const { x, y, x2, y2, height, width } = this.getProps([
|
|
805
1075
|
'x',
|
|
806
1076
|
'y',
|
|
807
1077
|
'x2',
|
|
808
1078
|
'y2',
|
|
809
|
-
'height'
|
|
1079
|
+
'height',
|
|
1080
|
+
'width'
|
|
810
1081
|
], useFinalPosition);
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
814
|
-
const
|
|
815
|
-
const t = (mouseX - x) / (x2 - x);
|
|
1082
|
+
const vertical = this.options.orientation === 'vertical';
|
|
1083
|
+
if (vertical ? mouseY < y || mouseY > y2 : mouseX < x || mouseX > x2) return false;
|
|
1084
|
+
const { cp1, cp2 } = controlPoints(x, y, x2, y2, this.options.orientation);
|
|
1085
|
+
const t = vertical ? (mouseY - y) / (y2 - y) : (mouseX - x) / (x2 - x);
|
|
816
1086
|
const p1 = {
|
|
817
1087
|
x,
|
|
818
1088
|
y
|
|
@@ -826,15 +1096,18 @@ class Flow extends chart_js.Element {
|
|
|
826
1096
|
const c = pointInLine(cp2, p2, t);
|
|
827
1097
|
const d = pointInLine(a, b, t);
|
|
828
1098
|
const e = pointInLine(b, c, t);
|
|
829
|
-
const
|
|
830
|
-
return mouseY >=
|
|
1099
|
+
const edge = pointInLine(d, e, t);
|
|
1100
|
+
return vertical ? mouseX >= edge.x && mouseX <= edge.x + width : mouseY >= edge.y && mouseY <= edge.y + height;
|
|
831
1101
|
}
|
|
832
1102
|
inXRange(mouseX, useFinalPosition) {
|
|
833
|
-
const { x, x2 } = this.getProps([
|
|
1103
|
+
const { x, x2, width } = this.getProps([
|
|
834
1104
|
'x',
|
|
835
|
-
'x2'
|
|
1105
|
+
'x2',
|
|
1106
|
+
'width'
|
|
836
1107
|
], useFinalPosition);
|
|
837
|
-
|
|
1108
|
+
const min = Math.min(x, x2);
|
|
1109
|
+
const max = Math.max(x, x2) + (this.options.orientation === 'vertical' ? width : 0);
|
|
1110
|
+
return mouseX >= min && mouseX <= max;
|
|
838
1111
|
}
|
|
839
1112
|
inYRange(mouseY, useFinalPosition) {
|
|
840
1113
|
const { y, y2, height } = this.getProps([
|
|
@@ -843,30 +1116,34 @@ class Flow extends chart_js.Element {
|
|
|
843
1116
|
'height'
|
|
844
1117
|
], useFinalPosition);
|
|
845
1118
|
const minY = Math.min(y, y2);
|
|
846
|
-
const maxY = Math.max(y, y2) + height;
|
|
1119
|
+
const maxY = Math.max(y, y2) + (this.options.orientation === 'vertical' ? 0 : height);
|
|
847
1120
|
return mouseY >= minY && mouseY <= maxY;
|
|
848
1121
|
}
|
|
849
1122
|
getCenterPoint(useFinalPosition) {
|
|
850
|
-
const { x, y, x2, y2, height } = this.getProps([
|
|
1123
|
+
const { x, y, x2, y2, height, width } = this.getProps([
|
|
851
1124
|
'x',
|
|
852
1125
|
'y',
|
|
853
1126
|
'x2',
|
|
854
1127
|
'y2',
|
|
855
|
-
'height'
|
|
1128
|
+
'height',
|
|
1129
|
+
'width'
|
|
856
1130
|
], useFinalPosition);
|
|
1131
|
+
const vertical = this.options.orientation === 'vertical';
|
|
857
1132
|
return {
|
|
858
|
-
x: (x + x2) / 2,
|
|
859
|
-
y: (y + y2 + height) / 2
|
|
1133
|
+
x: (x + x2 + (vertical ? width : 0)) / 2,
|
|
1134
|
+
y: (y + y2 + (vertical ? 0 : height)) / 2
|
|
860
1135
|
};
|
|
861
1136
|
}
|
|
862
1137
|
tooltipPosition(useFinalPosition = false) {
|
|
863
1138
|
return this.getCenterPoint(useFinalPosition);
|
|
864
1139
|
}
|
|
865
1140
|
getRange(axis) {
|
|
866
|
-
|
|
1141
|
+
const vertical = this.options.orientation === 'vertical';
|
|
1142
|
+
if (axis === 'x') return vertical ? this.width / 2 : 0;
|
|
1143
|
+
return vertical ? 0 : this.height / 2;
|
|
867
1144
|
}
|
|
868
1145
|
constructor(cfg){
|
|
869
|
-
super(), this.x2 = 0, this.y2 = 0, this.width = 0, this.height = 0, this.progress = 1;
|
|
1146
|
+
super(), this.flow = 0, this.x2 = 0, this.y2 = 0, this.width = 0, this.height = 0, this.progress = 1;
|
|
870
1147
|
if (cfg) {
|
|
871
1148
|
Object.assign(this, cfg);
|
|
872
1149
|
}
|
|
@@ -878,11 +1155,23 @@ Flow.defaults = {
|
|
|
878
1155
|
colorFrom: 'red',
|
|
879
1156
|
colorMode: 'gradient',
|
|
880
1157
|
colorTo: 'green',
|
|
1158
|
+
flowColor: null,
|
|
1159
|
+
flowLabels: {
|
|
1160
|
+
borderRadius: 0,
|
|
1161
|
+
color: 'black',
|
|
1162
|
+
display: false,
|
|
1163
|
+
padding: 4,
|
|
1164
|
+
position: 'center'
|
|
1165
|
+
},
|
|
881
1166
|
hoverColorFrom: (_ctx, options)=>getHoverColorOption(options.colorFrom),
|
|
882
|
-
hoverColorTo: (_ctx, options)=>getHoverColorOption(options.colorTo)
|
|
1167
|
+
hoverColorTo: (_ctx, options)=>getHoverColorOption(options.colorTo),
|
|
1168
|
+
orientation: 'horizontal'
|
|
883
1169
|
};
|
|
884
1170
|
Flow.descriptors = {
|
|
885
|
-
_scriptable: true
|
|
1171
|
+
_scriptable: true,
|
|
1172
|
+
flowLabels: {
|
|
1173
|
+
_scriptable: true
|
|
1174
|
+
}
|
|
886
1175
|
};
|
|
887
1176
|
|
|
888
1177
|
chart_js.Chart.register(SankeyController, Flow);
|