chartjs-chart-treemap 2.0.1 → 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.1
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.1";
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';
@@ -593,15 +979,14 @@ class TreemapController extends chart_js.DatasetController {
593
979
  for (let i = start; i < start + count; i++) {
594
980
  const sq = dataset.data[i];
595
981
  const options = sharedOptions || me.resolveDataElementOptions(i, mode);
596
- const height = reset ? 0 : sq.h - options.spacing * 2;
597
- const width = reset ? 0 : sq.w - options.spacing * 2;
598
- const x = sq.x + options.spacing;
599
- const y = sq.y + options.spacing;
982
+ const sp = options.spacing;
983
+ const sp2 = sp * 2;
600
984
  const properties = {
601
- x,
602
- y,
603
- width,
604
- height
985
+ x: sq.x + sp,
986
+ y: sq.y + sp,
987
+ width: reset ? 0 : sq.w - sp2,
988
+ height: reset ? 0 : sq.h - sp2,
989
+ hidden: sp2 > sq.w || sp2 > sq.h,
605
990
  };
606
991
 
607
992
  if (includeOptions) {
@@ -613,41 +998,23 @@ class TreemapController extends chart_js.DatasetController {
613
998
  me.updateSharedOptions(sharedOptions, mode, firstOpts);
614
999
  }
615
1000
 
616
- _drawDividers(ctx, data, metadata) {
617
- for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
618
- const rect = metadata[i];
619
- const item = data[i];
620
- const dividersOpts = rect.options.dividers || {};
621
- if (dividersOpts.display && item._data.children.length > 1) {
622
- drawDivider(ctx, rect);
623
- }
624
- }
625
- }
626
-
627
- _drawRects(ctx, data, metadata, levels) {
628
- for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
629
- const rect = metadata[i];
630
- const item = data[i];
631
- if (!rect.hidden) {
632
- rect.draw(ctx);
633
- const opts = rect.options;
634
- if (shouldDrawCaption(rect, opts.captions.font)) {
635
- drawCaption(ctx, rect, item, opts, levels);
636
- }
637
- }
638
- }
639
- }
640
-
641
1001
  draw() {
642
1002
  const me = this;
643
1003
  const ctx = me.chart.ctx;
1004
+ const area = me.chart.chartArea;
644
1005
  const metadata = me.getMeta().data || [];
645
1006
  const dataset = me.getDataset();
646
1007
  const levels = (dataset.groups || []).length - 1;
647
- const data = dataset.data || [];
1008
+ const data = dataset.data;
648
1009
 
649
- me._drawRects(ctx, data, metadata, levels);
650
- 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);
651
1018
  }
652
1019
  }
653
1020
 
@@ -665,13 +1032,6 @@ TreemapController.defaults = {
665
1032
  },
666
1033
  },
667
1034
 
668
- borderWidth: 0,
669
- spacing: 0.5,
670
- dividers: {
671
- display: false,
672
- lineWidth: 1,
673
- }
674
-
675
1035
  };
676
1036
 
677
1037
  TreemapController.descriptors = {
@@ -702,7 +1062,7 @@ TreemapController.overrides = {
702
1062
  label(item) {
703
1063
  const dataset = item.dataset;
704
1064
  const dataItem = dataset.data[item.dataIndex];
705
- const label = dataItem.g || dataset.label;
1065
+ const label = dataItem.g || dataItem._data.label || dataset.label;
706
1066
  return (label ? label + ': ' : '') + dataItem.v;
707
1067
  }
708
1068
  }
@@ -737,6 +1097,8 @@ TreemapController.afterRegister = function() {
737
1097
 
738
1098
  return el.tooltipPosition();
739
1099
  };
1100
+ } else {
1101
+ console.warn('Unable to register the treemap positioner because tooltip plugin is not registered');
740
1102
  }
741
1103
  };
742
1104
 
@@ -747,189 +1109,15 @@ TreemapController.afterUnregister = function() {
747
1109
  }
748
1110
  };
749
1111
 
750
- /**
751
- * Helper function to get the bounds of the rect
752
- * @param {TreemapElement} rect the rect
753
- * @param {boolean} [useFinalPosition]
754
- * @return {object} bounds of the rect
755
- * @private
756
- */
757
- function getBounds(rect, useFinalPosition) {
758
- const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
759
- return {left: x, top: y, right: x + width, bottom: y + height};
760
- }
761
-
762
- function limit(value, min, max) {
763
- return Math.max(Math.min(value, max), min);
764
- }
765
-
766
- function parseBorderWidth(value, maxW, maxH) {
767
- let t, r, b, l;
768
-
769
- if (isObject(value)) {
770
- t = +value.top || 0;
771
- r = +value.right || 0;
772
- b = +value.bottom || 0;
773
- l = +value.left || 0;
774
- } else {
775
- t = r = b = l = +value || 0;
776
- }
777
-
778
- return {
779
- t: limit(t, 0, maxH),
780
- r: limit(r, 0, maxW),
781
- b: limit(b, 0, maxH),
782
- l: limit(l, 0, maxW)
783
- };
784
- }
785
-
786
- function boundingRects(rect) {
787
- const bounds = getBounds(rect);
788
- const width = bounds.right - bounds.left;
789
- const height = bounds.bottom - bounds.top;
790
- const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
791
-
792
- return {
793
- outer: {
794
- x: bounds.left,
795
- y: bounds.top,
796
- w: width,
797
- h: height
798
- },
799
- inner: {
800
- x: bounds.left + border.l,
801
- y: bounds.top + border.t,
802
- w: width - border.l - border.r,
803
- h: height - border.t - border.b
804
- }
805
- };
806
- }
807
-
808
- function inRange(rect, x, y, useFinalPosition) {
809
- const skipX = x === null;
810
- const skipY = y === null;
811
- const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition);
812
-
813
- return bounds
814
- && (skipX || x >= bounds.left && x <= bounds.right)
815
- && (skipY || y >= bounds.top && y <= bounds.bottom);
816
- }
817
-
818
- class TreemapElement extends chart_js.Element {
819
-
820
- constructor(cfg) {
821
- super();
822
-
823
- this.options = undefined;
824
- this.width = undefined;
825
- this.height = undefined;
826
-
827
- if (cfg) {
828
- Object.assign(this, cfg);
829
- }
830
- }
831
-
832
- draw(ctx) {
833
- const options = this.options;
834
- const {inner, outer} = boundingRects(this);
835
-
836
- ctx.save();
837
-
838
- if (outer.w !== inner.w || outer.h !== inner.h) {
839
- ctx.beginPath();
840
- ctx.rect(outer.x, outer.y, outer.w, outer.h);
841
- ctx.clip();
842
- ctx.rect(inner.x, inner.y, inner.w, inner.h);
843
- ctx.fillStyle = options.backgroundColor;
844
- ctx.fill();
845
- ctx.fillStyle = options.borderColor;
846
- ctx.fill('evenodd');
847
- } else {
848
- ctx.fillStyle = options.backgroundColor;
849
- ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
850
- }
851
- ctx.restore();
852
- }
853
-
854
- inRange(mouseX, mouseY, useFinalPosition) {
855
- return inRange(this, mouseX, mouseY, useFinalPosition);
856
- }
857
-
858
- inXRange(mouseX, useFinalPosition) {
859
- return inRange(this, mouseX, null, useFinalPosition);
860
- }
861
-
862
- inYRange(mouseY, useFinalPosition) {
863
- return inRange(this, null, mouseY, useFinalPosition);
864
- }
865
-
866
- getCenterPoint(useFinalPosition) {
867
- const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
868
- return {
869
- x: x + width / 2,
870
- y: y + height / 2
871
- };
872
- }
873
-
874
- tooltipPosition() {
875
- return this.getCenterPoint();
876
- }
877
-
878
- getRange(axis) {
879
- return axis === 'x' ? this.width / 2 : this.height / 2;
880
- }
881
- }
882
-
883
- TreemapElement.id = 'treemap';
884
-
885
- TreemapElement.defaults = {
886
- borderWidth: undefined,
887
- spacing: undefined,
888
- label: undefined,
889
- rtl: undefined,
890
- dividers: {
891
- display: false,
892
- lineCapStyle: 'butt',
893
- lineColor: 'black',
894
- lineDash: undefined,
895
- lineDashOffset: 0,
896
- lineWidth: 0,
897
- },
898
- captions: {
899
- align: undefined,
900
- color: undefined,
901
- display: true,
902
- formatter: (ctx) => ctx.raw.g || '',
903
- font: {},
904
- padding: 3
905
- },
906
- labels: {
907
- align: 'center',
908
- color: undefined,
909
- display: false,
910
- formatter: (ctx) => ctx.raw.g ? [ctx.raw.g, ctx.raw.v] : ctx.raw.v,
911
- font: {},
912
- position: 'middle',
913
- padding: 3
914
- }
915
- };
916
-
917
- TreemapElement.descriptors = {
918
- _scriptable: true,
919
- _indexable: false
920
- };
921
-
922
- TreemapElement.defaultRoutes = {
923
- backgroundColor: 'backgroundColor',
924
- borderColor: 'borderColor'
925
- };
926
-
927
1112
  chart_js.Chart.register(TreemapController, TreemapElement);
928
1113
 
929
1114
  exports.flatten = flatten;
1115
+ exports.getGroupKey = getGroupKey;
930
1116
  exports.group = group;
931
1117
  exports.index = index;
932
- exports.isObject = isObject;
1118
+ exports.maxValue = maxValue;
1119
+ exports.minValue = minValue;
1120
+ exports.normalizeTreeToArray = normalizeTreeToArray;
933
1121
  exports.requireVersion = requireVersion;
934
1122
  exports.sort = sort;
935
1123
  exports.sum = sum;