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