chartjs-chart-treemap 5.0.0-next.7 → 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.7
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.7";
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){
@@ -486,7 +496,272 @@ function compareAspectRatio(oldStat, newStat, args) {
486
496
  }
487
497
  }
488
498
 
499
+ const WIDTH_CACHE_LIMIT = 4096;
489
500
  const widthCache = new Map();
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);
530
+ }
531
+ return size;
532
+ }
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
+ };
549
+ }
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
+ });
558
+ }
559
+
560
+ function clipToPadding(ctx, rect, padding, clipY) {
561
+ const x = Math.max(0, padding);
562
+ const y = Math.max(0, clipY);
563
+ ctx.beginPath();
564
+ ctx.rect(rect.x + x, rect.y + y, Math.max(0, rect.w - 2 * x), Math.max(0, rect.h - 2 * y));
565
+ ctx.clip();
566
+ }
567
+ function drawTextBlock(ctx, rect, block) {
568
+ const { align, colors, fonts, lines, padding, styles, textAlign, top, width } = block;
569
+ const lineAlign = textAlign || align;
570
+ const x = calculateLineX(rect, align, lineAlign, padding, width);
571
+ ctx.textAlign = lineAlign;
572
+ ctx.textBaseline = 'middle';
573
+ let offset = 0;
574
+ lines.forEach((line, i)=>{
575
+ const style = styles[i];
576
+ const font = fontFor(fonts, style);
577
+ ctx.font = font.string;
578
+ ctx.fillStyle = colors[Math.min(style, colors.length - 1)];
579
+ ctx.fillText(line, x, top + offset + font.lineHeight / 2);
580
+ offset += font.lineHeight;
581
+ });
582
+ }
583
+ function calculateBlockTop(rect, options, labelSize) {
584
+ const { position, padding } = options;
585
+ if (position === 'top') {
586
+ return rect.y + padding;
587
+ }
588
+ if (position === 'bottom') {
589
+ return rect.y + rect.h - padding - labelSize.height;
590
+ }
591
+ return rect.y + (rect.h - labelSize.height) / 2 + padding;
592
+ }
593
+ function calculateBlockLeft(rect, align, padding, width) {
594
+ if (align === 'left') {
595
+ return rect.x + padding;
596
+ }
597
+ if (align === 'right') {
598
+ return rect.x + rect.w - padding - width;
599
+ }
600
+ return rect.x + (rect.w - width) / 2;
601
+ }
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;
609
+ }
610
+ return rect.x + rect.w / 2;
611
+ }
612
+ const left = calculateBlockLeft(rect, align, padding, width);
613
+ if (lineAlign === 'left') {
614
+ return left;
615
+ }
616
+ if (lineAlign === 'right') {
617
+ return left + width;
618
+ }
619
+ return left + width / 2;
620
+ }
621
+
622
+ function sliceTextToFitWidth(ctx, text, width, fonts) {
623
+ const ellipsis = '...';
624
+ const ellipsisWidth = measureLabelSize(ctx, [
625
+ ellipsis
626
+ ], fonts).width;
627
+ if (ellipsisWidth >= width) {
628
+ return '';
629
+ }
630
+ let lowerBoundLen = 1;
631
+ let upperBoundLen = text.length;
632
+ let currentWidth;
633
+ while(lowerBoundLen <= upperBoundLen){
634
+ const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
635
+ const currentText = text.slice(0, currentLen);
636
+ currentWidth = measureLabelSize(ctx, [
637
+ currentText
638
+ ], fonts).width;
639
+ if (currentWidth + ellipsisWidth > width) {
640
+ upperBoundLen = currentLen - 1;
641
+ } else {
642
+ lowerBoundLen = currentLen + 1;
643
+ }
644
+ }
645
+ const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
646
+ return slicedText ? slicedText + ellipsis : '';
647
+ }
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]);
660
+ }
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, [
702
+ value
703
+ ], [
704
+ font
705
+ ]).width <= width;
706
+ const lines = [];
707
+ let line = '';
708
+ const pushWord = (word)=>{
709
+ if (fits(word)) {
710
+ line = word;
711
+ return;
712
+ }
713
+ let chunk = '';
714
+ for (const char of word){
715
+ if (chunk && !fits(chunk + char)) {
716
+ lines.push(chunk);
717
+ chunk = '';
718
+ }
719
+ chunk += char;
720
+ }
721
+ line = chunk;
722
+ };
723
+ for (const word of text.split(' ')){
724
+ const candidate = line ? `${line} ${word}` : word;
725
+ if (fits(candidate)) {
726
+ line = candidate;
727
+ continue;
728
+ }
729
+ if (line) {
730
+ lines.push(line);
731
+ line = '';
732
+ }
733
+ pushWord(word);
734
+ }
735
+ if (line) {
736
+ lines.push(line);
737
+ }
738
+ return lines.length ? lines : [
739
+ ''
740
+ ];
741
+ }
742
+ function toLines(ctx, content, fonts, width, wrap) {
743
+ const paragraphs = (helpers.isArray(content) ? content : [
744
+ content
745
+ ]).map((entry)=>`${entry}`);
746
+ const lines = [];
747
+ const styles = [];
748
+ paragraphs.forEach((paragraph, index)=>{
749
+ const font = fontFor(fonts, index);
750
+ for (const piece of paragraph.split('\n')){
751
+ for (const line of wrap ? wrapToWidth(ctx, piece, font, width) : [
752
+ piece
753
+ ]){
754
+ lines.push(line);
755
+ styles.push(index);
756
+ }
757
+ }
758
+ });
759
+ return {
760
+ lines,
761
+ styles
762
+ };
763
+ }
764
+
490
765
  function shouldDrawCaption(displayMode, rect, options) {
491
766
  if (!options || options.display === false) {
492
767
  return false;
@@ -526,28 +801,6 @@ function drawText(ctx, rect, options, item, element, layout) {
526
801
  clipToPadding(ctx, rect, block.padding, block.clipY);
527
802
  drawTextBlock(ctx, rect, block);
528
803
  ctx.restore();
529
- }
530
- function clipToPadding(ctx, rect, padding, clipY) {
531
- const x = Math.max(0, padding);
532
- const y = Math.max(0, clipY);
533
- ctx.beginPath();
534
- ctx.rect(rect.x + x, rect.y + y, Math.max(0, rect.w - 2 * x), Math.max(0, rect.h - 2 * y));
535
- ctx.clip();
536
- }
537
- function drawTextBlock(ctx, rect, block) {
538
- const { align, colors, fonts, lines, padding, textAlign, top, width } = block;
539
- const lineAlign = textAlign || align;
540
- const x = calculateLineX(rect, align, lineAlign, padding, width);
541
- ctx.textAlign = lineAlign;
542
- ctx.textBaseline = 'middle';
543
- let offset = 0;
544
- lines.forEach((line, i)=>{
545
- const font = fonts[Math.min(i, fonts.length - 1)];
546
- ctx.font = font.string;
547
- ctx.fillStyle = colors[Math.min(i, colors.length - 1)];
548
- ctx.fillText(line, x, top + offset + font.lineHeight / 2);
549
- offset += font.lineHeight;
550
- });
551
804
  }
552
805
  function callbackContext(element, item) {
553
806
  const context = element.$context || {
@@ -573,10 +826,17 @@ function resolveCaptionText(captions, element, item) {
573
826
  const padding = captions.padding;
574
827
  if (measureLabelSize(ctx, [
575
828
  text
576
- ], fonts).width + 2 * padding > rect.w) {
577
- return sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
829
+ ], fonts).width + 2 * padding <= rect.w) {
830
+ return text;
578
831
  }
579
- return text;
832
+ const overflow = captions.overflow || 'ellipsis';
833
+ if (overflow === 'hidden') {
834
+ return undefined;
835
+ }
836
+ if (overflow === 'cut') {
837
+ return text;
838
+ }
839
+ return sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
580
840
  }
581
841
  function captionBlock(ctx, rect, options, item, element, layout) {
582
842
  const { displayMode, rtl, spacing } = layout;
@@ -608,6 +868,9 @@ function captionBlock(ctx, rect, options, item, element, layout) {
608
868
  text
609
869
  ],
610
870
  padding,
871
+ styles: [
872
+ 0
873
+ ],
611
874
  textAlign: captions.textAlign,
612
875
  top,
613
876
  width: measureLabelSize(ctx, [
@@ -621,23 +884,36 @@ function labelBlock(ctx, rect, options, item, element) {
621
884
  if (!content) {
622
885
  return;
623
886
  }
624
- const lines = helpers.isArray(content) ? content : [
625
- content
626
- ];
887
+ const { align, color, hoverColor, overflow, padding, wrap } = labels;
888
+ const maxWidth = rect.w - padding * 2;
889
+ const maxHeight = rect.h - padding * 2;
627
890
  let fonts = getFontFromOptions(rect, labels);
628
- let labelSize = measureLabelSize(ctx, lines, fonts);
891
+ let { lines, styles } = toLines(ctx, content, fonts, maxWidth, !!wrap);
892
+ let labelSize = measureLines(ctx, lines, styles, fonts);
629
893
  const lblToDraw = labelToDraw(rect, labels, labelSize);
630
894
  if (!lblToDraw) {
631
895
  return;
632
896
  }
633
897
  if (helpers.isNumber(lblToDraw)) {
634
- labelSize = {
635
- height: labelSize.height * lblToDraw,
636
- width: labelSize.width * lblToDraw
637
- };
638
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
+ };
909
+ }
910
+ }
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);
639
916
  }
640
- const { color, hoverColor, align, padding } = labels;
641
917
  const optColor = (rect.active ? hoverColor : color) || color;
642
918
  return {
643
919
  align,
@@ -648,87 +924,12 @@ function labelBlock(ctx, rect, options, item, element) {
648
924
  fonts,
649
925
  lines,
650
926
  padding,
927
+ styles,
651
928
  textAlign: labels.textAlign,
652
929
  top: calculateBlockTop(rect, labels, labelSize),
653
930
  width: labelSize.width
654
931
  };
655
932
  }
656
- function sliceTextToFitWidth(ctx, text, width, fonts) {
657
- const ellipsis = '...';
658
- const ellipsisWidth = measureLabelSize(ctx, [
659
- ellipsis
660
- ], fonts).width;
661
- if (ellipsisWidth >= width) {
662
- return '';
663
- }
664
- let lowerBoundLen = 1;
665
- let upperBoundLen = text.length;
666
- let currentWidth;
667
- while(lowerBoundLen <= upperBoundLen){
668
- const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
669
- const currentText = text.slice(0, currentLen);
670
- currentWidth = measureLabelSize(ctx, [
671
- currentText
672
- ], fonts).width;
673
- if (currentWidth + ellipsisWidth > width) {
674
- upperBoundLen = currentLen - 1;
675
- } else {
676
- lowerBoundLen = currentLen + 1;
677
- }
678
- }
679
- const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
680
- return slicedText ? slicedText + ellipsis : '';
681
- }
682
- function measureLabelSize(ctx, lines, fonts) {
683
- const fontsKey = fonts.reduce((prev, item)=>{
684
- prev += item.string;
685
- return prev;
686
- }, '');
687
- const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
688
- let size = widthCache.get(mapKey);
689
- if (!size) {
690
- ctx.save();
691
- const count = lines.length;
692
- let width = 0;
693
- let height = 0;
694
- for(let i = 0; i < count; i++){
695
- const font = fonts[Math.min(i, fonts.length - 1)];
696
- ctx.font = font.string;
697
- const text = lines[i];
698
- width = Math.max(width, ctx.measureText(text).width);
699
- height += font.lineHeight;
700
- }
701
- ctx.restore();
702
- size = {
703
- height,
704
- width
705
- };
706
- widthCache.set(mapKey, size);
707
- }
708
- return size;
709
- }
710
- function toFonts(fonts, fitRatio) {
711
- return fonts.map((f)=>{
712
- const { lineHeight: _lineHeight, ...font } = f;
713
- return helpers.toFont({
714
- ...font,
715
- size: Math.floor(f.size * fitRatio)
716
- });
717
- });
718
- }
719
- function labelToDraw(rect, options, labelSize) {
720
- const { overflow, padding } = options;
721
- const { width, height } = labelSize;
722
- if (overflow === 'hidden') {
723
- return !(width + padding * 2 > rect.w || height + padding * 2 > rect.h);
724
- } else if (overflow === 'fit') {
725
- const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
726
- if (ratio < 1) {
727
- return ratio;
728
- }
729
- }
730
- return true;
731
- }
732
933
  function getFontFromOptions(rect, labels) {
733
934
  const { font, hoverFont } = labels;
734
935
  const optFont = (rect.active ? hoverFont : font) || font;
@@ -736,58 +937,17 @@ function getFontFromOptions(rect, labels) {
736
937
  helpers.toFont(optFont)
737
938
  ];
738
939
  }
739
- function calculateBlockTop(rect, options, labelSize) {
740
- const { position, padding } = options;
741
- if (position === 'top') {
742
- return rect.y + padding;
743
- }
744
- if (position === 'bottom') {
745
- return rect.y + rect.h - padding - labelSize.height;
746
- }
747
- return rect.y + (rect.h - labelSize.height) / 2 + padding;
748
- }
749
- function calculateBlockLeft(rect, align, padding, width) {
750
- if (align === 'left') {
751
- return rect.x + padding;
752
- }
753
- if (align === 'right') {
754
- return rect.x + rect.w - padding - width;
755
- }
756
- return rect.x + (rect.w - width) / 2;
757
- }
758
- function calculateLineX(rect, align, lineAlign, padding, width) {
759
- if (align === lineAlign) {
760
- if (align === 'left') {
761
- return rect.x + padding;
762
- }
763
- if (align === 'right') {
764
- return rect.x + rect.w - padding;
765
- }
766
- return rect.x + rect.w / 2;
767
- }
768
- const left = calculateBlockLeft(rect, align, padding, width);
769
- if (lineAlign === 'left') {
770
- return left;
771
- }
772
- if (lineAlign === 'right') {
773
- return left + width;
774
- }
775
- return left + width / 2;
776
- }
777
940
 
778
- function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
941
+ function buildHierarchy(values, gidx, keys, layout, weighted, parentGroupValue) {
779
942
  const { groups, leafKey, unsorted } = layout;
780
943
  const glen = groups.length;
781
944
  const g = getGroupKey(groups[gidx]);
782
945
  const gdata = group(values, g, keys, leafKey, undefined, parentGroupValue, groups.filter((_item, index)=>index <= gidx), groups, gidx);
783
946
  const nodes = toNodes(gdata, keys, g, gidx);
784
- if (!unsorted) {
785
- sortNodes(nodes);
786
- }
787
947
  for (const node of nodes){
788
948
  const record = node._data;
789
949
  const nextGroupIndex = record.groupIndex + 1;
790
- 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) : [];
791
951
  Object.defineProperty(node, '_children', {
792
952
  configurable: true,
793
953
  value: children,
@@ -795,8 +955,91 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
795
955
  });
796
956
  node.isLeaf = !children.length;
797
957
  }
798
- 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;
799
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;
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
+ };
800
1043
  function flattenNodes(nodes) {
801
1044
  const flat = nodes.slice();
802
1045
  for (const node of nodes){
@@ -806,22 +1049,33 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
806
1049
  }
807
1050
  return flat;
808
1051
  }
809
- function layoutLevel(nodes, rect, layout, parentSum) {
1052
+ function layoutLevel(nodes, rect, layout, weighted, parentSum) {
810
1053
  if (parentSum !== undefined) {
811
1054
  for (const node of nodes){
812
1055
  node.gs = parentSum;
813
1056
  }
814
1057
  }
815
- packInto(nodes, rect);
1058
+ packInto(nodes, rect, weighted);
816
1059
  for (const node of nodes){
817
1060
  if (node._children?.length) {
818
- layoutLevel(node._children, getSubRect(node, rect, layout), layout, node.s);
1061
+ layoutLevel(node._children, getSubRect(node, rect, layout), layout, weighted, node.s);
819
1062
  }
820
1063
  }
821
1064
  }
822
1065
  function layoutNodes(nodes, rect, layout) {
823
- layoutLevel(nodes, rect, layout);
1066
+ layoutLevel(nodes, rect, layout, isWeighted(layout));
824
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);
825
1079
  }
826
1080
  function getSubRect(sq, rect, layout) {
827
1081
  const { borderWidth, captions, displayMode } = layout;
@@ -849,12 +1103,21 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
849
1103
  function buildNodes(tree, keys, layout) {
850
1104
  const { groups, leafKey, unsorted } = layout;
851
1105
  const input = helpers.isObject(tree) ? normalizeTreeToArray(keys, leafKey, tree) : tree;
1106
+ const weighted = isWeighted(layout);
1107
+ let nodes;
852
1108
  if (groups.length) {
853
- return buildHierarchy(input, 0, keys, layout);
854
- }
855
- const nodes = toNodes(input || [], keys);
856
- if (!unsorted) {
857
- 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
+ }
858
1121
  }
859
1122
  return nodes;
860
1123
  }
@@ -878,6 +1141,9 @@ function buildHierarchy(values, gidx, keys, layout, parentGroupValue) {
878
1141
  }
879
1142
  }
880
1143
 
1144
+ function clampFraction(value, fallback) {
1145
+ return value === undefined ? fallback : Math.min(Math.max(value, 0), 1);
1146
+ }
881
1147
  function registerTooltipPositioner() {
882
1148
  const tooltipPlugin = chart_js.registry.plugins.get('tooltip');
883
1149
  if (!tooltipPlugin || tooltipPlugin.positioners.treemap) {
@@ -912,12 +1178,9 @@ class TreemapController extends chart_js.DatasetController {
912
1178
  const w = xScale.right - xScale.left;
913
1179
  const h = yScale.bottom - yScale.top;
914
1180
  const rect = {
915
- h,
1181
+ ...this._regionBounds(w, h),
916
1182
  rtl: !!this.options.rtl,
917
- unsorted: !!this.options.unsorted,
918
- w,
919
- x: 0,
920
- y: 0
1183
+ unsorted: !!this.options.unsorted
921
1184
  };
922
1185
  if (rectNotEqual(this._rect, rect)) {
923
1186
  this._rect = rect;
@@ -930,6 +1193,31 @@ class TreemapController extends chart_js.DatasetController {
930
1193
  yScale.configure();
931
1194
  }
932
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
+ }
933
1221
  _layoutOptions() {
934
1222
  const dataset = this.getDataset();
935
1223
  const options = this.options;
@@ -939,8 +1227,10 @@ class TreemapController extends chart_js.DatasetController {
939
1227
  displayMode: options.displayMode,
940
1228
  groups: options.groups || [],
941
1229
  leafKey: options.leafKey,
1230
+ others: options.others || false,
942
1231
  spacing: helpers.valueOrDefault(dataset.spacing, 0),
943
- unsorted: !!options.unsorted
1232
+ unsorted: !!options.unsorted,
1233
+ valueScale: options.valueScale || 'linear'
944
1234
  };
945
1235
  }
946
1236
  buildOrUpdateElements(_resetNewElements) {
@@ -995,8 +1285,10 @@ class TreemapController extends chart_js.DatasetController {
995
1285
  }
996
1286
  const dataset = this.getDataset();
997
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;
998
1290
  return {
999
- label: label === undefined ? '' : `${label}`,
1291
+ label: others ? `${text} (${others} items)` : text,
1000
1292
  value: `${node.v}`
1001
1293
  };
1002
1294
  }
@@ -1058,6 +1350,7 @@ class TreemapController extends chart_js.DatasetController {
1058
1350
  super(chart, datasetIndex);
1059
1351
  this._rect = undefined;
1060
1352
  this._rectChanged = true;
1353
+ this._regionWarned = false;
1061
1354
  this._nodes = [];
1062
1355
  this._layoutDirty = true;
1063
1356
  this._fingerprint = undefined;
@@ -1082,10 +1375,13 @@ TreemapController.defaults = {
1082
1375
  groups: [],
1083
1376
  key: '',
1084
1377
  leafKey: '_leaf',
1378
+ others: false,
1379
+ region: undefined,
1085
1380
  rtl: layoutDefaults.rtl,
1086
1381
  spacing: layoutDefaults.spacing,
1087
1382
  sumKeys: [],
1088
- unsorted: false
1383
+ unsorted: false,
1384
+ valueScale: 'linear'
1089
1385
  };
1090
1386
  TreemapController.descriptors = {
1091
1387
  _indexable: false,
@@ -1283,6 +1579,7 @@ TreemapElement.defaults = {
1283
1579
  const label = ctx.raw._data?.label;
1284
1580
  return ctx.raw.g || (typeof label === 'string' ? label : '');
1285
1581
  },
1582
+ overflow: 'ellipsis',
1286
1583
  padding: 3,
1287
1584
  textAlign: undefined
1288
1585
  },
@@ -1307,7 +1604,8 @@ TreemapElement.defaults = {
1307
1604
  overflow: 'cut',
1308
1605
  padding: 3,
1309
1606
  position: 'middle',
1310
- textAlign: undefined
1607
+ textAlign: undefined,
1608
+ wrap: false
1311
1609
  }
1312
1610
  };
1313
1611
  TreemapElement.descriptors = {