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.
- package/dist/chartjs-chart-treemap.cjs +476 -178
- package/dist/chartjs-chart-treemap.esm.js +477 -179
- package/dist/chartjs-chart-treemap.min.js +2 -2
- package/dist/chartjs-chart-treemap.min.js.map +1 -1
- package/dist/controller.d.ts +14 -0
- package/dist/element.d.ts +2 -0
- package/dist/layout.d.ts +3 -1
- package/dist/squarify.d.ts +7 -2
- package/dist/text/draw.d.ts +30 -0
- package/dist/text/fit.d.ts +9 -0
- package/dist/{text.d.ts → text/index.d.ts} +2 -2
- package/dist/text/measure.d.ts +26 -0
- package/dist/text/wrap.d.ts +6 -0
- package/dist/types.d.ts +32 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-treemap v5.0.0-next.
|
|
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.
|
|
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(
|
|
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
|
|
577
|
-
return
|
|
829
|
+
], fonts).width + 2 * padding <= rect.w) {
|
|
830
|
+
return text;
|
|
578
831
|
}
|
|
579
|
-
|
|
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
|
|
625
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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:
|
|
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 = {
|