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