chartjs-chart-treemap 4.0.0 → 4.1.1

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.
@@ -4,445 +4,11 @@
4
4
  * (c) 2026 Jukka Kurkela
5
5
  * Released under the MIT license
6
6
  */
7
- import { Element, DatasetController, Chart, registry } from 'chart.js';
8
- import { addRoundedRectPath, defined, toTRBL, toTRBLCorners, isArray, isNumber, toFont, valueOrDefault, isObject, clipArea, unclipArea } from 'chart.js/helpers';
7
+ import { DatasetController, Chart, registry, Element } from 'chart.js';
8
+ import { toTRBL, toTRBLCorners, isObject, defined, toFont, valueOrDefault, isArray, isNumber, clipArea, unclipArea, addRoundedRectPath } from 'chart.js/helpers';
9
9
 
10
10
  var version = "0.0.0-development";
11
11
 
12
- const widthCache = new Map();
13
- function getBounds(rect, useFinalPosition) {
14
- const { x, y, width, height } = rect.getProps([
15
- 'x',
16
- 'y',
17
- 'width',
18
- 'height'
19
- ], useFinalPosition);
20
- return {
21
- bottom: y + height,
22
- left: x,
23
- right: x + width,
24
- top: y
25
- };
26
- }
27
- function limit(value, min, max) {
28
- return Math.max(Math.min(value, max), min);
29
- }
30
- function parseBorderWidth(value, maxW, maxH) {
31
- const o = toTRBL(value);
32
- return {
33
- b: limit(o.bottom, 0, maxH),
34
- l: limit(o.left, 0, maxW),
35
- r: limit(o.right, 0, maxW),
36
- t: limit(o.top, 0, maxH)
37
- };
38
- }
39
- function parseBorderRadius(value, maxW, maxH) {
40
- const o = toTRBLCorners(value);
41
- const maxR = Math.min(maxW, maxH);
42
- return {
43
- bottomLeft: limit(o.bottomLeft, 0, maxR),
44
- bottomRight: limit(o.bottomRight, 0, maxR),
45
- topLeft: limit(o.topLeft, 0, maxR),
46
- topRight: limit(o.topRight, 0, maxR)
47
- };
48
- }
49
- function boundingRects(rect) {
50
- const bounds = getBounds(rect);
51
- const width = bounds.right - bounds.left;
52
- const height = bounds.bottom - bounds.top;
53
- const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
54
- const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2);
55
- const outer = {
56
- active: rect.active,
57
- h: height,
58
- radius,
59
- w: width,
60
- x: bounds.left,
61
- y: bounds.top
62
- };
63
- return {
64
- inner: {
65
- active: rect.active,
66
- h: outer.h - border.t - border.b,
67
- radius: {
68
- bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
69
- bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
70
- topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
71
- topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r))
72
- },
73
- w: outer.w - border.l - border.r,
74
- x: outer.x + border.l,
75
- y: outer.y + border.t
76
- },
77
- outer
78
- };
79
- }
80
- function inRange(rect, x, y, useFinalPosition) {
81
- const skipX = x === null;
82
- const skipY = y === null;
83
- const bounds = !rect || skipX && skipY ? false : getBounds(rect, useFinalPosition);
84
- return bounds && (skipX || x >= bounds.left && x <= bounds.right) && (skipY || y >= bounds.top && y <= bounds.bottom);
85
- }
86
- function hasRadius(radius) {
87
- return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
88
- }
89
- function addNormalRectPath(ctx, rect) {
90
- ctx.rect(rect.x, rect.y, rect.w, rect.h);
91
- }
92
- function shouldDrawCaption(displayMode, rect, options) {
93
- if (!options || options.display === false) {
94
- return false;
95
- }
96
- if (displayMode === 'headerBoxes') {
97
- return true;
98
- }
99
- const { w, h } = rect;
100
- const font = toFont(options.font);
101
- const min = font.lineHeight;
102
- const padding = limit(valueOrDefault(options.padding, 3) * 2, 0, Math.min(w, h));
103
- return w - padding > min && h - padding > min;
104
- }
105
- function getCaptionHeight(displayMode, rect, font, padding) {
106
- if (displayMode !== 'headerBoxes') {
107
- return font.lineHeight + padding * 2;
108
- }
109
- const captionHeight = font.lineHeight + padding * 2;
110
- return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight;
111
- }
112
- function drawText(ctx, rect, options, item) {
113
- const { captions, labels, displayMode } = options;
114
- ctx.save();
115
- ctx.beginPath();
116
- ctx.rect(rect.x, rect.y, rect.w, rect.h);
117
- ctx.clip();
118
- const isLeaf = item && (!defined(item.l) || item.isLeaf);
119
- if (isLeaf && labels.display) {
120
- drawLabel(ctx, rect, options);
121
- } else if (!isLeaf && shouldDrawCaption(displayMode, rect, captions)) {
122
- drawCaption(ctx, rect, options, item);
123
- }
124
- ctx.restore();
125
- }
126
- function drawCaption(ctx, rect, options, item) {
127
- const { captions, spacing, rtl, displayMode } = options;
128
- const { color, hoverColor, font, hoverFont, padding, align, formatter } = captions;
129
- const oColor = (rect.active ? hoverColor : color) || color;
130
- const oAlign = align || (rtl ? 'right' : 'left');
131
- const optFont = (rect.active ? hoverFont : font) || font;
132
- const oFont = toFont(optFont);
133
- const fonts = [
134
- oFont
135
- ];
136
- if (oFont.lineHeight > rect.h) {
137
- return;
138
- }
139
- let text = formatter || item.g;
140
- const captionSize = measureLabelSize(ctx, [
141
- formatter
142
- ], fonts);
143
- if (captionSize.width + 2 * padding > rect.w) {
144
- text = sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
145
- }
146
- const lh = oFont.lineHeight / 2;
147
- const x = calculateX(rect, oAlign, padding);
148
- ctx.fillStyle = oColor;
149
- ctx.font = oFont.string;
150
- ctx.textAlign = oAlign;
151
- ctx.textBaseline = 'middle';
152
- const y = displayMode === 'headerBoxes' ? rect.y + rect.h / 2 : rect.y + padding + spacing + lh;
153
- ctx.fillText(text, x, y);
154
- }
155
- function sliceTextToFitWidth(ctx, text, width, fonts) {
156
- const ellipsis = '...';
157
- const ellipsisWidth = measureLabelSize(ctx, [
158
- ellipsis
159
- ], fonts).width;
160
- if (ellipsisWidth >= width) {
161
- return '';
162
- }
163
- let lowerBoundLen = 1;
164
- let upperBoundLen = text.length;
165
- let currentWidth;
166
- while(lowerBoundLen <= upperBoundLen){
167
- const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
168
- const currentText = text.slice(0, currentLen);
169
- currentWidth = measureLabelSize(ctx, [
170
- currentText
171
- ], fonts).width;
172
- if (currentWidth + ellipsisWidth > width) {
173
- upperBoundLen = currentLen - 1;
174
- } else {
175
- lowerBoundLen = currentLen + 1;
176
- }
177
- }
178
- const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
179
- return slicedText ? slicedText + ellipsis : '';
180
- }
181
- function measureLabelSize(ctx, lines, fonts) {
182
- const fontsKey = fonts.reduce((prev, item)=>{
183
- prev += item.string;
184
- return prev;
185
- }, '');
186
- const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
187
- if (!widthCache.has(mapKey)) {
188
- ctx.save();
189
- const count = lines.length;
190
- let width = 0;
191
- let height = 0;
192
- for(let i = 0; i < count; i++){
193
- const font = fonts[Math.min(i, fonts.length - 1)];
194
- ctx.font = font.string;
195
- const text = lines[i];
196
- width = Math.max(width, ctx.measureText(text).width);
197
- height += font.lineHeight;
198
- }
199
- ctx.restore();
200
- widthCache.set(mapKey, {
201
- height,
202
- width
203
- });
204
- }
205
- return widthCache.get(mapKey);
206
- }
207
- function toFonts(fonts, fitRatio) {
208
- return fonts.map((f)=>{
209
- f.size = Math.floor(f.size * fitRatio);
210
- f.lineHeight = undefined;
211
- return toFont(f);
212
- });
213
- }
214
- function labelToDraw(_ctx, rect, options, labelSize) {
215
- const { overflow, padding } = options;
216
- const { width, height } = labelSize;
217
- if (overflow === 'hidden') {
218
- return !(width + padding * 2 > rect.w || height + padding * 2 > rect.h);
219
- } else if (overflow === 'fit') {
220
- const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
221
- if (ratio < 1) {
222
- return ratio;
223
- }
224
- }
225
- return true;
226
- }
227
- function getFontFromOptions(rect, labels) {
228
- const { font, hoverFont } = labels;
229
- const optFont = (rect.active ? hoverFont : font) || font;
230
- return Array.isArray(optFont) ? optFont.map((f)=>toFont(f)) : [
231
- toFont(optFont)
232
- ];
233
- }
234
- function drawLabel(ctx, rect, options) {
235
- const labels = options.labels;
236
- const content = labels.formatter;
237
- if (!content) {
238
- return;
239
- }
240
- const contents = isArray(content) ? content : [
241
- content
242
- ];
243
- let fonts = getFontFromOptions(rect, labels);
244
- let labelSize = measureLabelSize(ctx, contents, fonts);
245
- const lblToDraw = labelToDraw(ctx, rect, labels, labelSize);
246
- if (!lblToDraw) {
247
- return;
248
- }
249
- if (isNumber(lblToDraw)) {
250
- labelSize = {
251
- height: labelSize.height * lblToDraw,
252
- width: labelSize.width * lblToDraw
253
- };
254
- fonts = toFonts(fonts, lblToDraw);
255
- }
256
- const { color, hoverColor, align } = labels;
257
- const optColor = (rect.active ? hoverColor : color) || color;
258
- const colors = isArray(optColor) ? optColor : [
259
- optColor
260
- ];
261
- const xyPoint = calculateXYLabel(rect, labels, labelSize);
262
- ctx.textAlign = align;
263
- ctx.textBaseline = 'middle';
264
- let lhs = 0;
265
- contents.forEach((l, i)=>{
266
- const c = colors[Math.min(i, colors.length - 1)];
267
- const f = fonts[Math.min(i, fonts.length - 1)];
268
- const lh = f.lineHeight;
269
- ctx.font = f.string;
270
- ctx.fillStyle = c;
271
- ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs);
272
- lhs += lh;
273
- });
274
- }
275
- function drawDivider(ctx, rect, options, item) {
276
- const dividers = options.dividers;
277
- if (!dividers.display || !item._data.children.length) {
278
- return;
279
- }
280
- const { x, y, w, h } = rect;
281
- const { lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth } = dividers;
282
- ctx.save();
283
- ctx.strokeStyle = lineColor;
284
- ctx.lineCap = lineCapStyle;
285
- ctx.setLineDash(lineDash);
286
- ctx.lineDashOffset = lineDashOffset;
287
- ctx.lineWidth = lineWidth;
288
- ctx.beginPath();
289
- if (w > h) {
290
- const w2 = w / 2;
291
- ctx.moveTo(x + w2, y);
292
- ctx.lineTo(x + w2, y + h);
293
- } else {
294
- const h2 = h / 2;
295
- ctx.moveTo(x, y + h2);
296
- ctx.lineTo(x + w, y + h2);
297
- }
298
- ctx.stroke();
299
- ctx.restore();
300
- }
301
- function calculateXYLabel(rect, options, labelSize) {
302
- const { align, position, padding } = options;
303
- const x = calculateX(rect, align, padding);
304
- let y;
305
- if (position === 'top') {
306
- y = rect.y + padding;
307
- } else if (position === 'bottom') {
308
- y = rect.y + rect.h - padding - labelSize.height;
309
- } else {
310
- y = rect.y + (rect.h - labelSize.height) / 2 + padding;
311
- }
312
- return {
313
- x,
314
- y
315
- };
316
- }
317
- function calculateX(rect, align, padding) {
318
- if (align === 'left') {
319
- return rect.x + padding;
320
- } else if (align === 'right') {
321
- return rect.x + rect.w - padding;
322
- }
323
- return rect.x + rect.w / 2;
324
- }
325
- class TreemapElement extends Element {
326
- draw(ctx, data) {
327
- if (!data) {
328
- return;
329
- }
330
- const options = this.options;
331
- const { inner, outer } = boundingRects(this);
332
- const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
333
- ctx.save();
334
- if (outer.w !== inner.w || outer.h !== inner.h) {
335
- ctx.beginPath();
336
- addRectPath(ctx, outer);
337
- ctx.clip();
338
- addRectPath(ctx, inner);
339
- ctx.fillStyle = options.borderColor;
340
- ctx.fill('evenodd');
341
- }
342
- ctx.beginPath();
343
- addRectPath(ctx, inner);
344
- ctx.fillStyle = options.backgroundColor;
345
- ctx.fill();
346
- drawDivider(ctx, inner, options, data);
347
- drawText(ctx, inner, options, data);
348
- ctx.restore();
349
- }
350
- inRange(mouseX, mouseY, useFinalPosition) {
351
- return inRange(this, mouseX, mouseY, useFinalPosition);
352
- }
353
- inXRange(mouseX, useFinalPosition) {
354
- return inRange(this, mouseX, null, useFinalPosition);
355
- }
356
- inYRange(mouseY, useFinalPosition) {
357
- return inRange(this, null, mouseY, useFinalPosition);
358
- }
359
- getCenterPoint(useFinalPosition) {
360
- const { x, y, width, height } = this.getProps([
361
- 'x',
362
- 'y',
363
- 'width',
364
- 'height'
365
- ], useFinalPosition);
366
- return {
367
- x: x + width / 2,
368
- y: y + height / 2
369
- };
370
- }
371
- tooltipPosition() {
372
- return this.getCenterPoint();
373
- }
374
- constructor(cfg){
375
- super();
376
- this.options = undefined;
377
- this.width = undefined;
378
- this.height = undefined;
379
- if (cfg) {
380
- Object.assign(this, cfg);
381
- }
382
- }
383
- }
384
- TreemapElement.id = 'treemap';
385
- TreemapElement.defaults = {
386
- borderRadius: 0,
387
- borderWidth: 0,
388
- captions: {
389
- align: undefined,
390
- color: 'black',
391
- display: true,
392
- font: {},
393
- formatter: (ctx)=>ctx.raw.g || ctx.raw._data.label || '',
394
- padding: 3
395
- },
396
- displayMode: 'containerBoxes',
397
- dividers: {
398
- display: false,
399
- lineCapStyle: 'butt',
400
- lineColor: 'black',
401
- lineDash: [],
402
- lineDashOffset: 0,
403
- lineWidth: 1
404
- },
405
- label: undefined,
406
- labels: {
407
- align: 'center',
408
- color: 'black',
409
- display: false,
410
- font: {},
411
- formatter (ctx) {
412
- if (ctx.raw.g) {
413
- return [
414
- ctx.raw.g,
415
- `${ctx.raw.v}`
416
- ];
417
- }
418
- return ctx.raw._data.label ? [
419
- ctx.raw._data.label,
420
- `${ctx.raw.v}`
421
- ] : `${ctx.raw.v}`;
422
- },
423
- overflow: 'cut',
424
- padding: 3,
425
- position: 'middle'
426
- },
427
- rtl: false,
428
- spacing: 0.5,
429
- unsorted: false
430
- };
431
- TreemapElement.descriptors = {
432
- _indexable: false,
433
- _scriptable: true,
434
- captions: {
435
- _fallback: true
436
- },
437
- labels: {
438
- _fallback: true
439
- }
440
- };
441
- TreemapElement.defaultRoutes = {
442
- backgroundColor: 'backgroundColor',
443
- borderColor: 'borderColor'
444
- };
445
-
446
12
  function scaleRect(sq, xScale, yScale, sp) {
447
13
  const sp2 = sp * 2;
448
14
  const x = xScale.getPixelForValue(sq.x);
@@ -480,6 +46,29 @@ function arrayNotEqual(a, b) {
480
46
  return false;
481
47
  }
482
48
 
49
+ function limit(value, min, max) {
50
+ return Math.max(Math.min(value, max), min);
51
+ }
52
+ function parseBorderWidth(value, maxW, maxH) {
53
+ const o = toTRBL(value);
54
+ return {
55
+ b: limit(o.bottom, 0, maxH),
56
+ l: limit(o.left, 0, maxW),
57
+ r: limit(o.right, 0, maxW),
58
+ t: limit(o.top, 0, maxH)
59
+ };
60
+ }
61
+ function parseBorderRadius(value, maxW, maxH) {
62
+ const o = toTRBLCorners(value);
63
+ const maxR = Math.min(maxW, maxH);
64
+ return {
65
+ bottomLeft: limit(o.bottomLeft, 0, maxR),
66
+ bottomRight: limit(o.bottomRight, 0, maxR),
67
+ topLeft: limit(o.topLeft, 0, maxR),
68
+ topRight: limit(o.topRight, 0, maxR)
69
+ };
70
+ }
71
+
483
72
  function getDims(itm, w2, s2, key) {
484
73
  const a = itm._normalized;
485
74
  const ar = w2 * a / s2;
@@ -644,6 +233,7 @@ class StatArray {
644
233
 
645
234
  const isOlderPart = (act, req)=>req > act || act.length > req.length && act.startsWith(req);
646
235
  const getGroupKey = (lvl)=>String(lvl);
236
+ const hasGroupValue = (value)=>value !== undefined && value !== null && value !== '';
647
237
  function scanTreeObject(keys, treeLeafKey, obj, tree = [], lvl = 0, result = []) {
648
238
  const objIndex = lvl - 1;
649
239
  if (keys[0] in obj && lvl > 0) {
@@ -705,55 +295,78 @@ function flatten(input) {
705
295
  return res.reverse();
706
296
  }
707
297
  function getPath(groups, value, defaultValue) {
708
- if (!groups.length) {
709
- return undefined;
710
- }
711
298
  const path = [];
712
299
  for (const grp of groups){
713
300
  const item = value[grp];
714
- if (item === '') {
715
- path.push(defaultValue);
716
- break;
301
+ if (!hasGroupValue(item)) {
302
+ continue;
717
303
  }
718
304
  path.push(item);
719
305
  }
720
306
  return path.length ? path.join('.') : defaultValue;
721
307
  }
722
- function group(values, grp, keys, treeLeafKey, mainGrp, mainValue, groups = []) {
308
+ function resolveGroup(value, treeLeafKey, allGroups, groupIndex) {
309
+ for(let idx = groupIndex; idx < allGroups.length; idx++){
310
+ const groupKey = getGroupKey(allGroups[idx]);
311
+ if (hasGroupValue(value[groupKey])) {
312
+ return {
313
+ group: groupKey,
314
+ groupIndex: idx,
315
+ value: value[groupKey]
316
+ };
317
+ }
318
+ }
319
+ if (hasGroupValue(value[treeLeafKey])) {
320
+ return {
321
+ group: treeLeafKey,
322
+ groupIndex,
323
+ value: value[treeLeafKey]
324
+ };
325
+ }
326
+ return undefined;
327
+ }
328
+ function ensureGroup(tmp, data, groupValue, addKeys) {
329
+ if (groupValue in tmp) {
330
+ return;
331
+ }
332
+ tmp[groupValue] = {
333
+ value: 0
334
+ };
335
+ const tmpRef = tmp[groupValue];
336
+ addKeys.forEach((k)=>{
337
+ tmpRef[k] = 0;
338
+ });
339
+ data[groupValue] = [];
340
+ }
341
+ function group(values, grp, keys, treeLeafKey, mainGrp, mainValue, groups = [], allGroups = groups, groupIndex = groups.length - 1) {
723
342
  const key = keys[0];
724
343
  const addKeys = keys.slice(1);
725
344
  const tmp = Object.create(null);
726
345
  const data = Object.create(null);
727
346
  const ret = [];
728
- let g;
347
+ const resolvedAllGroups = allGroups.length ? allGroups : [
348
+ grp
349
+ ];
350
+ const resolvedGroupIndex = allGroups.length ? groupIndex : 0;
729
351
  let i;
730
352
  let n;
731
353
  for(i = 0, n = values.length; i < n; ++i){
732
354
  const v = values[i];
733
- if (mainGrp && v[mainGrp] !== mainValue) {
734
- continue;
735
- }
736
- g = v[grp] || v[treeLeafKey] || '';
737
- if (!g) {
355
+ const itemGroup = resolveGroup(v, treeLeafKey, resolvedAllGroups, resolvedGroupIndex);
356
+ if (!itemGroup) {
738
357
  return [];
739
358
  }
740
- if (!(g in tmp)) {
741
- tmp[g] = {
742
- value: 0
743
- };
744
- const tmpRef = tmp[g];
745
- addKeys.forEach((k)=>{
746
- tmpRef[k] = 0;
747
- });
748
- data[g] = [];
749
- }
359
+ const g = itemGroup.value;
360
+ ensureGroup(tmp, data, g, addKeys);
750
361
  tmp[g].value += +v[key];
751
- tmp[g].label = v[grp] || '';
362
+ tmp[g].label = g;
363
+ tmp[g].group = itemGroup.group;
364
+ tmp[g].groupIndex = itemGroup.groupIndex;
752
365
  const tmpRef = tmp[g];
753
366
  addKeys.forEach((k)=>{
754
367
  tmpRef[k] += v[k];
755
368
  });
756
- tmp[g].path = getPath(groups, v, g);
369
+ tmp[g].path = getPath(resolvedAllGroups.slice(0, itemGroup.groupIndex + 1), v, g);
757
370
  data[g].push(v);
758
371
  }
759
372
  Object.keys(tmp).forEach((k)=>{
@@ -765,11 +378,10 @@ function group(values, grp, keys, treeLeafKey, mainGrp, mainValue, groups = [])
765
378
  v[ak] = +tmp[k][ak];
766
379
  });
767
380
  v[grp] = tmp[k].label;
381
+ v.group = tmp[k].group;
382
+ v.groupIndex = tmp[k].groupIndex;
768
383
  v.label = k;
769
384
  v.path = tmp[k].path;
770
- if (mainGrp) {
771
- v[mainGrp] = mainValue;
772
- }
773
385
  ret.push(v);
774
386
  });
775
387
  return ret;
@@ -853,42 +465,301 @@ function squarify(values, rectangle, keys = [], grp, lvl, gsum) {
853
465
  if (!n) {
854
466
  return rows;
855
467
  }
856
- const tmp = values.slice();
857
- const key = index(tmp, keys[0]);
858
- if (!rectangle?.unsorted) {
859
- sort(tmp, key);
468
+ const tmp = values.slice();
469
+ const key = index(tmp, keys[0]);
470
+ if (!rectangle?.unsorted) {
471
+ sort(tmp, key);
472
+ }
473
+ const val = (idx)=>key ? +tmp[idx][key] : +tmp[idx];
474
+ const gval = (idx)=>grp && tmp[idx][grp];
475
+ for(i = 0; i < n; ++i){
476
+ o = {
477
+ _data: values[tmp[i]._idx],
478
+ group: undefined,
479
+ groupSum: gsum,
480
+ level: undefined,
481
+ value: val(i)
482
+ };
483
+ if (grp) {
484
+ o.level = tmp[i].groupIndex ?? lvl;
485
+ o.group = gval(i);
486
+ const tmpRef = tmp[i];
487
+ o.values = keys.reduce((obj, k)=>{
488
+ obj[k] = +tmpRef[k];
489
+ return obj;
490
+ }, {});
491
+ }
492
+ o = row.pushIf(o, compareAspectRatio, length);
493
+ if (o) {
494
+ rows.push(rect.map(row));
495
+ length = rect.side;
496
+ row.reset();
497
+ row.push(o);
498
+ }
499
+ }
500
+ if (row.length) {
501
+ rows.push(rect.map(row));
502
+ }
503
+ return flatten(rows);
504
+ }
505
+
506
+ const widthCache = new Map();
507
+ function shouldDrawCaption(displayMode, rect, options) {
508
+ if (!options || options.display === false) {
509
+ return false;
510
+ }
511
+ if (displayMode === 'headerBoxes') {
512
+ return true;
513
+ }
514
+ const { w, h } = rect;
515
+ const font = toFont(options.font || {});
516
+ const min = font.lineHeight;
517
+ const padding = Math.max(Math.min(valueOrDefault(options.padding, 3) * 2, Math.min(w, h)), 0);
518
+ return w - padding > min && h - padding > min;
519
+ }
520
+ function getCaptionHeight(displayMode, rect, font, padding) {
521
+ if (displayMode !== 'headerBoxes') {
522
+ return font.lineHeight + padding * 2;
523
+ }
524
+ const captionHeight = font.lineHeight + padding * 2;
525
+ return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight;
526
+ }
527
+ function drawText(ctx, rect, options, item, element) {
528
+ const { captions, labels, displayMode } = options;
529
+ ctx.save();
530
+ ctx.beginPath();
531
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
532
+ ctx.clip();
533
+ const isLeaf = item && (!defined(item.l) || item.isLeaf);
534
+ if (isLeaf && labels.display) {
535
+ drawLabel(ctx, rect, options, item, element);
536
+ } else if (!isLeaf && shouldDrawCaption(displayMode, rect, captions)) {
537
+ drawCaption(ctx, rect, options, item, element);
538
+ }
539
+ ctx.restore();
540
+ }
541
+ function callbackContext(element, item) {
542
+ const context = element.$context || {
543
+ active: !!element.active,
544
+ raw: item,
545
+ type: 'data'
546
+ };
547
+ context.active = !!element.active;
548
+ context.raw = item;
549
+ return context;
550
+ }
551
+ function resolveOption(option, context) {
552
+ return typeof option === 'function' ? option(context) : option;
553
+ }
554
+ function resolveCaptionText(captions, element, item) {
555
+ return resolveOption(captions.formatter, callbackContext(element, item)) || item.g || '';
556
+ }
557
+ function drawCaption(ctx, rect, options, item, element) {
558
+ const { captions, spacing, rtl, displayMode } = options;
559
+ const { color, hoverColor, font, hoverFont, padding, align } = captions;
560
+ const oColor = (rect.active ? hoverColor : color) || color;
561
+ const oAlign = align || (rtl ? 'right' : 'left');
562
+ const optFont = (rect.active ? hoverFont : font) || font;
563
+ const oFont = toFont(optFont);
564
+ const fonts = [
565
+ oFont
566
+ ];
567
+ if (oFont.lineHeight > rect.h) {
568
+ return;
569
+ }
570
+ let text = resolveCaptionText(captions, element, item);
571
+ if (!text) {
572
+ return;
573
+ }
574
+ const captionSize = measureLabelSize(ctx, [
575
+ text
576
+ ], fonts);
577
+ if (captionSize.width + 2 * padding > rect.w) {
578
+ text = sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
579
+ }
580
+ const lh = oFont.lineHeight / 2;
581
+ const x = calculateX(rect, oAlign, padding);
582
+ ctx.fillStyle = oColor;
583
+ ctx.font = oFont.string;
584
+ ctx.textAlign = oAlign;
585
+ ctx.textBaseline = 'middle';
586
+ const y = displayMode === 'headerBoxes' ? rect.y + rect.h / 2 : rect.y + padding + spacing + lh;
587
+ ctx.fillText(text, x, y);
588
+ }
589
+ function sliceTextToFitWidth(ctx, text, width, fonts) {
590
+ const ellipsis = '...';
591
+ const ellipsisWidth = measureLabelSize(ctx, [
592
+ ellipsis
593
+ ], fonts).width;
594
+ if (ellipsisWidth >= width) {
595
+ return '';
596
+ }
597
+ let lowerBoundLen = 1;
598
+ let upperBoundLen = text.length;
599
+ let currentWidth;
600
+ while(lowerBoundLen <= upperBoundLen){
601
+ const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
602
+ const currentText = text.slice(0, currentLen);
603
+ currentWidth = measureLabelSize(ctx, [
604
+ currentText
605
+ ], fonts).width;
606
+ if (currentWidth + ellipsisWidth > width) {
607
+ upperBoundLen = currentLen - 1;
608
+ } else {
609
+ lowerBoundLen = currentLen + 1;
610
+ }
611
+ }
612
+ const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
613
+ return slicedText ? slicedText + ellipsis : '';
614
+ }
615
+ function measureLabelSize(ctx, lines, fonts) {
616
+ const fontsKey = fonts.reduce((prev, item)=>{
617
+ prev += item.string;
618
+ return prev;
619
+ }, '');
620
+ const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
621
+ let size = widthCache.get(mapKey);
622
+ if (!size) {
623
+ ctx.save();
624
+ const count = lines.length;
625
+ let width = 0;
626
+ let height = 0;
627
+ for(let i = 0; i < count; i++){
628
+ const font = fonts[Math.min(i, fonts.length - 1)];
629
+ ctx.font = font.string;
630
+ const text = lines[i];
631
+ width = Math.max(width, ctx.measureText(text).width);
632
+ height += font.lineHeight;
633
+ }
634
+ ctx.restore();
635
+ size = {
636
+ height,
637
+ width
638
+ };
639
+ widthCache.set(mapKey, size);
640
+ }
641
+ return size;
642
+ }
643
+ function toFonts(fonts, fitRatio) {
644
+ return fonts.map((f)=>{
645
+ const { lineHeight: _lineHeight, ...font } = f;
646
+ return toFont({
647
+ ...font,
648
+ size: Math.floor(f.size * fitRatio)
649
+ });
650
+ });
651
+ }
652
+ function labelToDraw(_ctx, rect, options, labelSize) {
653
+ const { overflow, padding } = options;
654
+ const { width, height } = labelSize;
655
+ if (overflow === 'hidden') {
656
+ return !(width + padding * 2 > rect.w || height + padding * 2 > rect.h);
657
+ } else if (overflow === 'fit') {
658
+ const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
659
+ if (ratio < 1) {
660
+ return ratio;
661
+ }
662
+ }
663
+ return true;
664
+ }
665
+ function getFontFromOptions(rect, labels) {
666
+ const { font, hoverFont } = labels;
667
+ const optFont = (rect.active ? hoverFont : font) || font;
668
+ return Array.isArray(optFont) ? optFont.map((f)=>toFont(f)) : [
669
+ toFont(optFont)
670
+ ];
671
+ }
672
+ function drawLabel(ctx, rect, options, item, element) {
673
+ const labels = options.labels;
674
+ const content = resolveOption(labels.formatter, callbackContext(element, item));
675
+ if (!content) {
676
+ return;
677
+ }
678
+ const contents = isArray(content) ? content : [
679
+ content
680
+ ];
681
+ let fonts = getFontFromOptions(rect, labels);
682
+ let labelSize = measureLabelSize(ctx, contents, fonts);
683
+ const lblToDraw = labelToDraw(ctx, rect, labels, labelSize);
684
+ if (!lblToDraw) {
685
+ return;
686
+ }
687
+ if (isNumber(lblToDraw)) {
688
+ labelSize = {
689
+ height: labelSize.height * lblToDraw,
690
+ width: labelSize.width * lblToDraw
691
+ };
692
+ fonts = toFonts(fonts, lblToDraw);
693
+ }
694
+ const { color, hoverColor, align } = labels;
695
+ const optColor = (rect.active ? hoverColor : color) || color;
696
+ const colors = isArray(optColor) ? optColor : [
697
+ optColor
698
+ ];
699
+ const xyPoint = calculateXYLabel(rect, labels, labelSize);
700
+ ctx.textAlign = align;
701
+ ctx.textBaseline = 'middle';
702
+ let lhs = 0;
703
+ contents.forEach((l, i)=>{
704
+ const c = colors[Math.min(i, colors.length - 1)];
705
+ const f = fonts[Math.min(i, fonts.length - 1)];
706
+ const lh = f.lineHeight;
707
+ ctx.font = f.string;
708
+ ctx.fillStyle = c;
709
+ ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs);
710
+ lhs += lh;
711
+ });
712
+ }
713
+ function drawDivider(ctx, rect, options, item) {
714
+ const dividers = options.dividers;
715
+ const children = item._data?.children;
716
+ if (!dividers.display || !Array.isArray(children) || !children.length) {
717
+ return;
718
+ }
719
+ const { x, y, w, h } = rect;
720
+ const { lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth } = dividers;
721
+ ctx.save();
722
+ ctx.strokeStyle = lineColor;
723
+ ctx.lineCap = lineCapStyle;
724
+ ctx.setLineDash(lineDash);
725
+ ctx.lineDashOffset = lineDashOffset;
726
+ ctx.lineWidth = lineWidth;
727
+ ctx.beginPath();
728
+ if (w > h) {
729
+ const w2 = w / 2;
730
+ ctx.moveTo(x + w2, y);
731
+ ctx.lineTo(x + w2, y + h);
732
+ } else {
733
+ const h2 = h / 2;
734
+ ctx.moveTo(x, y + h2);
735
+ ctx.lineTo(x + w, y + h2);
860
736
  }
861
- const val = (idx)=>key ? +tmp[idx][key] : +tmp[idx];
862
- const gval = (idx)=>grp && tmp[idx][grp];
863
- for(i = 0; i < n; ++i){
864
- o = {
865
- _data: values[tmp[i]._idx],
866
- group: undefined,
867
- groupSum: gsum,
868
- level: undefined,
869
- value: val(i)
870
- };
871
- if (grp) {
872
- o.level = lvl;
873
- o.group = gval(i);
874
- const tmpRef = tmp[i];
875
- o.values = keys.reduce((obj, k)=>{
876
- obj[k] = +tmpRef[k];
877
- return obj;
878
- }, {});
879
- }
880
- o = row.pushIf(o, compareAspectRatio, length);
881
- if (o) {
882
- rows.push(rect.map(row));
883
- length = rect.side;
884
- row.reset();
885
- row.push(o);
886
- }
737
+ ctx.stroke();
738
+ ctx.restore();
739
+ }
740
+ function calculateXYLabel(rect, options, labelSize) {
741
+ const { align, position, padding } = options;
742
+ const x = calculateX(rect, align, padding);
743
+ let y;
744
+ if (position === 'top') {
745
+ y = rect.y + padding;
746
+ } else if (position === 'bottom') {
747
+ y = rect.y + rect.h - padding - labelSize.height;
748
+ } else {
749
+ y = rect.y + (rect.h - labelSize.height) / 2 + padding;
887
750
  }
888
- if (row.length) {
889
- rows.push(rect.map(row));
751
+ return {
752
+ x,
753
+ y
754
+ };
755
+ }
756
+ function calculateX(rect, align, padding) {
757
+ if (align === 'left') {
758
+ return rect.x + padding;
759
+ } else if (align === 'right') {
760
+ return rect.x + rect.w - padding;
890
761
  }
891
- return flatten(rows);
762
+ return rect.x + rect.w / 2;
892
763
  }
893
764
 
894
765
  function buildData(tree, dataset, keys, mainRect) {
@@ -902,36 +773,40 @@ function buildData(tree, dataset, keys, mainRect) {
902
773
  const captions = dataset.captions || {};
903
774
  const font = toFont(captions.font);
904
775
  const padding = valueOrDefault(captions.padding, 3);
776
+ function getSubRect(sq, rect) {
777
+ const bw = dataset.displayMode === 'headerBoxes' ? {
778
+ b: 0,
779
+ l: 0,
780
+ r: 0,
781
+ t: 0
782
+ } : parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2);
783
+ const subRect = {
784
+ ...rect,
785
+ h: sq.h - 2 * sp - bw.t - bw.b,
786
+ w: sq.w - 2 * sp - bw.l - bw.r,
787
+ x: sq.x + sp + bw.l,
788
+ y: sq.y + sp + bw.t
789
+ };
790
+ if (shouldDrawCaption(dataset.displayMode, subRect, captions)) {
791
+ const captionHeight = getCaptionHeight(dataset.displayMode, subRect, font, padding);
792
+ subRect.y += captionHeight;
793
+ subRect.h -= captionHeight;
794
+ }
795
+ return subRect;
796
+ }
905
797
  function recur(treeElements, gidx, rect, parent, gs) {
906
798
  const g = getGroupKey(groups[gidx]);
907
- const pg = gidx > 0 ? getGroupKey(groups[gidx - 1]) : undefined;
908
- const gdata = group(treeElements, g, keys, treeLeafKey, pg, parent, groups.filter((_item, index)=>index <= gidx));
799
+ const gdata = group(treeElements, g, keys, treeLeafKey, undefined, parent, groups.filter((_item, index)=>index <= gidx), groups, gidx);
909
800
  const gsq = squarify(gdata, rect, keys, g, gidx, gs);
910
801
  const ret = gsq.slice();
911
802
  if (gidx < glen - 1) {
912
803
  gsq.forEach((sq)=>{
913
- const bw = dataset.displayMode === 'headerBoxes' ? {
914
- b: 0,
915
- l: 0,
916
- r: 0,
917
- t: 0
918
- } : parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2);
919
- const subRect = {
920
- ...rect,
921
- h: sq.h - 2 * sp - bw.t - bw.b,
922
- w: sq.w - 2 * sp - bw.l - bw.r,
923
- x: sq.x + sp + bw.l,
924
- y: sq.y + sp + bw.t
925
- };
926
- if (shouldDrawCaption(dataset.displayMode, subRect, captions)) {
927
- const captionHeight = getCaptionHeight(dataset.displayMode, subRect, font, padding);
928
- subRect.y += captionHeight;
929
- subRect.h -= captionHeight;
930
- }
804
+ const subRect = getSubRect(sq, rect);
931
805
  const children = [];
932
- gdata.forEach((gEl)=>{
933
- children.push(...recur(gEl.children, gidx + 1, subRect, sq.g, sq.s));
934
- });
806
+ const nextGroupIndex = sq._data.groupIndex + 1;
807
+ if (nextGroupIndex < glen) {
808
+ children.push(...recur(sq._data.children, nextGroupIndex, subRect, sq.g, sq.s));
809
+ }
935
810
  ret.push(...children);
936
811
  sq.isLeaf = !children.length;
937
812
  });
@@ -1080,7 +955,15 @@ TreemapController.defaults = {
1080
955
  };
1081
956
  TreemapController.descriptors = {
1082
957
  _indexable: false,
1083
- _scriptable: true
958
+ _scriptable: true,
959
+ captions: {
960
+ _fallback: true,
961
+ _scriptable: (name)=>name !== 'formatter'
962
+ },
963
+ labels: {
964
+ _fallback: true,
965
+ _scriptable: (name)=>name !== 'formatter'
966
+ }
1084
967
  };
1085
968
  TreemapController.overrides = {
1086
969
  hover: {},
@@ -1151,4 +1034,188 @@ TreemapController.afterUnregister = ()=>{
1151
1034
  }
1152
1035
  };
1153
1036
 
1037
+ function getBounds(rect, useFinalPosition) {
1038
+ const { x, y, width, height } = rect.getProps([
1039
+ 'x',
1040
+ 'y',
1041
+ 'width',
1042
+ 'height'
1043
+ ], useFinalPosition);
1044
+ return {
1045
+ bottom: y + height,
1046
+ left: x,
1047
+ right: x + width,
1048
+ top: y
1049
+ };
1050
+ }
1051
+ function boundingRects(rect) {
1052
+ const bounds = getBounds(rect);
1053
+ const width = bounds.right - bounds.left;
1054
+ const height = bounds.bottom - bounds.top;
1055
+ const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
1056
+ const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2);
1057
+ const outer = {
1058
+ active: rect.active,
1059
+ h: height,
1060
+ radius,
1061
+ w: width,
1062
+ x: bounds.left,
1063
+ y: bounds.top
1064
+ };
1065
+ return {
1066
+ inner: {
1067
+ active: rect.active,
1068
+ h: outer.h - border.t - border.b,
1069
+ radius: {
1070
+ bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
1071
+ bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
1072
+ topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
1073
+ topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r))
1074
+ },
1075
+ w: outer.w - border.l - border.r,
1076
+ x: outer.x + border.l,
1077
+ y: outer.y + border.t
1078
+ },
1079
+ outer
1080
+ };
1081
+ }
1082
+ function inRange(rect, x, y, useFinalPosition) {
1083
+ const skipX = x === null;
1084
+ const skipY = y === null;
1085
+ const bounds = !rect || skipX && skipY ? false : getBounds(rect, useFinalPosition);
1086
+ return bounds && (skipX || x >= bounds.left && x <= bounds.right) && (skipY || y >= bounds.top && y <= bounds.bottom);
1087
+ }
1088
+ function hasRadius(radius) {
1089
+ return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
1090
+ }
1091
+ function addNormalRectPath(ctx, rect) {
1092
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
1093
+ }
1094
+
1095
+ class TreemapElement extends Element {
1096
+ draw(ctx, data) {
1097
+ if (!data) {
1098
+ return;
1099
+ }
1100
+ const options = this.options;
1101
+ const { inner, outer } = boundingRects(this);
1102
+ const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
1103
+ ctx.save();
1104
+ if (outer.w !== inner.w || outer.h !== inner.h) {
1105
+ ctx.beginPath();
1106
+ addRectPath(ctx, outer);
1107
+ ctx.clip();
1108
+ addRectPath(ctx, inner);
1109
+ ctx.fillStyle = options.borderColor;
1110
+ ctx.fill('evenodd');
1111
+ }
1112
+ ctx.beginPath();
1113
+ addRectPath(ctx, inner);
1114
+ ctx.fillStyle = options.backgroundColor;
1115
+ ctx.fill();
1116
+ drawDivider(ctx, inner, options, data);
1117
+ drawText(ctx, inner, options, data, this);
1118
+ ctx.restore();
1119
+ }
1120
+ inRange(mouseX, mouseY, useFinalPosition) {
1121
+ return inRange(this, mouseX, mouseY, useFinalPosition);
1122
+ }
1123
+ inXRange(mouseX, useFinalPosition) {
1124
+ return inRange(this, mouseX, null, useFinalPosition);
1125
+ }
1126
+ inYRange(mouseY, useFinalPosition) {
1127
+ return inRange(this, null, mouseY, useFinalPosition);
1128
+ }
1129
+ getCenterPoint(useFinalPosition) {
1130
+ const { x, y, width, height } = this.getProps([
1131
+ 'x',
1132
+ 'y',
1133
+ 'width',
1134
+ 'height'
1135
+ ], useFinalPosition);
1136
+ return {
1137
+ x: x + width / 2,
1138
+ y: y + height / 2
1139
+ };
1140
+ }
1141
+ tooltipPosition() {
1142
+ return this.getCenterPoint();
1143
+ }
1144
+ constructor(cfg){
1145
+ super();
1146
+ this.width = undefined;
1147
+ this.height = undefined;
1148
+ if (cfg) {
1149
+ Object.assign(this, cfg);
1150
+ }
1151
+ }
1152
+ }
1153
+ TreemapElement.id = 'treemap';
1154
+ TreemapElement.defaults = {
1155
+ borderRadius: 0,
1156
+ borderWidth: 0,
1157
+ captions: {
1158
+ align: undefined,
1159
+ color: 'black',
1160
+ display: true,
1161
+ font: {},
1162
+ formatter: (ctx)=>{
1163
+ const label = ctx.raw._data?.label;
1164
+ return ctx.raw.g || (typeof label === 'string' ? label : '');
1165
+ },
1166
+ padding: 3
1167
+ },
1168
+ displayMode: 'containerBoxes',
1169
+ dividers: {
1170
+ display: false,
1171
+ lineCapStyle: 'butt',
1172
+ lineColor: 'black',
1173
+ lineDash: [],
1174
+ lineDashOffset: 0,
1175
+ lineWidth: 1
1176
+ },
1177
+ label: undefined,
1178
+ labels: {
1179
+ align: 'center',
1180
+ color: 'black',
1181
+ display: false,
1182
+ font: {},
1183
+ formatter (ctx) {
1184
+ const label = ctx.raw._data?.label;
1185
+ if (ctx.raw.g) {
1186
+ return [
1187
+ ctx.raw.g,
1188
+ `${ctx.raw.v}`
1189
+ ];
1190
+ }
1191
+ return typeof label === 'string' ? [
1192
+ label,
1193
+ `${ctx.raw.v}`
1194
+ ] : `${ctx.raw.v}`;
1195
+ },
1196
+ overflow: 'cut',
1197
+ padding: 3,
1198
+ position: 'middle'
1199
+ },
1200
+ rtl: false,
1201
+ spacing: 0.5,
1202
+ unsorted: false
1203
+ };
1204
+ TreemapElement.descriptors = {
1205
+ _indexable: false,
1206
+ _scriptable: true,
1207
+ captions: {
1208
+ _fallback: true,
1209
+ _scriptable: (name)=>name !== 'formatter'
1210
+ },
1211
+ labels: {
1212
+ _fallback: true,
1213
+ _scriptable: (name)=>name !== 'formatter'
1214
+ }
1215
+ };
1216
+ TreemapElement.defaultRoutes = {
1217
+ backgroundColor: 'backgroundColor',
1218
+ borderColor: 'borderColor'
1219
+ };
1220
+
1154
1221
  export { TreemapController, TreemapElement };