chartjs-chart-treemap 5.0.0-next.8 → 5.0.0-next.9

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-treemap v5.0.0-next.8
2
+ * chartjs-chart-treemap v5.0.0-next.9
3
3
  * https://chartjs-chart-treemap.pages.dev/
4
4
  * (c) 2026 Jukka Kurkela
5
5
  * Released under the MIT license
@@ -10,7 +10,7 @@ typeof define === 'function' && define.amd ? define(['exports', 'chart.js', 'cha
10
10
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["chartjs-chart-treemap"] = {}, global.Chart, global.Chart.helpers));
11
11
  })(this, (function (exports, chart_js, helpers) { 'use strict';
12
12
 
13
- var version = "5.0.0-next.8";
13
+ var version = "5.0.0-next.9";
14
14
 
15
15
  function scaleRect(sq, xScale, yScale, sp) {
16
16
  const sp2 = sp * 2;
@@ -60,6 +60,14 @@ function parseBorderRadius(value, maxW, maxH) {
60
60
 
61
61
  function getDims(itm, w2, s2, key) {
62
62
  const a = itm._normalized;
63
+ if (!a) {
64
+ return {
65
+ d1: 0,
66
+ d2: 0,
67
+ h: 0,
68
+ w: 0
69
+ };
70
+ }
63
71
  const ar = w2 * a / s2;
64
72
  const d1 = Math.sqrt(a * ar);
65
73
  const d2 = a / d1;
@@ -427,6 +435,7 @@ function requireVersion(pkg, min, ver, strict = true) {
427
435
  return true;
428
436
  }
429
437
 
438
+ const WEIGHT_KEY = '_weight';
430
439
  function compareAspectRatio(oldStat, newStat, args) {
431
440
  if (oldStat.sum === 0) {
432
441
  return true;
@@ -460,16 +469,17 @@ function compareAspectRatio(oldStat, newStat, args) {
460
469
  return node;
461
470
  });
462
471
  }
463
- function sortNodes(nodes) {
464
- sort(nodes, 'v');
472
+ function sortNodes(nodes, weighted = false) {
473
+ sort(nodes, weighted ? WEIGHT_KEY : 'v');
465
474
  }
466
- function packInto(nodes, rectangle) {
475
+ function packInto(nodes, rectangle, weighted = false) {
467
476
  const n = nodes.length;
468
477
  if (!n) {
469
478
  return;
470
479
  }
480
+ const key = weighted ? WEIGHT_KEY : 'v';
471
481
  const rect = new Rect(rectangle);
472
- const row = new StatArray('v', rect.area / sum(nodes, 'v'));
482
+ const row = new StatArray(key, rect.area / sum(nodes, key));
473
483
  let length = rect.side;
474
484
  let o;
475
485
  for(let i = 0; i < n; ++i){
@@ -488,47 +498,66 @@ function compareAspectRatio(oldStat, newStat, args) {
488
498
 
489
499
  const WIDTH_CACHE_LIMIT = 4096;
490
500
  const widthCache = new Map();
491
- function shouldDrawCaption(displayMode, rect, options) {
492
- if (!options || options.display === false) {
493
- return false;
494
- }
495
- if (displayMode === 'headerBoxes') {
496
- return true;
501
+ const fontFor = (fonts, index)=>fonts[Math.min(index, fonts.length - 1)];
502
+ function measureLabelSize(ctx, lines, fonts) {
503
+ const fontsKey = fonts.reduce((prev, item)=>{
504
+ prev += item.string;
505
+ return prev;
506
+ }, '');
507
+ const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
508
+ let size = widthCache.get(mapKey);
509
+ if (!size) {
510
+ ctx.save();
511
+ const count = lines.length;
512
+ let width = 0;
513
+ let height = 0;
514
+ for(let i = 0; i < count; i++){
515
+ const font = fonts[Math.min(i, fonts.length - 1)];
516
+ ctx.font = font.string;
517
+ const text = lines[i];
518
+ width = Math.max(width, ctx.measureText(text).width);
519
+ height += font.lineHeight;
520
+ }
521
+ ctx.restore();
522
+ size = {
523
+ height,
524
+ width
525
+ };
526
+ if (widthCache.size >= WIDTH_CACHE_LIMIT) {
527
+ widthCache.clear();
528
+ }
529
+ widthCache.set(mapKey, size);
497
530
  }
498
- const { w, h } = rect;
499
- const font = helpers.toFont(options.font || {});
500
- const min = font.lineHeight;
501
- const padding = Math.max(Math.min(helpers.valueOrDefault(options.padding, 3) * 2, Math.min(w, h)), 0);
502
- return w - padding > min && h - padding > min;
531
+ return size;
503
532
  }
504
- function getCaptionHeight(displayMode, rect, font, padding) {
505
- if (displayMode !== 'headerBoxes') {
506
- return font.lineHeight + padding * 2;
507
- }
508
- const captionHeight = font.lineHeight + padding * 2;
509
- return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight;
533
+ function measureLines(ctx, lines, styles, fonts) {
534
+ let width = 0;
535
+ let height = 0;
536
+ lines.forEach((line, i)=>{
537
+ const font = fontFor(fonts, styles[i]);
538
+ width = Math.max(width, measureLabelSize(ctx, [
539
+ line
540
+ ], [
541
+ font
542
+ ]).width);
543
+ height += font.lineHeight;
544
+ });
545
+ return {
546
+ height,
547
+ width
548
+ };
510
549
  }
511
- function drawText(ctx, rect, options, item, element, layout) {
512
- const { captions, labels } = options;
513
- const { displayMode } = layout;
514
- const isLeaf = item && (!helpers.defined(item.l) || item.isLeaf);
515
- let block;
516
- if (isLeaf) {
517
- if (labels.display) {
518
- block = labelBlock(ctx, rect, options, item, element);
519
- }
520
- } else if (shouldDrawCaption(displayMode, rect, captions)) {
521
- block = captionBlock(ctx, rect, options, item, element, layout);
522
- }
523
- if (!block) {
524
- return;
525
- }
526
- ctx.save();
527
- clipToPadding(ctx, rect, block.padding, block.clipY);
528
- drawTextBlock(ctx, rect, block);
529
- ctx.restore();
550
+ function toFonts(fonts, fitRatio) {
551
+ return fonts.map((f)=>{
552
+ const { lineHeight: _lineHeight, ...font } = f;
553
+ return helpers.toFont({
554
+ ...font,
555
+ size: Math.floor(f.size * fitRatio)
556
+ });
557
+ });
530
558
  }
531
- function clipToPadding(ctx, rect, padding, clipY) {
559
+
560
+ function clipToPadding(ctx, rect, padding, clipY) {
532
561
  const x = Math.max(0, padding);
533
562
  const y = Math.max(0, clipY);
534
563
  ctx.beginPath();
@@ -551,134 +580,45 @@ function drawTextBlock(ctx, rect, block) {
551
580
  offset += font.lineHeight;
552
581
  });
553
582
  }
554
- function callbackContext(element, item) {
555
- const context = element.$context || {
556
- active: !!element.active,
557
- raw: item,
558
- type: 'data'
559
- };
560
- context.active = !!element.active;
561
- context.raw = item;
562
- return context;
563
- }
564
- function resolveOption(option, context) {
565
- return typeof option === 'function' ? option(context) : option;
566
- }
567
- function resolveCaptionText(captions, element, item) {
568
- return resolveOption(captions.formatter, callbackContext(element, item)) || item.g || '';
569
- }
570
- function captionText(ctx, rect, captions, element, item, fonts) {
571
- const text = resolveCaptionText(captions, element, item);
572
- if (!text) {
573
- return undefined;
574
- }
575
- const padding = captions.padding;
576
- if (measureLabelSize(ctx, [
577
- text
578
- ], fonts).width + 2 * padding <= rect.w) {
579
- return text;
580
- }
581
- const overflow = captions.overflow || 'ellipsis';
582
- if (overflow === 'hidden') {
583
- return undefined;
583
+ function calculateBlockTop(rect, options, labelSize) {
584
+ const { position, padding } = options;
585
+ if (position === 'top') {
586
+ return rect.y + padding;
584
587
  }
585
- if (overflow === 'cut') {
586
- return text;
588
+ if (position === 'bottom') {
589
+ return rect.y + rect.h - padding - labelSize.height;
587
590
  }
588
- return sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
591
+ return rect.y + (rect.h - labelSize.height) / 2 + padding;
589
592
  }
590
- function captionBlock(ctx, rect, options, item, element, layout) {
591
- const { displayMode, rtl, spacing } = layout;
592
- const { captions } = options;
593
- const { color, hoverColor, font, hoverFont, padding, align } = captions;
594
- const optFont = (rect.active ? hoverFont : font) || font;
595
- const oFont = helpers.toFont(optFont);
596
- if (oFont.lineHeight > rect.h) {
597
- return;
593
+ function calculateBlockLeft(rect, align, padding, width) {
594
+ if (align === 'left') {
595
+ return rect.x + padding;
598
596
  }
599
- const text = captionText(ctx, rect, captions, element, item, [
600
- oFont
601
- ]);
602
- if (text === undefined) {
603
- return;
597
+ if (align === 'right') {
598
+ return rect.x + rect.w - padding - width;
604
599
  }
605
- const fonts = [
606
- oFont
607
- ];
608
- const top = displayMode === 'headerBoxes' ? rect.y + (rect.h - oFont.lineHeight) / 2 : rect.y + padding + spacing;
609
- return {
610
- align: align || (rtl ? 'right' : 'left'),
611
- clipY: displayMode === 'headerBoxes' ? 0 : padding,
612
- colors: [
613
- (rect.active ? hoverColor : color) || color
614
- ],
615
- fonts,
616
- lines: [
617
- text
618
- ],
619
- padding,
620
- styles: [
621
- 0
622
- ],
623
- textAlign: captions.textAlign,
624
- top,
625
- width: measureLabelSize(ctx, [
626
- text
627
- ], fonts).width
628
- };
600
+ return rect.x + (rect.w - width) / 2;
629
601
  }
630
- function labelBlock(ctx, rect, options, item, element) {
631
- const labels = options.labels;
632
- const content = resolveOption(labels.formatter, callbackContext(element, item));
633
- if (!content) {
634
- return;
635
- }
636
- const { align, color, hoverColor, overflow, padding, wrap } = labels;
637
- const maxWidth = rect.w - padding * 2;
638
- const maxHeight = rect.h - padding * 2;
639
- let fonts = getFontFromOptions(rect, labels);
640
- let { lines, styles } = toLines(ctx, content, fonts, maxWidth, !!wrap);
641
- let labelSize = measureLines(ctx, lines, styles, fonts);
642
- const lblToDraw = labelToDraw(rect, labels, labelSize);
643
- if (!lblToDraw) {
644
- return;
645
- }
646
- if (helpers.isNumber(lblToDraw)) {
647
- fonts = toFonts(fonts, lblToDraw);
648
- if (wrap) {
649
- const rewrapped = toLines(ctx, content, fonts, maxWidth, true);
650
- lines = rewrapped.lines;
651
- styles = rewrapped.styles;
652
- labelSize = measureLines(ctx, lines, styles, fonts);
653
- } else {
654
- labelSize = {
655
- height: labelSize.height * lblToDraw,
656
- width: labelSize.width * lblToDraw
657
- };
602
+ function calculateLineX(rect, align, lineAlign, padding, width) {
603
+ if (align === lineAlign) {
604
+ if (align === 'left') {
605
+ return rect.x + padding;
606
+ }
607
+ if (align === 'right') {
608
+ return rect.x + rect.w - padding;
658
609
  }
610
+ return rect.x + rect.w / 2;
659
611
  }
660
- if (overflow === 'ellipsis') {
661
- const trimmed = applyEllipsis(ctx, lines, styles, fonts, maxWidth, maxHeight);
662
- lines = trimmed.lines;
663
- styles = trimmed.styles;
664
- labelSize = measureLines(ctx, lines, styles, fonts);
612
+ const left = calculateBlockLeft(rect, align, padding, width);
613
+ if (lineAlign === 'left') {
614
+ return left;
665
615
  }
666
- const optColor = (rect.active ? hoverColor : color) || color;
667
- return {
668
- align,
669
- clipY: padding,
670
- colors: helpers.isArray(optColor) ? optColor : [
671
- optColor
672
- ],
673
- fonts,
674
- lines,
675
- padding,
676
- styles,
677
- textAlign: labels.textAlign,
678
- top: calculateBlockTop(rect, labels, labelSize),
679
- width: labelSize.width
680
- };
616
+ if (lineAlign === 'right') {
617
+ return left + width;
618
+ }
619
+ return left + width / 2;
681
620
  }
621
+
682
622
  function sliceTextToFitWidth(ctx, text, width, fonts) {
683
623
  const ellipsis = '...';
684
624
  const ellipsisWidth = measureLabelSize(ctx, [
@@ -705,14 +645,60 @@ function sliceTextToFitWidth(ctx, text, width, fonts) {
705
645
  const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
706
646
  return slicedText ? slicedText + ellipsis : '';
707
647
  }
708
- const fontFor = (fonts, index)=>fonts[Math.min(index, fonts.length - 1)];
709
- function wrapToWidth(ctx, text, font, width) {
710
- if (width <= 0) {
711
- return [
712
- text
713
- ];
648
+ function applyEllipsis(ctx, lines, styles, fonts, maxWidth, maxHeight) {
649
+ const keptLines = [];
650
+ const keptStyles = [];
651
+ let used = 0;
652
+ for(let i = 0; i < lines.length; i++){
653
+ const font = fontFor(fonts, styles[i]);
654
+ if (keptLines.length && used + font.lineHeight > maxHeight) {
655
+ break;
656
+ }
657
+ used += font.lineHeight;
658
+ keptLines.push(lines[i]);
659
+ keptStyles.push(styles[i]);
714
660
  }
715
- const fits = (value)=>measureLabelSize(ctx, [
661
+ const dropped = keptLines.length < lines.length;
662
+ const last = keptLines.length - 1;
663
+ for(let i = 0; i < keptLines.length; i++){
664
+ const font = fontFor(fonts, keptStyles[i]);
665
+ const tooWide = measureLabelSize(ctx, [
666
+ keptLines[i]
667
+ ], [
668
+ font
669
+ ]).width > maxWidth;
670
+ if (tooWide || dropped && i === last) {
671
+ keptLines[i] = sliceTextToFitWidth(ctx, keptLines[i], maxWidth, [
672
+ font
673
+ ]);
674
+ }
675
+ }
676
+ return {
677
+ lines: keptLines,
678
+ styles: keptStyles
679
+ };
680
+ }
681
+ function labelToDraw(rect, options, labelSize) {
682
+ const { overflow, padding } = options;
683
+ const { width, height } = labelSize;
684
+ if (overflow === 'hidden') {
685
+ return !(width + padding * 2 > rect.w || height + padding * 2 > rect.h);
686
+ } else if (overflow === 'fit') {
687
+ const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
688
+ if (ratio < 1) {
689
+ return ratio;
690
+ }
691
+ }
692
+ return true;
693
+ }
694
+
695
+ function wrapToWidth(ctx, text, font, width) {
696
+ if (width <= 0) {
697
+ return [
698
+ text
699
+ ];
700
+ }
701
+ const fits = (value)=>measureLabelSize(ctx, [
716
702
  value
717
703
  ], [
718
704
  font
@@ -753,7 +739,7 @@ const fontFor = (fonts, index)=>fonts[Math.min(index, fonts.length - 1)];
753
739
  ''
754
740
  ];
755
741
  }
756
- function toLines(ctx, content, fonts, width, wrap) {
742
+ function toLines(ctx, content, fonts, width, wrap) {
757
743
  const paragraphs = (helpers.isArray(content) ? content : [
758
744
  content
759
745
  ]).map((entry)=>`${entry}`);
@@ -775,168 +761,193 @@ const fontFor = (fonts, index)=>fonts[Math.min(index, fonts.length - 1)];
775
761
  styles
776
762
  };
777
763
  }
778
- function measureLines(ctx, lines, styles, fonts) {
779
- let width = 0;
780
- let height = 0;
781
- lines.forEach((line, i)=>{
782
- const font = fontFor(fonts, styles[i]);
783
- width = Math.max(width, measureLabelSize(ctx, [
784
- line
785
- ], [
786
- font
787
- ]).width);
788
- height += font.lineHeight;
789
- });
790
- return {
791
- height,
792
- width
793
- };
764
+
765
+ function shouldDrawCaption(displayMode, rect, options) {
766
+ if (!options || options.display === false) {
767
+ return false;
768
+ }
769
+ if (displayMode === 'headerBoxes') {
770
+ return true;
771
+ }
772
+ const { w, h } = rect;
773
+ const font = helpers.toFont(options.font || {});
774
+ const min = font.lineHeight;
775
+ const padding = Math.max(Math.min(helpers.valueOrDefault(options.padding, 3) * 2, Math.min(w, h)), 0);
776
+ return w - padding > min && h - padding > min;
794
777
  }
795
- function applyEllipsis(ctx, lines, styles, fonts, maxWidth, maxHeight) {
796
- const keptLines = [];
797
- const keptStyles = [];
798
- let used = 0;
799
- for(let i = 0; i < lines.length; i++){
800
- const font = fontFor(fonts, styles[i]);
801
- if (keptLines.length && used + font.lineHeight > maxHeight) {
802
- break;
803
- }
804
- used += font.lineHeight;
805
- keptLines.push(lines[i]);
806
- keptStyles.push(styles[i]);
778
+ function getCaptionHeight(displayMode, rect, font, padding) {
779
+ if (displayMode !== 'headerBoxes') {
780
+ return font.lineHeight + padding * 2;
807
781
  }
808
- const dropped = keptLines.length < lines.length;
809
- const last = keptLines.length - 1;
810
- for(let i = 0; i < keptLines.length; i++){
811
- const font = fontFor(fonts, keptStyles[i]);
812
- const tooWide = measureLabelSize(ctx, [
813
- keptLines[i]
814
- ], [
815
- font
816
- ]).width > maxWidth;
817
- if (tooWide || dropped && i === last) {
818
- keptLines[i] = sliceTextToFitWidth(ctx, keptLines[i], maxWidth, [
819
- font
820
- ]);
782
+ const captionHeight = font.lineHeight + padding * 2;
783
+ return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight;
784
+ }
785
+ function drawText(ctx, rect, options, item, element, layout) {
786
+ const { captions, labels } = options;
787
+ const { displayMode } = layout;
788
+ const isLeaf = item && (!helpers.defined(item.l) || item.isLeaf);
789
+ let block;
790
+ if (isLeaf) {
791
+ if (labels.display) {
792
+ block = labelBlock(ctx, rect, options, item, element);
821
793
  }
794
+ } else if (shouldDrawCaption(displayMode, rect, captions)) {
795
+ block = captionBlock(ctx, rect, options, item, element, layout);
822
796
  }
823
- return {
824
- lines: keptLines,
825
- styles: keptStyles
797
+ if (!block) {
798
+ return;
799
+ }
800
+ ctx.save();
801
+ clipToPadding(ctx, rect, block.padding, block.clipY);
802
+ drawTextBlock(ctx, rect, block);
803
+ ctx.restore();
804
+ }
805
+ function callbackContext(element, item) {
806
+ const context = element.$context || {
807
+ active: !!element.active,
808
+ raw: item,
809
+ type: 'data'
826
810
  };
811
+ context.active = !!element.active;
812
+ context.raw = item;
813
+ return context;
827
814
  }
828
- function measureLabelSize(ctx, lines, fonts) {
829
- const fontsKey = fonts.reduce((prev, item)=>{
830
- prev += item.string;
831
- return prev;
832
- }, '');
833
- const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
834
- let size = widthCache.get(mapKey);
835
- if (!size) {
836
- ctx.save();
837
- const count = lines.length;
838
- let width = 0;
839
- let height = 0;
840
- for(let i = 0; i < count; i++){
841
- const font = fonts[Math.min(i, fonts.length - 1)];
842
- ctx.font = font.string;
843
- const text = lines[i];
844
- width = Math.max(width, ctx.measureText(text).width);
845
- height += font.lineHeight;
846
- }
847
- ctx.restore();
848
- size = {
849
- height,
850
- width
851
- };
852
- if (widthCache.size >= WIDTH_CACHE_LIMIT) {
853
- widthCache.clear();
854
- }
855
- widthCache.set(mapKey, size);
856
- }
857
- return size;
815
+ function resolveOption(option, context) {
816
+ return typeof option === 'function' ? option(context) : option;
858
817
  }
859
- function toFonts(fonts, fitRatio) {
860
- return fonts.map((f)=>{
861
- const { lineHeight: _lineHeight, ...font } = f;
862
- return helpers.toFont({
863
- ...font,
864
- size: Math.floor(f.size * fitRatio)
865
- });
866
- });
818
+ function resolveCaptionText(captions, element, item) {
819
+ return resolveOption(captions.formatter, callbackContext(element, item)) || item.g || '';
867
820
  }
868
- function labelToDraw(rect, options, labelSize) {
869
- const { overflow, padding } = options;
870
- const { width, height } = labelSize;
821
+ function captionText(ctx, rect, captions, element, item, fonts) {
822
+ const text = resolveCaptionText(captions, element, item);
823
+ if (!text) {
824
+ return undefined;
825
+ }
826
+ const padding = captions.padding;
827
+ if (measureLabelSize(ctx, [
828
+ text
829
+ ], fonts).width + 2 * padding <= rect.w) {
830
+ return text;
831
+ }
832
+ const overflow = captions.overflow || 'ellipsis';
871
833
  if (overflow === 'hidden') {
872
- return !(width + padding * 2 > rect.w || height + padding * 2 > rect.h);
873
- } else if (overflow === 'fit') {
874
- const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
875
- if (ratio < 1) {
876
- return ratio;
877
- }
834
+ return undefined;
878
835
  }
879
- return true;
836
+ if (overflow === 'cut') {
837
+ return text;
838
+ }
839
+ return sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
880
840
  }
881
- function getFontFromOptions(rect, labels) {
882
- const { font, hoverFont } = labels;
841
+ function captionBlock(ctx, rect, options, item, element, layout) {
842
+ const { displayMode, rtl, spacing } = layout;
843
+ const { captions } = options;
844
+ const { color, hoverColor, font, hoverFont, padding, align } = captions;
883
845
  const optFont = (rect.active ? hoverFont : font) || font;
884
- return Array.isArray(optFont) ? optFont.map((f)=>helpers.toFont(f)) : [
885
- helpers.toFont(optFont)
886
- ];
887
- }
888
- function calculateBlockTop(rect, options, labelSize) {
889
- const { position, padding } = options;
890
- if (position === 'top') {
891
- return rect.y + padding;
846
+ const oFont = helpers.toFont(optFont);
847
+ if (oFont.lineHeight > rect.h) {
848
+ return;
892
849
  }
893
- if (position === 'bottom') {
894
- return rect.y + rect.h - padding - labelSize.height;
850
+ const text = captionText(ctx, rect, captions, element, item, [
851
+ oFont
852
+ ]);
853
+ if (text === undefined) {
854
+ return;
895
855
  }
896
- return rect.y + (rect.h - labelSize.height) / 2 + padding;
856
+ const fonts = [
857
+ oFont
858
+ ];
859
+ const top = displayMode === 'headerBoxes' ? rect.y + (rect.h - oFont.lineHeight) / 2 : rect.y + padding + spacing;
860
+ return {
861
+ align: align || (rtl ? 'right' : 'left'),
862
+ clipY: displayMode === 'headerBoxes' ? 0 : padding,
863
+ colors: [
864
+ (rect.active ? hoverColor : color) || color
865
+ ],
866
+ fonts,
867
+ lines: [
868
+ text
869
+ ],
870
+ padding,
871
+ styles: [
872
+ 0
873
+ ],
874
+ textAlign: captions.textAlign,
875
+ top,
876
+ width: measureLabelSize(ctx, [
877
+ text
878
+ ], fonts).width
879
+ };
897
880
  }
898
- function calculateBlockLeft(rect, align, padding, width) {
899
- if (align === 'left') {
900
- return rect.x + padding;
881
+ function labelBlock(ctx, rect, options, item, element) {
882
+ const labels = options.labels;
883
+ const content = resolveOption(labels.formatter, callbackContext(element, item));
884
+ if (!content) {
885
+ return;
901
886
  }
902
- if (align === 'right') {
903
- return rect.x + rect.w - padding - width;
887
+ const { align, color, hoverColor, overflow, padding, wrap } = labels;
888
+ const maxWidth = rect.w - padding * 2;
889
+ const maxHeight = rect.h - padding * 2;
890
+ let fonts = getFontFromOptions(rect, labels);
891
+ let { lines, styles } = toLines(ctx, content, fonts, maxWidth, !!wrap);
892
+ let labelSize = measureLines(ctx, lines, styles, fonts);
893
+ const lblToDraw = labelToDraw(rect, labels, labelSize);
894
+ if (!lblToDraw) {
895
+ return;
904
896
  }
905
- return rect.x + (rect.w - width) / 2;
906
- }
907
- function calculateLineX(rect, align, lineAlign, padding, width) {
908
- if (align === lineAlign) {
909
- if (align === 'left') {
910
- return rect.x + padding;
911
- }
912
- if (align === 'right') {
913
- return rect.x + rect.w - padding;
897
+ if (helpers.isNumber(lblToDraw)) {
898
+ fonts = toFonts(fonts, lblToDraw);
899
+ if (wrap) {
900
+ const rewrapped = toLines(ctx, content, fonts, maxWidth, true);
901
+ lines = rewrapped.lines;
902
+ styles = rewrapped.styles;
903
+ labelSize = measureLines(ctx, lines, styles, fonts);
904
+ } else {
905
+ labelSize = {
906
+ height: labelSize.height * lblToDraw,
907
+ width: labelSize.width * lblToDraw
908
+ };
914
909
  }
915
- return rect.x + rect.w / 2;
916
910
  }
917
- const left = calculateBlockLeft(rect, align, padding, width);
918
- if (lineAlign === 'left') {
919
- return left;
920
- }
921
- if (lineAlign === 'right') {
922
- return left + width;
911
+ if (overflow === 'ellipsis') {
912
+ const trimmed = applyEllipsis(ctx, lines, styles, fonts, maxWidth, maxHeight);
913
+ lines = trimmed.lines;
914
+ styles = trimmed.styles;
915
+ labelSize = measureLines(ctx, lines, styles, fonts);
923
916
  }
924
- return left + width / 2;
917
+ const optColor = (rect.active ? hoverColor : color) || color;
918
+ return {
919
+ align,
920
+ clipY: padding,
921
+ colors: helpers.isArray(optColor) ? optColor : [
922
+ optColor
923
+ ],
924
+ fonts,
925
+ lines,
926
+ padding,
927
+ styles,
928
+ textAlign: labels.textAlign,
929
+ top: calculateBlockTop(rect, labels, labelSize),
930
+ width: labelSize.width
931
+ };
932
+ }
933
+ function getFontFromOptions(rect, labels) {
934
+ const { font, hoverFont } = labels;
935
+ const optFont = (rect.active ? hoverFont : font) || font;
936
+ return Array.isArray(optFont) ? optFont.map((f)=>helpers.toFont(f)) : [
937
+ helpers.toFont(optFont)
938
+ ];
925
939
  }
926
940
 
927
- function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
941
+ function buildHierarchy(values, gidx, keys, layout, weighted, parentGroupValue) {
928
942
  const { groups, leafKey, unsorted } = layout;
929
943
  const glen = groups.length;
930
944
  const g = getGroupKey(groups[gidx]);
931
945
  const gdata = group(values, g, keys, leafKey, undefined, parentGroupValue, groups.filter((_item, index)=>index <= gidx), groups, gidx);
932
946
  const nodes = toNodes(gdata, keys, g, gidx);
933
- if (!unsorted) {
934
- sortNodes(nodes);
935
- }
936
947
  for (const node of nodes){
937
948
  const record = node._data;
938
949
  const nextGroupIndex = record.groupIndex + 1;
939
- const children = gidx < glen - 1 && nextGroupIndex < glen ? buildHierarchy(record.children, nextGroupIndex, keys, layout, node.g) : [];
950
+ const children = gidx < glen - 1 && nextGroupIndex < glen ? buildHierarchy(record.children, nextGroupIndex, keys, layout, weighted, node.g) : [];
940
951
  Object.defineProperty(node, '_children', {
941
952
  configurable: true,
942
953
  value: children,
@@ -944,8 +955,91 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
944
955
  });
945
956
  node.isLeaf = !children.length;
946
957
  }
947
- return nodes;
958
+ if (weighted) {
959
+ applyValueScale(nodes, scaleFor(layout));
960
+ }
961
+ const withOthers = layout.others ? applyOthers(nodes, layout.others, weighted) : nodes;
962
+ if (!unsorted) {
963
+ sortNodes(withOthers, weighted);
964
+ }
965
+ return withOthers;
966
+ }
967
+ function applyValueScale(nodes, scale) {
968
+ for (const node of nodes){
969
+ const children = node._children;
970
+ let weight;
971
+ if (children?.length) {
972
+ applyValueScale(children, scale);
973
+ weight = children.reduce((total, child)=>total + child[WEIGHT_KEY], 0);
974
+ } else {
975
+ const scaled = scale(node.v, node._data);
976
+ weight = Number.isFinite(scaled) && scaled > 0 ? scaled : 0;
977
+ }
978
+ Object.defineProperty(node, WEIGHT_KEY, {
979
+ configurable: true,
980
+ value: weight,
981
+ writable: true
982
+ });
983
+ }
984
+ }
985
+ function applyOthers(nodes, others, weighted) {
986
+ const { label = 'Other', minCount = 2, threshold } = others;
987
+ if (!(threshold > 0) || nodes.some((node)=>node._children?.length)) {
988
+ return nodes;
989
+ }
990
+ const weightOf = (node)=>weighted ? node[WEIGHT_KEY] : node.v;
991
+ const total = nodes.reduce((sum, node)=>sum + weightOf(node), 0);
992
+ if (!(total > 0)) {
993
+ return nodes;
994
+ }
995
+ const small = nodes.filter((node)=>weightOf(node) / total < threshold);
996
+ if (small.length < Math.max(2, minCount)) {
997
+ return nodes;
998
+ }
999
+ const kept = nodes.filter((node)=>!small.includes(node));
1000
+ const bucket = {
1001
+ _data: {
1002
+ label,
1003
+ others: small.map((node)=>node._data)
1004
+ },
1005
+ h: 0,
1006
+ isLeaf: true,
1007
+ isOthers: true,
1008
+ s: 0,
1009
+ v: small.reduce((sum, node)=>sum + node.v, 0),
1010
+ vs: sumValues(small),
1011
+ w: 0,
1012
+ x: 0,
1013
+ y: 0
1014
+ };
1015
+ const grouped = nodes.find((node)=>node.g !== undefined);
1016
+ if (grouped) {
1017
+ bucket.g = label;
1018
+ bucket.l = grouped.l;
1019
+ }
1020
+ if (weighted) {
1021
+ Object.defineProperty(bucket, WEIGHT_KEY, {
1022
+ configurable: true,
1023
+ value: small.reduce((sum, node)=>sum + weightOf(node), 0),
1024
+ writable: true
1025
+ });
1026
+ }
1027
+ kept.push(bucket);
1028
+ return kept;
948
1029
  }
1030
+ function sumValues(nodes) {
1031
+ const totals = {};
1032
+ for (const node of nodes){
1033
+ for (const [key, value] of Object.entries(node.vs || {})){
1034
+ totals[key] = (totals[key] || 0) + value;
1035
+ }
1036
+ }
1037
+ return Object.keys(totals).length ? totals : undefined;
1038
+ }
1039
+ const VALUE_SCALES = {
1040
+ log: (value)=>Math.log1p(Math.max(value, 0)),
1041
+ sqrt: (value)=>Math.sqrt(Math.max(value, 0))
1042
+ };
949
1043
  function flattenNodes(nodes) {
950
1044
  const flat = nodes.slice();
951
1045
  for (const node of nodes){
@@ -955,22 +1049,33 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
955
1049
  }
956
1050
  return flat;
957
1051
  }
958
- function layoutLevel(nodes, rect, layout, parentSum) {
1052
+ function layoutLevel(nodes, rect, layout, weighted, parentSum) {
959
1053
  if (parentSum !== undefined) {
960
1054
  for (const node of nodes){
961
1055
  node.gs = parentSum;
962
1056
  }
963
1057
  }
964
- packInto(nodes, rect);
1058
+ packInto(nodes, rect, weighted);
965
1059
  for (const node of nodes){
966
1060
  if (node._children?.length) {
967
- layoutLevel(node._children, getSubRect(node, rect, layout), layout, node.s);
1061
+ layoutLevel(node._children, getSubRect(node, rect, layout), layout, weighted, node.s);
968
1062
  }
969
1063
  }
970
1064
  }
971
1065
  function layoutNodes(nodes, rect, layout) {
972
- layoutLevel(nodes, rect, layout);
1066
+ layoutLevel(nodes, rect, layout, isWeighted(layout));
973
1067
  applyHeaderBoxes(flattenNodes(nodes), layout);
1068
+ }
1069
+ function isWeighted(layout) {
1070
+ const scale = layout.valueScale;
1071
+ return scale !== 'linear' && scale !== undefined;
1072
+ }
1073
+ function scaleFor(layout) {
1074
+ const scale = layout.valueScale;
1075
+ if (typeof scale === 'function') {
1076
+ return scale;
1077
+ }
1078
+ return VALUE_SCALES[scale] || ((value)=>value);
974
1079
  }
975
1080
  function getSubRect(sq, rect, layout) {
976
1081
  const { borderWidth, captions, displayMode } = layout;
@@ -998,12 +1103,21 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
998
1103
  function buildNodes(tree, keys, layout) {
999
1104
  const { groups, leafKey, unsorted } = layout;
1000
1105
  const input = helpers.isObject(tree) ? normalizeTreeToArray(keys, leafKey, tree) : tree;
1106
+ const weighted = isWeighted(layout);
1107
+ let nodes;
1001
1108
  if (groups.length) {
1002
- return buildHierarchy(input, 0, keys, layout);
1003
- }
1004
- const nodes = toNodes(input || [], keys);
1005
- if (!unsorted) {
1006
- sortNodes(nodes);
1109
+ nodes = buildHierarchy(input, 0, keys, layout, weighted);
1110
+ } else {
1111
+ nodes = toNodes(input || [], keys);
1112
+ if (weighted) {
1113
+ applyValueScale(nodes, scaleFor(layout));
1114
+ }
1115
+ if (layout.others) {
1116
+ nodes = applyOthers(nodes, layout.others, weighted);
1117
+ }
1118
+ if (!unsorted) {
1119
+ sortNodes(nodes, weighted);
1120
+ }
1007
1121
  }
1008
1122
  return nodes;
1009
1123
  }
@@ -1027,6 +1141,9 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
1027
1141
  }
1028
1142
  }
1029
1143
 
1144
+ function clampFraction(value, fallback) {
1145
+ return value === undefined ? fallback : Math.min(Math.max(value, 0), 1);
1146
+ }
1030
1147
  function registerTooltipPositioner() {
1031
1148
  const tooltipPlugin = chart_js.registry.plugins.get('tooltip');
1032
1149
  if (!tooltipPlugin || tooltipPlugin.positioners.treemap) {
@@ -1061,12 +1178,9 @@ class TreemapController extends chart_js.DatasetController {
1061
1178
  const w = xScale.right - xScale.left;
1062
1179
  const h = yScale.bottom - yScale.top;
1063
1180
  const rect = {
1064
- h,
1181
+ ...this._regionBounds(w, h),
1065
1182
  rtl: !!this.options.rtl,
1066
- unsorted: !!this.options.unsorted,
1067
- w,
1068
- x: 0,
1069
- y: 0
1183
+ unsorted: !!this.options.unsorted
1070
1184
  };
1071
1185
  if (rectNotEqual(this._rect, rect)) {
1072
1186
  this._rect = rect;
@@ -1079,6 +1193,31 @@ class TreemapController extends chart_js.DatasetController {
1079
1193
  yScale.configure();
1080
1194
  }
1081
1195
  }
1196
+ _regionBounds(w, h) {
1197
+ const region = this.options.region;
1198
+ if (!region) {
1199
+ return {
1200
+ h,
1201
+ w,
1202
+ x: 0,
1203
+ y: 0
1204
+ };
1205
+ }
1206
+ const left = clampFraction(region.left, 0);
1207
+ const top = clampFraction(region.top, 0);
1208
+ const width = clampFraction(region.width, 1);
1209
+ const height = clampFraction(region.height, 1);
1210
+ if (!this._regionWarned && (left + width > 1 || top + height > 1)) {
1211
+ this._regionWarned = true;
1212
+ console.warn('chartjs-chart-treemap: a dataset region reaches outside the chart area and is clipped to it.');
1213
+ }
1214
+ return {
1215
+ h: h * Math.min(height, 1 - top),
1216
+ w: w * Math.min(width, 1 - left),
1217
+ x: w * left,
1218
+ y: h * top
1219
+ };
1220
+ }
1082
1221
  _layoutOptions() {
1083
1222
  const dataset = this.getDataset();
1084
1223
  const options = this.options;
@@ -1088,8 +1227,10 @@ class TreemapController extends chart_js.DatasetController {
1088
1227
  displayMode: options.displayMode,
1089
1228
  groups: options.groups || [],
1090
1229
  leafKey: options.leafKey,
1230
+ others: options.others || false,
1091
1231
  spacing: helpers.valueOrDefault(dataset.spacing, 0),
1092
- unsorted: !!options.unsorted
1232
+ unsorted: !!options.unsorted,
1233
+ valueScale: options.valueScale || 'linear'
1093
1234
  };
1094
1235
  }
1095
1236
  buildOrUpdateElements(_resetNewElements) {
@@ -1144,8 +1285,10 @@ class TreemapController extends chart_js.DatasetController {
1144
1285
  }
1145
1286
  const dataset = this.getDataset();
1146
1287
  const label = node.g ?? node._data?.label ?? dataset.label;
1288
+ const text = label === undefined ? '' : `${label}`;
1289
+ const others = node.isOthers ? node._data?.others?.length : undefined;
1147
1290
  return {
1148
- label: label === undefined ? '' : `${label}`,
1291
+ label: others ? `${text} (${others} items)` : text,
1149
1292
  value: `${node.v}`
1150
1293
  };
1151
1294
  }
@@ -1207,6 +1350,7 @@ class TreemapController extends chart_js.DatasetController {
1207
1350
  super(chart, datasetIndex);
1208
1351
  this._rect = undefined;
1209
1352
  this._rectChanged = true;
1353
+ this._regionWarned = false;
1210
1354
  this._nodes = [];
1211
1355
  this._layoutDirty = true;
1212
1356
  this._fingerprint = undefined;
@@ -1231,10 +1375,13 @@ TreemapController.defaults = {
1231
1375
  groups: [],
1232
1376
  key: '',
1233
1377
  leafKey: '_leaf',
1378
+ others: false,
1379
+ region: undefined,
1234
1380
  rtl: layoutDefaults.rtl,
1235
1381
  spacing: layoutDefaults.spacing,
1236
1382
  sumKeys: [],
1237
- unsorted: false
1383
+ unsorted: false,
1384
+ valueScale: 'linear'
1238
1385
  };
1239
1386
  TreemapController.descriptors = {
1240
1387
  _indexable: false,