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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.14.4
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 (const flow of node.to){
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
- y = Math.max(n.y + Math.max(n.in, n.out), y);
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
- nextYStart = nodes.length ? nodes[0].to.filter((to)=>nodeX$1(to.node) > x + 1).reduce((acc, cur)=>acc + cur.flow, 0) || 0 : 0;
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 === this._cachedMeta.xScale ? this._maxX : this._maxY,
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
- to: custom.to,
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 x = xScale.getPixelForValue(nodeX(node));
529
- const y = yScale.getPixelForValue(nodeY(node));
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
- let textX = x;
534
- ctx.fillStyle = options.color ?? 'black';
535
- ctx.textBaseline = 'middle';
536
- if (x < chartArea.width / 2) {
537
- ctx.textAlign = 'left';
538
- textX += nodeWidth + borderWidth + 4;
539
- } else {
540
- ctx.textAlign = 'right';
541
- textX -= borderWidth + 4;
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 = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
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, nodeWidth, height);
763
+ ctx.strokeRect(x, y, width, height);
584
764
  }
585
- ctx.fillRect(x, y, nodeWidth, height);
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)=>x < x2 ? {
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.colorMode === 'from') {
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 { cp1, cp2 } = controlPoints(x, y, x2, y2);
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.beginPath();
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.beginPath();
793
- ctx.moveTo(x, y);
794
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
795
- ctx.lineTo(x2, y2 + height);
796
- ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
797
- ctx.lineTo(x, y);
798
- ctx.stroke();
799
- ctx.closePath();
800
- ctx.fill();
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
- if (mouseX < x || mouseX > x2) {
812
- return false;
813
- }
814
- const { cp1, cp2 } = controlPoints(x, y, x2, y2);
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 topY = pointInLine(d, e, t).y;
830
- return mouseY >= topY && mouseY <= topY + height;
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
- return mouseX >= x && mouseX <= x2;
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
- return axis === 'x' ? this.width / 2 : this.height / 2;
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);