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
@@ -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 (const flow of node.to){
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
- y = Math.max(n.y + Math.max(n.in, n.out), y);
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
- nextYStart = nodes.length ? nodes[0].to.filter((to)=>nodeX$1(to.node) > x + 1).reduce((acc, cur)=>acc + cur.flow, 0) || 0 : 0;
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 === this._cachedMeta.xScale ? this._maxX : this._maxY,
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
- to: custom.to,
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 x = xScale.getPixelForValue(nodeX(node));
526
- const y = yScale.getPixelForValue(nodeY(node));
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
- let textX = x;
531
- ctx.fillStyle = options.color ?? 'black';
532
- ctx.textBaseline = 'middle';
533
- if (x < chartArea.width / 2) {
534
- ctx.textAlign = 'left';
535
- textX += nodeWidth + borderWidth + 4;
536
- } else {
537
- ctx.textAlign = 'right';
538
- textX -= borderWidth + 4;
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 = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
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, nodeWidth, height);
760
+ ctx.strokeRect(x, y, width, height);
581
761
  }
582
- ctx.fillRect(x, y, nodeWidth, height);
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)=>x < x2 ? {
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.colorMode === 'from') {
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 { cp1, cp2 } = controlPoints(x, y, x2, y2);
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.beginPath();
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.beginPath();
790
- ctx.moveTo(x, y);
791
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
792
- ctx.lineTo(x2, y2 + height);
793
- ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
794
- ctx.lineTo(x, y);
795
- ctx.stroke();
796
- ctx.closePath();
797
- ctx.fill();
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
- if (mouseX < x || mouseX > x2) {
809
- return false;
810
- }
811
- const { cp1, cp2 } = controlPoints(x, y, x2, y2);
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 topY = pointInLine(d, e, t).y;
827
- return mouseY >= topY && mouseY <= topY + height;
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
- return mouseX >= x && mouseX <= x2;
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
- return axis === 'x' ? this.width / 2 : this.height / 2;
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 };