chartjs-chart-treemap 4.0.0 → 4.1.0

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