chartjs-chart-treemap 2.0.2 → 2.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.
@@ -1,11 +1,70 @@
1
1
  /*!
2
- * chartjs-chart-treemap v2.0.2
2
+ * chartjs-chart-treemap v2.1.0
3
3
  * https://chartjs-chart-treemap.pages.dev/
4
- * (c) 2021 Jukka Kurkela
4
+ * (c) 2022 Jukka Kurkela
5
5
  * Released under the MIT license
6
6
  */
7
- import { Chart, registry, DatasetController, Element } from 'chart.js';
8
- import { toFont, valueOrDefault, isArray } from 'chart.js/helpers';
7
+ import { Element, Chart, registry, DatasetController } from 'chart.js';
8
+ import { isObject, addRoundedRectPath, toFont, isArray, toTRBL, toTRBLCorners, valueOrDefault, defined, clipArea, unclipArea } from 'chart.js/helpers';
9
+
10
+ const getValue = (item) => isObject(item) ? item.v : item;
11
+
12
+ const maxValue = (data) => data.reduce(function(m, v) {
13
+ return (m > getValue(v) ? m : getValue(v));
14
+ }, 0);
15
+
16
+ const minValue = (data, mx) => data.reduce(function(m, v) {
17
+ return (m < getValue(v) ? m : getValue(v));
18
+ }, mx);
19
+
20
+ const getGroupKey = (lvl) => '' + lvl;
21
+
22
+ function scanTreeObject(key, treeLeafKey, obj, tree = [], lvl = 0, result = []) {
23
+ const objIndex = lvl - 1;
24
+ if (key in obj && lvl > 0) {
25
+ const record = tree.reduce(function(reduced, item, i) {
26
+ if (i !== objIndex) {
27
+ reduced[getGroupKey(i)] = item;
28
+ }
29
+ return reduced;
30
+ }, {});
31
+ record[treeLeafKey] = tree[objIndex];
32
+ record[key] = obj[key];
33
+ result.push(record);
34
+ } else {
35
+ for (const childKey of Object.keys(obj)) {
36
+ const child = obj[childKey];
37
+ if (isObject(child)) {
38
+ tree.push(childKey);
39
+ scanTreeObject(key, treeLeafKey, child, tree, lvl + 1, result);
40
+ }
41
+ }
42
+ }
43
+ tree.splice(objIndex, 1);
44
+ return result;
45
+ }
46
+
47
+ function normalizeTreeToArray(key, treeLeafKey, obj) {
48
+ const data = scanTreeObject(key, treeLeafKey, obj);
49
+ if (!data.length) {
50
+ return data;
51
+ }
52
+ const max = data.reduce(function(maxVal, element) {
53
+ // minus 2 because _leaf and value properties are added
54
+ // on top to groups ones
55
+ const keys = Object.keys(element).length - 2;
56
+ return maxVal > keys ? maxVal : keys;
57
+ });
58
+ data.forEach(function(element) {
59
+ for (let i = 0; i < max; i++) {
60
+ const groupKey = getGroupKey(i);
61
+ if (!element[groupKey]) {
62
+ element[groupKey] = '';
63
+ }
64
+ }
65
+ });
66
+ return data;
67
+ }
9
68
 
10
69
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
11
70
  function flatten(input) {
@@ -25,36 +84,59 @@ function flatten(input) {
25
84
  return res.reverse();
26
85
  }
27
86
 
87
+ function getPath(groups, value, defaultValue) {
88
+ if (!groups.length) {
89
+ return;
90
+ }
91
+ const path = [];
92
+ for (const grp of groups) {
93
+ const item = value[grp];
94
+ if (item === '') {
95
+ path.push(defaultValue);
96
+ break;
97
+ }
98
+ path.push(item);
99
+ }
100
+ return path.length ? path.join('.') : defaultValue;
101
+ }
102
+
28
103
  /**
29
104
  * @param {[]} values
30
105
  * @param {string} grp
31
106
  * @param {string} key
107
+ * @param {string} treeeLeafKey
32
108
  * @param {string} [mainGrp]
33
109
  * @param {*} [mainValue]
110
+ * @param {[]} groups
34
111
  */
35
- function group(values, grp, key, mainGrp, mainValue) {
112
+ function group(values, grp, key, treeLeafKey, mainGrp, mainValue, groups = []) {
36
113
  const tmp = Object.create(null);
37
114
  const data = Object.create(null);
38
115
  const ret = [];
39
- let g, i, n, v;
116
+ let g, i, n;
40
117
  for (i = 0, n = values.length; i < n; ++i) {
41
- v = values[i];
118
+ const v = values[i];
42
119
  if (mainGrp && v[mainGrp] !== mainValue) {
43
120
  continue;
44
121
  }
45
- g = v[grp] || '';
122
+ g = v[grp] || v[treeLeafKey] || '';
46
123
  if (!(g in tmp)) {
47
- tmp[g] = 0;
124
+ tmp[g] = {value: 0};
48
125
  data[g] = [];
49
126
  }
50
- tmp[g] += +v[key];
127
+ tmp[g].value += +v[key];
128
+ tmp[g].label = v[grp] || '';
129
+ tmp[g].path = getPath(groups, v, g);
51
130
  data[g].push(v);
52
131
  }
53
132
 
54
133
  Object.keys(tmp).forEach((k) => {
55
- v = {children: data[k]};
56
- v[key] = +tmp[k];
57
- v[grp] = k;
134
+ const v = {children: data[k]};
135
+ v[key] = +tmp[k].value;
136
+ v[grp] = tmp[k].label;
137
+ v.label = k;
138
+ v.path = tmp[k].path;
139
+
58
140
  if (mainGrp) {
59
141
  v[mainGrp] = mainValue;
60
142
  }
@@ -64,11 +146,6 @@ function group(values, grp, key, mainGrp, mainValue) {
64
146
  return ret;
65
147
  }
66
148
 
67
- function isObject(obj) {
68
- const type = typeof obj;
69
- return type === 'function' || type === 'object' && !!obj;
70
- }
71
-
72
149
  function index(values, key) {
73
150
  let n = values.length;
74
151
  let i;
@@ -115,6 +192,387 @@ function requireVersion(min, ver) {
115
192
  }
116
193
  }
117
194
 
195
+ const widthCache = new Map();
196
+
197
+ /**
198
+ * Helper function to get the bounds of the rect
199
+ * @param {TreemapElement} rect the rect
200
+ * @param {boolean} [useFinalPosition]
201
+ * @return {object} bounds of the rect
202
+ * @private
203
+ */
204
+ function getBounds(rect, useFinalPosition) {
205
+ const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
206
+ return {left: x, top: y, right: x + width, bottom: y + height};
207
+ }
208
+
209
+ function limit(value, min, max) {
210
+ return Math.max(Math.min(value, max), min);
211
+ }
212
+
213
+ function parseBorderWidth(value, maxW, maxH) {
214
+ const o = toTRBL(value);
215
+
216
+ return {
217
+ t: limit(o.top, 0, maxH),
218
+ r: limit(o.right, 0, maxW),
219
+ b: limit(o.bottom, 0, maxH),
220
+ l: limit(o.left, 0, maxW)
221
+ };
222
+ }
223
+
224
+ function parseBorderRadius(value, maxW, maxH) {
225
+ const o = toTRBLCorners(value);
226
+ const maxR = Math.min(maxW, maxH);
227
+
228
+ return {
229
+ topLeft: limit(o.topLeft, 0, maxR),
230
+ topRight: limit(o.topRight, 0, maxR),
231
+ bottomLeft: limit(o.bottomLeft, 0, maxR),
232
+ bottomRight: limit(o.bottomRight, 0, maxR)
233
+ };
234
+ }
235
+
236
+ function boundingRects(rect) {
237
+ const bounds = getBounds(rect);
238
+ const width = bounds.right - bounds.left;
239
+ const height = bounds.bottom - bounds.top;
240
+ const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
241
+ const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2);
242
+
243
+ return {
244
+ outer: {
245
+ x: bounds.left,
246
+ y: bounds.top,
247
+ w: width,
248
+ h: height,
249
+ radius
250
+ },
251
+ inner: {
252
+ x: bounds.left + border.l,
253
+ y: bounds.top + border.t,
254
+ w: width - border.l - border.r,
255
+ h: height - border.t - border.b,
256
+ radius: {
257
+ topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
258
+ topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
259
+ bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
260
+ bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
261
+ }
262
+ }
263
+ };
264
+ }
265
+
266
+ function inRange(rect, x, y, useFinalPosition) {
267
+ const skipX = x === null;
268
+ const skipY = y === null;
269
+ const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition);
270
+
271
+ return bounds
272
+ && (skipX || x >= bounds.left && x <= bounds.right)
273
+ && (skipY || y >= bounds.top && y <= bounds.bottom);
274
+ }
275
+
276
+ function hasRadius(radius) {
277
+ return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
278
+ }
279
+
280
+ /**
281
+ * Add a path of a rectangle to the current sub-path
282
+ * @param {CanvasRenderingContext2D} ctx Context
283
+ * @param {*} rect Bounding rect
284
+ */
285
+ function addNormalRectPath(ctx, rect) {
286
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
287
+ }
288
+
289
+ function shouldDrawCaption(rect, options) {
290
+ if (!options || !options.display) {
291
+ return false;
292
+ }
293
+ const {w, h} = rect;
294
+ const font = toFont(options.font);
295
+ const min = font.lineHeight;
296
+ const padding = limit(valueOrDefault(options.padding, 3) * 2, 0, Math.min(w, h));
297
+ return (w - padding) > min && (h - padding) > min;
298
+ }
299
+
300
+ function drawText(ctx, rect, options, item, levels) {
301
+ const {captions, labels} = options;
302
+ ctx.save();
303
+ ctx.beginPath();
304
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
305
+ ctx.clip();
306
+ const isLeaf = (!('l' in item) || item.l === levels);
307
+ if (isLeaf && labels.display) {
308
+ drawLabel(ctx, rect, options);
309
+ } else if (!isLeaf && shouldDrawCaption(rect, captions)) {
310
+ drawCaption(ctx, rect, options, item);
311
+ }
312
+ ctx.restore();
313
+ }
314
+
315
+ function drawCaption(ctx, rect, options, item) {
316
+ const {captions, spacing, rtl} = options;
317
+ const {color, hoverColor, font, hoverFont, padding, align, formatter} = captions;
318
+ const oColor = (rect.active ? hoverColor : color) || color;
319
+ const oAlign = align || (rtl ? 'right' : 'left');
320
+ const optFont = (rect.active ? hoverFont : font) || font;
321
+ const oFont = toFont(optFont);
322
+ const lh = oFont.lineHeight / 2;
323
+ const x = calculateX(rect, oAlign, padding);
324
+ ctx.fillStyle = oColor;
325
+ ctx.font = oFont.string;
326
+ ctx.textAlign = oAlign;
327
+ ctx.textBaseline = 'middle';
328
+ ctx.fillText(formatter || item.g, x, rect.y + padding + spacing + lh);
329
+ }
330
+
331
+ function measureLabelSize(ctx, lines, fonts) {
332
+ const fontsKey = fonts.reduce(function(prev, item) {
333
+ prev += item.string;
334
+ return prev;
335
+ }, '');
336
+ const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
337
+ if (!widthCache.has(mapKey)) {
338
+ ctx.save();
339
+ const count = lines.length;
340
+ let width = 0;
341
+ let height = 0;
342
+ for (let i = 0; i < count; i++) {
343
+ const font = fonts[Math.min(i, fonts.length - 1)];
344
+ ctx.font = font.string;
345
+ const text = lines[i];
346
+ width = Math.max(width, ctx.measureText(text).width);
347
+ height += font.lineHeight;
348
+ }
349
+ ctx.restore();
350
+ widthCache.set(mapKey, {width, height});
351
+ }
352
+ return widthCache.get(mapKey);
353
+ }
354
+
355
+ function labelToDraw(ctx, rect, options, labelSize) {
356
+ const {overflow, padding} = options;
357
+ const {width, height} = labelSize;
358
+ if (overflow === 'hidden') {
359
+ return !((width + padding * 2) > rect.w || (height + padding * 2) > rect.h);
360
+ }
361
+ return true;
362
+ }
363
+
364
+ function drawLabel(ctx, rect, options) {
365
+ const labels = options.labels;
366
+ const content = labels.formatter;
367
+ if (!content) {
368
+ return;
369
+ }
370
+ const contents = isArray(content) ? content : [content];
371
+ const {font, hoverFont} = labels;
372
+ const optFont = (rect.active ? hoverFont : font) || font;
373
+ const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
374
+ const labelSize = measureLabelSize(ctx, contents, fonts);
375
+ if (!labelToDraw(ctx, rect, labels, labelSize)) {
376
+ return;
377
+ }
378
+ const {color, hoverColor, align} = labels;
379
+ const optColor = (rect.active ? hoverColor : color) || color;
380
+ const colors = isArray(optColor) ? optColor : [optColor];
381
+ const xyPoint = calculateXYLabel(rect, labels, labelSize);
382
+ ctx.textAlign = align;
383
+ ctx.textBaseline = 'middle';
384
+ let lhs = 0;
385
+ contents.forEach(function(l, i) {
386
+ const c = colors[Math.min(i, colors.length - 1)];
387
+ const f = fonts[Math.min(i, fonts.length - 1)];
388
+ const lh = f.lineHeight;
389
+ ctx.font = f.string;
390
+ ctx.fillStyle = c;
391
+ ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs);
392
+ lhs += lh;
393
+ });
394
+ }
395
+
396
+ function drawDivider(ctx, rect, options, item) {
397
+ const dividers = options.dividers;
398
+ if (!dividers.display || !item._data.children.length) {
399
+ return;
400
+ }
401
+ const {x, y, w, h} = rect;
402
+ const {lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth} = dividers;
403
+ ctx.save();
404
+ ctx.strokeStyle = lineColor;
405
+ ctx.lineCap = lineCapStyle;
406
+ ctx.setLineDash(lineDash);
407
+ ctx.lineDashOffset = lineDashOffset;
408
+ ctx.lineWidth = lineWidth;
409
+ ctx.beginPath();
410
+ if (w > h) {
411
+ const w2 = w / 2;
412
+ ctx.moveTo(x + w2, y);
413
+ ctx.lineTo(x + w2, y + h);
414
+ } else {
415
+ const h2 = h / 2;
416
+ ctx.moveTo(x, y + h2);
417
+ ctx.lineTo(x + w, y + h2);
418
+ }
419
+ ctx.stroke();
420
+ ctx.restore();
421
+ }
422
+
423
+ function calculateXYLabel(rect, options, labelSize) {
424
+ const {align, position, padding} = options;
425
+ let x, y;
426
+ x = calculateX(rect, align, padding);
427
+ if (position === 'top') {
428
+ y = rect.y + padding;
429
+ } else if (position === 'bottom') {
430
+ y = rect.y + rect.h - padding - labelSize.height;
431
+ } else {
432
+ y = rect.y + (rect.h - labelSize.height) / 2 + padding;
433
+ }
434
+ return {x, y};
435
+ }
436
+
437
+ function calculateX(rect, align, padding) {
438
+ if (align === 'left') {
439
+ return rect.x + padding;
440
+ } else if (align === 'right') {
441
+ return rect.x + rect.w - padding;
442
+ }
443
+ return rect.x + rect.w / 2;
444
+ }
445
+
446
+ class TreemapElement extends Element {
447
+
448
+ constructor(cfg) {
449
+ super();
450
+
451
+ this.options = undefined;
452
+ this.width = undefined;
453
+ this.height = undefined;
454
+
455
+ if (cfg) {
456
+ Object.assign(this, cfg);
457
+ }
458
+ }
459
+
460
+ draw(ctx, data, levels = 0) {
461
+ if (!data) {
462
+ return;
463
+ }
464
+ const options = this.options;
465
+ const {inner, outer} = boundingRects(this);
466
+
467
+ const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
468
+
469
+ ctx.save();
470
+
471
+ if (outer.w !== inner.w || outer.h !== inner.h) {
472
+ ctx.beginPath();
473
+ addRectPath(ctx, outer);
474
+ ctx.clip();
475
+ addRectPath(ctx, inner);
476
+ ctx.fillStyle = options.borderColor;
477
+ ctx.fill('evenodd');
478
+ }
479
+
480
+ ctx.beginPath();
481
+ addRectPath(ctx, inner);
482
+ ctx.fillStyle = options.backgroundColor;
483
+ ctx.fill();
484
+
485
+ drawDivider(ctx, inner, options, data);
486
+ drawText(ctx, inner, options, data, levels);
487
+ ctx.restore();
488
+ }
489
+
490
+ inRange(mouseX, mouseY, useFinalPosition) {
491
+ return inRange(this, mouseX, mouseY, useFinalPosition);
492
+ }
493
+
494
+ inXRange(mouseX, useFinalPosition) {
495
+ return inRange(this, mouseX, null, useFinalPosition);
496
+ }
497
+
498
+ inYRange(mouseY, useFinalPosition) {
499
+ return inRange(this, null, mouseY, useFinalPosition);
500
+ }
501
+
502
+ getCenterPoint(useFinalPosition) {
503
+ const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
504
+ return {
505
+ x: x + width / 2,
506
+ y: y + height / 2
507
+ };
508
+ }
509
+
510
+ tooltipPosition() {
511
+ return this.getCenterPoint();
512
+ }
513
+
514
+ getRange(axis) {
515
+ return axis === 'x' ? this.width / 2 : this.height / 2;
516
+ }
517
+ }
518
+
519
+ TreemapElement.id = 'treemap';
520
+
521
+ TreemapElement.defaults = {
522
+ label: undefined,
523
+ borderRadius: 0,
524
+ borderWidth: 0,
525
+ captions: {
526
+ align: undefined,
527
+ color: 'black',
528
+ display: true,
529
+ font: {},
530
+ formatter: (ctx) => ctx.raw.g || ctx.raw._data.label || '',
531
+ padding: 3
532
+ },
533
+ dividers: {
534
+ display: false,
535
+ lineCapStyle: 'butt',
536
+ lineColor: 'black',
537
+ lineDash: [],
538
+ lineDashOffset: 0,
539
+ lineWidth: 1,
540
+ },
541
+ labels: {
542
+ align: 'center',
543
+ color: 'black',
544
+ display: false,
545
+ font: {},
546
+ formatter(ctx) {
547
+ if (ctx.raw.g) {
548
+ return [ctx.raw.g, ctx.raw.v + ''];
549
+ }
550
+ return ctx.raw._data.label ? [ctx.raw._data.label, ctx.raw.v + ''] : ctx.raw.v + '';
551
+ },
552
+ overflow: 'cut',
553
+ position: 'middle',
554
+ padding: 3
555
+ },
556
+ rtl: false,
557
+ spacing: 0.5
558
+ };
559
+
560
+ TreemapElement.descriptors = {
561
+ labels: {
562
+ _fallback: true
563
+ },
564
+ captions: {
565
+ _fallback: true
566
+ },
567
+ _scriptable: true,
568
+ _indexable: false
569
+ };
570
+
571
+ TreemapElement.defaultRoutes = {
572
+ backgroundColor: 'backgroundColor',
573
+ borderColor: 'borderColor'
574
+ };
575
+
118
576
  function round(v, n) {
119
577
  // @ts-ignore
120
578
  return (+(Math.round(v + 'e+' + n) + 'e-' + n)) || 0;
@@ -353,7 +811,7 @@ function squarify(values, rectangle, key, grp, lvl, gsum) {
353
811
  return flatten(rows);
354
812
  }
355
813
 
356
- var version = "2.0.2";
814
+ var version = "2.1.0";
357
815
 
358
816
  function rectNotEqual(r1, r2) {
359
817
  return !r1 || !r2
@@ -378,94 +836,38 @@ function arrayNotEqual(a1, a2) {
378
836
  return false;
379
837
  }
380
838
 
381
- function shouldDrawCaption(rect, font) {
382
- if (!font) {
383
- return false;
384
- }
385
- const w = rect.width || rect.w;
386
- const h = rect.height || rect.h;
387
- const min = font.lineHeight * 2;
388
- return w > min && h > min;
389
- }
390
-
391
- function drawCaption(ctx, rect, item, opts, levels) {
392
- ctx.save();
393
- ctx.beginPath();
394
- ctx.rect(rect.x, rect.y, rect.width, rect.height);
395
- ctx.clip();
396
- if (!('l' in item) || item.l === levels) {
397
- drawLabels(ctx, item, rect);
398
- } else if (opts.captions && opts.captions.display) {
399
- drawCaptionLabel(ctx, item, rect);
400
- }
401
- ctx.restore();
402
- }
403
-
404
- function drawCaptionLabel(ctx, item, rect) {
405
- const opts = rect.options;
406
- const captionsOpts = opts.captions || {};
407
- const borderWidth = opts.borderWidth || 0;
408
- const spacing = valueOrDefault(opts.spacing, 0) + borderWidth;
409
- const color = (rect.active ? captionsOpts.hoverColor : captionsOpts.color) || captionsOpts.color;
410
- const padding = captionsOpts.padding;
411
- const align = captionsOpts.align || (opts.rtl ? 'right' : 'left');
412
- const optFont = (rect.active ? captionsOpts.hoverFont : captionsOpts.font) || captionsOpts.font;
413
- const font = toFont(optFont);
414
- const x = calculateX(rect, align, padding, borderWidth);
415
- ctx.fillStyle = color;
416
- ctx.font = font.string;
417
- ctx.textAlign = align;
418
- ctx.textBaseline = 'middle';
419
- ctx.fillText(captionsOpts.formatter || item.g, x, rect.y + padding + spacing + (font.lineHeight / 2));
420
- }
421
-
422
- function drawDivider(ctx, rect) {
423
- const opts = rect.options;
424
- const dividersOpts = opts.dividers || {};
425
- const w = rect.width || rect.w;
426
- const h = rect.height || rect.h;
427
-
428
- ctx.save();
429
- ctx.strokeStyle = dividersOpts.lineColor || 'black';
430
- ctx.lineCap = dividersOpts.lineCapStyle;
431
- ctx.setLineDash(dividersOpts.lineDash || []);
432
- ctx.lineDashOffset = dividersOpts.lineDashOffset;
433
- ctx.lineWidth = dividersOpts.lineWidth;
434
- ctx.beginPath();
435
- if (w > h) {
436
- const w2 = w / 2;
437
- ctx.moveTo(rect.x + w2, rect.y);
438
- ctx.lineTo(rect.x + w2, rect.y + h);
439
- } else {
440
- const h2 = h / 2;
441
- ctx.moveTo(rect.x, rect.y + h2);
442
- ctx.lineTo(rect.x + w, rect.y + h2);
443
- }
444
- ctx.stroke();
445
- ctx.restore();
446
- }
447
-
448
- function buildData(dataset, mainRect, captions) {
839
+ function buildData(dataset, mainRect) {
449
840
  const key = dataset.key || '';
841
+ const treeLeafKey = dataset.treeLeafKey || '_leaf';
450
842
  let tree = dataset.tree || [];
843
+ if (isObject(tree)) {
844
+ tree = normalizeTreeToArray(key, treeLeafKey, tree);
845
+ }
451
846
  const groups = dataset.groups || [];
452
847
  const glen = groups.length;
453
- const sp = valueOrDefault(dataset.spacing, 0) + valueOrDefault(dataset.borderWidth, 0);
454
- const captionsFont = captions.font || {};
455
- const font = toFont(captionsFont);
848
+ const sp = valueOrDefault(dataset.spacing, 0);
849
+ const captions = dataset.captions || {display: true};
850
+ const font = toFont(captions.font);
456
851
  const padding = valueOrDefault(captions.padding, 3);
457
852
 
458
853
  function recur(gidx, rect, parent, gs) {
459
- const g = groups[gidx];
460
- const pg = (gidx > 0) && groups[gidx - 1];
461
- const gdata = group(tree, g, key, pg, parent);
854
+ const g = getGroupKey(groups[gidx]);
855
+ const pg = (gidx > 0) && getGroupKey(groups[gidx - 1]);
856
+ const gdata = group(tree, g, key, treeLeafKey, pg, parent, groups.filter((item, index) => index <= gidx));
462
857
  const gsq = squarify(gdata, rect, key, g, gidx, gs);
463
858
  const ret = gsq.slice();
464
859
  let subRect;
465
860
  if (gidx < glen - 1) {
466
861
  gsq.forEach((sq) => {
467
- subRect = {x: sq.x + sp, y: sq.y + sp, w: sq.w - 2 * sp, h: sq.h - 2 * sp};
468
- if (valueOrDefault(captions.display, true) && shouldDrawCaption(sq, font)) {
862
+ const bw = parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2);
863
+ subRect = {
864
+ x: sq.x + sp + bw.l,
865
+ y: sq.y + sp + bw.t,
866
+ w: sq.w - 2 * sp - bw.l - bw.r,
867
+ h: sq.h - 2 * sp - bw.t - bw.b,
868
+ rtl: rect.rtl
869
+ };
870
+ if (shouldDrawCaption(subRect, captions)) {
469
871
  subRect.y += font.lineHeight + padding * 2;
470
872
  subRect.h -= font.lineHeight + padding * 2;
471
873
  }
@@ -484,51 +886,19 @@ function buildData(dataset, mainRect, captions) {
484
886
  : squarify(tree, mainRect, key);
485
887
  }
486
888
 
487
- function drawLabels(ctx, item, rect) {
488
- const opts = rect.options;
489
- const labelsOpts = opts.labels;
490
- if (!labelsOpts || !labelsOpts.display) {
491
- return;
492
- }
493
- const optColor = (rect.active ? labelsOpts.hoverColor : labelsOpts.color) || labelsOpts.color;
494
- const optFont = (rect.active ? labelsOpts.hoverFont : labelsOpts.font) || labelsOpts.font;
495
- const font = toFont(optFont);
496
- const lh = font.lineHeight;
497
- const label = labelsOpts.formatter;
498
- if (label) {
499
- const labels = isArray(label) ? label : [label];
500
- const xyPoint = calculateXYLabel(opts, rect, labels, lh);
501
- ctx.font = font.string;
502
- ctx.textAlign = labelsOpts.align;
503
- ctx.textBaseline = labelsOpts.position;
504
- ctx.fillStyle = optColor;
505
- labels.forEach((l, i) => ctx.fillText(l, xyPoint.x, xyPoint.y + i * lh));
506
- }
507
- }
508
-
509
- function calculateXYLabel(options, rect, labels, lineHeight) {
510
- const labelsOpts = options.labels;
511
- const borderWidth = options.borderWidth || 0;
512
- const {align, position, padding} = labelsOpts;
513
- let x, y;
514
- x = calculateX(rect, align, padding, borderWidth);
515
- if (position === 'top') {
516
- y = rect.y + padding + borderWidth;
517
- } else if (position === 'bottom') {
518
- y = rect.y + rect.height - padding - borderWidth - (labels.length - 1) * lineHeight;
519
- } else {
520
- y = rect.y + rect.height / 2 - labels.length * lineHeight / 4;
521
- }
522
- return {x, y};
889
+ function getMinMax(data, useTree) {
890
+ const vMax = useTree ? 1 : maxValue(data);
891
+ const vMin = useTree ? 0 : minValue(data, vMax);
892
+ return {vMin, vMax};
523
893
  }
524
894
 
525
- function calculateX(rect, align, padding, borderWidth) {
526
- if (align === 'left') {
527
- return rect.x + padding + borderWidth;
528
- } else if (align === 'right') {
529
- return rect.x + rect.width - padding - borderWidth;
530
- }
531
- return rect.x + rect.width / 2;
895
+ function getArea({xScale, yScale}, data, rtl, useTree) {
896
+ const {vMin, vMax} = getMinMax(data, useTree);
897
+ const xMin = xScale.getPixelForValue(0);
898
+ const xMax = xScale.getPixelForValue(1);
899
+ const yMin = yScale.getPixelForValue(vMin);
900
+ const yMax = yScale.getPixelForValue(vMax);
901
+ return {x: xMin, y: yMax, w: xMax - xMin, h: yMin - yMax, rtl};
532
902
  }
533
903
 
534
904
  class TreemapController extends DatasetController {
@@ -538,6 +908,7 @@ class TreemapController extends DatasetController {
538
908
  this._rect = undefined;
539
909
  this._key = undefined;
540
910
  this._groups = undefined;
911
+ this._useTree = undefined;
541
912
  }
542
913
 
543
914
  initialize() {
@@ -545,24 +916,46 @@ class TreemapController extends DatasetController {
545
916
  super.initialize();
546
917
  }
547
918
 
919
+ /**
920
+ * TODO: to be removed when https://github.com/kurkle/chartjs-chart-treemap/issues/137
921
+ * will be implemented
922
+ */
923
+ updateRangeFromParsed(range, scale) {
924
+ if (range.updated) {
925
+ return;
926
+ }
927
+ range.updated = true;
928
+ if (scale.axis === 'x') {
929
+ range.min = 0;
930
+ range.max = 1;
931
+ return;
932
+ }
933
+ const me = this;
934
+ const dataset = me.getDataset();
935
+ const {vMin, vMax} = getMinMax(dataset.data, me._useTree);
936
+ range.min = vMin;
937
+ range.max = vMax;
938
+ }
939
+
548
940
  update(mode) {
549
941
  const me = this;
550
942
  const meta = me.getMeta();
551
943
  const dataset = me.getDataset();
944
+ if (!defined(me._useTree)) {
945
+ me._useTree = !!dataset.tree;
946
+ }
552
947
  const groups = dataset.groups || (dataset.groups = []);
553
- const captions = dataset.captions ? dataset.captions : {};
554
- const area = me.chart.chartArea;
555
948
  const key = dataset.key || '';
556
949
  const rtl = !!dataset.rtl;
557
950
 
558
- const mainRect = {x: area.left, y: area.top, w: area.right - area.left, h: area.bottom - area.top, rtl};
951
+ const mainRect = getArea(meta, dataset.data, rtl, me._useTree);
559
952
 
560
953
  if (mode === 'reset' || rectNotEqual(me._rect, mainRect) || me._key !== key || arrayNotEqual(me._groups, groups)) {
561
954
  me._rect = mainRect;
562
955
  me._groups = groups.slice();
563
956
  me._key = key;
564
957
 
565
- dataset.data = buildData(dataset, mainRect, captions);
958
+ dataset.data = buildData(dataset, mainRect);
566
959
  // @ts-ignore using private stuff
567
960
  me._dataCheck();
568
961
  // @ts-ignore using private stuff
@@ -572,13 +965,6 @@ class TreemapController extends DatasetController {
572
965
  me.updateElements(meta.data, 0, meta.data.length, mode);
573
966
  }
574
967
 
575
- resolveDataElementOptions(index, mode) {
576
- const options = super.resolveDataElementOptions(index, mode);
577
- const result = Object.isFrozen(options) ? Object.assign({}, options) : options;
578
- result.font = toFont(options.captions.font);
579
- return result;
580
- }
581
-
582
968
  updateElements(rects, start, count, mode) {
583
969
  const me = this;
584
970
  const reset = mode === 'reset';
@@ -609,41 +995,23 @@ class TreemapController extends DatasetController {
609
995
  me.updateSharedOptions(sharedOptions, mode, firstOpts);
610
996
  }
611
997
 
612
- _drawDividers(ctx, data, metadata) {
613
- for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
614
- const rect = metadata[i];
615
- const item = data[i];
616
- const dividersOpts = rect.options.dividers || {};
617
- if (dividersOpts.display && item._data.children.length > 1) {
618
- drawDivider(ctx, rect);
619
- }
620
- }
621
- }
622
-
623
- _drawRects(ctx, data, metadata, levels) {
624
- for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
625
- const rect = metadata[i];
626
- const item = data[i];
627
- if (!rect.hidden) {
628
- rect.draw(ctx);
629
- const opts = rect.options;
630
- if (shouldDrawCaption(rect, opts.captions.font)) {
631
- drawCaption(ctx, rect, item, opts, levels);
632
- }
633
- }
634
- }
635
- }
636
-
637
998
  draw() {
638
999
  const me = this;
639
1000
  const ctx = me.chart.ctx;
1001
+ const area = me.chart.chartArea;
640
1002
  const metadata = me.getMeta().data || [];
641
1003
  const dataset = me.getDataset();
642
1004
  const levels = (dataset.groups || []).length - 1;
643
- const data = dataset.data || [];
1005
+ const data = dataset.data;
644
1006
 
645
- me._drawRects(ctx, data, metadata, levels);
646
- me._drawDividers(ctx, data, metadata);
1007
+ clipArea(ctx, area);
1008
+ for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
1009
+ const rect = metadata[i];
1010
+ if (!rect.hidden) {
1011
+ rect.draw(ctx, data[i], levels);
1012
+ }
1013
+ }
1014
+ unclipArea(ctx);
647
1015
  }
648
1016
  }
649
1017
 
@@ -661,13 +1029,6 @@ TreemapController.defaults = {
661
1029
  },
662
1030
  },
663
1031
 
664
- borderWidth: 0,
665
- spacing: 0.5,
666
- dividers: {
667
- display: false,
668
- lineWidth: 1,
669
- }
670
-
671
1032
  };
672
1033
 
673
1034
  TreemapController.descriptors = {
@@ -698,7 +1059,7 @@ TreemapController.overrides = {
698
1059
  label(item) {
699
1060
  const dataset = item.dataset;
700
1061
  const dataItem = dataset.data[item.dataIndex];
701
- const label = dataItem.g || dataset.label;
1062
+ const label = dataItem.g || dataItem._data.label || dataset.label;
702
1063
  return (label ? label + ': ' : '') + dataItem.v;
703
1064
  }
704
1065
  }
@@ -733,6 +1094,8 @@ TreemapController.afterRegister = function() {
733
1094
 
734
1095
  return el.tooltipPosition();
735
1096
  };
1097
+ } else {
1098
+ console.warn('Unable to register the treemap positioner because tooltip plugin is not registered');
736
1099
  }
737
1100
  };
738
1101
 
@@ -743,181 +1106,4 @@ TreemapController.afterUnregister = function() {
743
1106
  }
744
1107
  };
745
1108
 
746
- /**
747
- * Helper function to get the bounds of the rect
748
- * @param {TreemapElement} rect the rect
749
- * @param {boolean} [useFinalPosition]
750
- * @return {object} bounds of the rect
751
- * @private
752
- */
753
- function getBounds(rect, useFinalPosition) {
754
- const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
755
- return {left: x, top: y, right: x + width, bottom: y + height};
756
- }
757
-
758
- function limit(value, min, max) {
759
- return Math.max(Math.min(value, max), min);
760
- }
761
-
762
- function parseBorderWidth(value, maxW, maxH) {
763
- let t, r, b, l;
764
-
765
- if (isObject(value)) {
766
- t = +value.top || 0;
767
- r = +value.right || 0;
768
- b = +value.bottom || 0;
769
- l = +value.left || 0;
770
- } else {
771
- t = r = b = l = +value || 0;
772
- }
773
-
774
- return {
775
- t: limit(t, 0, maxH),
776
- r: limit(r, 0, maxW),
777
- b: limit(b, 0, maxH),
778
- l: limit(l, 0, maxW)
779
- };
780
- }
781
-
782
- function boundingRects(rect) {
783
- const bounds = getBounds(rect);
784
- const width = bounds.right - bounds.left;
785
- const height = bounds.bottom - bounds.top;
786
- const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
787
-
788
- return {
789
- outer: {
790
- x: bounds.left,
791
- y: bounds.top,
792
- w: width,
793
- h: height
794
- },
795
- inner: {
796
- x: bounds.left + border.l,
797
- y: bounds.top + border.t,
798
- w: width - border.l - border.r,
799
- h: height - border.t - border.b
800
- }
801
- };
802
- }
803
-
804
- function inRange(rect, x, y, useFinalPosition) {
805
- const skipX = x === null;
806
- const skipY = y === null;
807
- const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition);
808
-
809
- return bounds
810
- && (skipX || x >= bounds.left && x <= bounds.right)
811
- && (skipY || y >= bounds.top && y <= bounds.bottom);
812
- }
813
-
814
- class TreemapElement extends Element {
815
-
816
- constructor(cfg) {
817
- super();
818
-
819
- this.options = undefined;
820
- this.width = undefined;
821
- this.height = undefined;
822
-
823
- if (cfg) {
824
- Object.assign(this, cfg);
825
- }
826
- }
827
-
828
- draw(ctx) {
829
- const options = this.options;
830
- const {inner, outer} = boundingRects(this);
831
-
832
- ctx.save();
833
-
834
- if (outer.w !== inner.w || outer.h !== inner.h) {
835
- ctx.beginPath();
836
- ctx.rect(outer.x, outer.y, outer.w, outer.h);
837
- ctx.clip();
838
- ctx.rect(inner.x, inner.y, inner.w, inner.h);
839
- ctx.fillStyle = options.backgroundColor;
840
- ctx.fill();
841
- ctx.fillStyle = options.borderColor;
842
- ctx.fill('evenodd');
843
- } else {
844
- ctx.fillStyle = options.backgroundColor;
845
- ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
846
- }
847
- ctx.restore();
848
- }
849
-
850
- inRange(mouseX, mouseY, useFinalPosition) {
851
- return inRange(this, mouseX, mouseY, useFinalPosition);
852
- }
853
-
854
- inXRange(mouseX, useFinalPosition) {
855
- return inRange(this, mouseX, null, useFinalPosition);
856
- }
857
-
858
- inYRange(mouseY, useFinalPosition) {
859
- return inRange(this, null, mouseY, useFinalPosition);
860
- }
861
-
862
- getCenterPoint(useFinalPosition) {
863
- const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
864
- return {
865
- x: x + width / 2,
866
- y: y + height / 2
867
- };
868
- }
869
-
870
- tooltipPosition() {
871
- return this.getCenterPoint();
872
- }
873
-
874
- getRange(axis) {
875
- return axis === 'x' ? this.width / 2 : this.height / 2;
876
- }
877
- }
878
-
879
- TreemapElement.id = 'treemap';
880
-
881
- TreemapElement.defaults = {
882
- borderWidth: undefined,
883
- spacing: undefined,
884
- label: undefined,
885
- rtl: undefined,
886
- dividers: {
887
- display: false,
888
- lineCapStyle: 'butt',
889
- lineColor: 'black',
890
- lineDash: undefined,
891
- lineDashOffset: 0,
892
- lineWidth: 0,
893
- },
894
- captions: {
895
- align: undefined,
896
- color: undefined,
897
- display: true,
898
- formatter: (ctx) => ctx.raw.g || '',
899
- font: {},
900
- padding: 3
901
- },
902
- labels: {
903
- align: 'center',
904
- color: undefined,
905
- display: false,
906
- formatter: (ctx) => ctx.raw.g ? [ctx.raw.g, ctx.raw.v] : ctx.raw.v,
907
- font: {},
908
- position: 'middle',
909
- padding: 3
910
- }
911
- };
912
-
913
- TreemapElement.descriptors = {
914
- _scriptable: true,
915
- _indexable: false
916
- };
917
-
918
- TreemapElement.defaultRoutes = {
919
- backgroundColor: 'backgroundColor',
920
- borderColor: 'borderColor'
921
- };
922
-
923
1109
  export { TreemapController, TreemapElement };