chartjs-chart-treemap 2.0.2 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/chartjs-chart-treemap.esm.js +641 -419
- package/dist/chartjs-chart-treemap.js +644 -419
- package/dist/chartjs-chart-treemap.min.js +3 -3
- package/package.json +21 -16
- package/types/index.esm.d.ts +19 -10
|
@@ -1,11 +1,70 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-treemap v2.
|
|
2
|
+
* chartjs-chart-treemap v2.1.1
|
|
3
3
|
* https://chartjs-chart-treemap.pages.dev/
|
|
4
|
-
* (c)
|
|
4
|
+
* (c) 2022 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
|
-
import { Chart, registry, DatasetController
|
|
8
|
-
import { toFont, valueOrDefault,
|
|
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
|
|
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,35 +192,427 @@ function requireVersion(min, ver) {
|
|
|
115
192
|
}
|
|
116
193
|
}
|
|
117
194
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
195
|
+
const rasterize = (v, dpr) => Math.round(v * dpr) / dpr;
|
|
196
|
+
|
|
197
|
+
function rasterizeRect(rect, dpr) {
|
|
198
|
+
return {
|
|
199
|
+
...rect,
|
|
200
|
+
x: rasterize(rect.x, dpr),
|
|
201
|
+
y: rasterize(rect.y, dpr),
|
|
202
|
+
w: rasterize(rect.w, dpr),
|
|
203
|
+
h: rasterize(rect.h, dpr),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const widthCache = new Map();
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Helper function to get the bounds of the rect
|
|
211
|
+
* @param {TreemapElement} rect the rect
|
|
212
|
+
* @param {boolean} [useFinalPosition]
|
|
213
|
+
* @return {object} bounds of the rect
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
function getBounds(rect, useFinalPosition) {
|
|
217
|
+
const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
|
|
218
|
+
return {left: x, top: y, right: x + width, bottom: y + height};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function limit(value, min, max) {
|
|
222
|
+
return Math.max(Math.min(value, max), min);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function parseBorderWidth(value, maxW, maxH) {
|
|
226
|
+
const o = toTRBL(value);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
t: limit(o.top, 0, maxH),
|
|
230
|
+
r: limit(o.right, 0, maxW),
|
|
231
|
+
b: limit(o.bottom, 0, maxH),
|
|
232
|
+
l: limit(o.left, 0, maxW)
|
|
233
|
+
};
|
|
121
234
|
}
|
|
122
235
|
|
|
123
|
-
function
|
|
236
|
+
function parseBorderRadius(value, maxW, maxH) {
|
|
237
|
+
const o = toTRBLCorners(value);
|
|
238
|
+
const maxR = Math.min(maxW, maxH);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
topLeft: limit(o.topLeft, 0, maxR),
|
|
242
|
+
topRight: limit(o.topRight, 0, maxR),
|
|
243
|
+
bottomLeft: limit(o.bottomLeft, 0, maxR),
|
|
244
|
+
bottomRight: limit(o.bottomRight, 0, maxR)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function boundingRects(rect, dpr) {
|
|
249
|
+
const bounds = getBounds(rect);
|
|
250
|
+
const width = bounds.right - bounds.left;
|
|
251
|
+
const height = bounds.bottom - bounds.top;
|
|
252
|
+
const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
|
|
253
|
+
const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2);
|
|
254
|
+
const outer = rasterizeRect({
|
|
255
|
+
x: bounds.left,
|
|
256
|
+
y: bounds.top,
|
|
257
|
+
w: width,
|
|
258
|
+
h: height,
|
|
259
|
+
radius
|
|
260
|
+
}, dpr);
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
outer,
|
|
264
|
+
inner: {
|
|
265
|
+
x: outer.x + border.l,
|
|
266
|
+
y: outer.y + border.t,
|
|
267
|
+
w: outer.w - border.l - border.r,
|
|
268
|
+
h: outer.h - border.t - border.b,
|
|
269
|
+
radius: {
|
|
270
|
+
topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
|
|
271
|
+
topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
|
|
272
|
+
bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
|
|
273
|
+
bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function inRange(rect, x, y, useFinalPosition) {
|
|
280
|
+
const skipX = x === null;
|
|
281
|
+
const skipY = y === null;
|
|
282
|
+
const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition);
|
|
283
|
+
|
|
284
|
+
return bounds
|
|
285
|
+
&& (skipX || x >= bounds.left && x <= bounds.right)
|
|
286
|
+
&& (skipY || y >= bounds.top && y <= bounds.bottom);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function hasRadius(radius) {
|
|
290
|
+
return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Add a path of a rectangle to the current sub-path
|
|
295
|
+
* @param {CanvasRenderingContext2D} ctx Context
|
|
296
|
+
* @param {*} rect Bounding rect
|
|
297
|
+
*/
|
|
298
|
+
function addNormalRectPath(ctx, rect) {
|
|
299
|
+
ctx.rect(rect.x, rect.y, rect.w, rect.h);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function shouldDrawCaption(rect, options) {
|
|
303
|
+
if (!options || options.display === false) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const {w, h} = rect;
|
|
307
|
+
const font = toFont(options.font);
|
|
308
|
+
const min = font.lineHeight;
|
|
309
|
+
const padding = limit(valueOrDefault(options.padding, 3) * 2, 0, Math.min(w, h));
|
|
310
|
+
return (w - padding) > min && (h - padding) > min;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function drawText(ctx, rect, options, item, levels) {
|
|
314
|
+
const {captions, labels} = options;
|
|
315
|
+
ctx.save();
|
|
316
|
+
ctx.beginPath();
|
|
317
|
+
ctx.rect(rect.x, rect.y, rect.w, rect.h);
|
|
318
|
+
ctx.clip();
|
|
319
|
+
const isLeaf = (!('l' in item) || item.l === levels);
|
|
320
|
+
if (isLeaf && labels.display) {
|
|
321
|
+
drawLabel(ctx, rect, options);
|
|
322
|
+
} else if (!isLeaf && shouldDrawCaption(rect, captions)) {
|
|
323
|
+
drawCaption(ctx, rect, options, item);
|
|
324
|
+
}
|
|
325
|
+
ctx.restore();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function drawCaption(ctx, rect, options, item) {
|
|
329
|
+
const {captions, spacing, rtl} = options;
|
|
330
|
+
const {color, hoverColor, font, hoverFont, padding, align, formatter} = captions;
|
|
331
|
+
const oColor = (rect.active ? hoverColor : color) || color;
|
|
332
|
+
const oAlign = align || (rtl ? 'right' : 'left');
|
|
333
|
+
const optFont = (rect.active ? hoverFont : font) || font;
|
|
334
|
+
const oFont = toFont(optFont);
|
|
335
|
+
const lh = oFont.lineHeight / 2;
|
|
336
|
+
const x = calculateX(rect, oAlign, padding);
|
|
337
|
+
ctx.fillStyle = oColor;
|
|
338
|
+
ctx.font = oFont.string;
|
|
339
|
+
ctx.textAlign = oAlign;
|
|
340
|
+
ctx.textBaseline = 'middle';
|
|
341
|
+
ctx.fillText(formatter || item.g, x, rect.y + padding + spacing + lh);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function measureLabelSize(ctx, lines, fonts) {
|
|
345
|
+
const fontsKey = fonts.reduce(function(prev, item) {
|
|
346
|
+
prev += item.string;
|
|
347
|
+
return prev;
|
|
348
|
+
}, '');
|
|
349
|
+
const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
|
|
350
|
+
if (!widthCache.has(mapKey)) {
|
|
351
|
+
ctx.save();
|
|
352
|
+
const count = lines.length;
|
|
353
|
+
let width = 0;
|
|
354
|
+
let height = 0;
|
|
355
|
+
for (let i = 0; i < count; i++) {
|
|
356
|
+
const font = fonts[Math.min(i, fonts.length - 1)];
|
|
357
|
+
ctx.font = font.string;
|
|
358
|
+
const text = lines[i];
|
|
359
|
+
width = Math.max(width, ctx.measureText(text).width);
|
|
360
|
+
height += font.lineHeight;
|
|
361
|
+
}
|
|
362
|
+
ctx.restore();
|
|
363
|
+
widthCache.set(mapKey, {width, height});
|
|
364
|
+
}
|
|
365
|
+
return widthCache.get(mapKey);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function labelToDraw(ctx, rect, options, labelSize) {
|
|
369
|
+
const {overflow, padding} = options;
|
|
370
|
+
const {width, height} = labelSize;
|
|
371
|
+
if (overflow === 'hidden') {
|
|
372
|
+
return !((width + padding * 2) > rect.w || (height + padding * 2) > rect.h);
|
|
373
|
+
}
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function drawLabel(ctx, rect, options) {
|
|
378
|
+
const labels = options.labels;
|
|
379
|
+
const content = labels.formatter;
|
|
380
|
+
if (!content) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const contents = isArray(content) ? content : [content];
|
|
384
|
+
const {font, hoverFont} = labels;
|
|
385
|
+
const optFont = (rect.active ? hoverFont : font) || font;
|
|
386
|
+
const fonts = isArray(optFont) ? optFont.map(f => toFont(f)) : [toFont(optFont)];
|
|
387
|
+
const labelSize = measureLabelSize(ctx, contents, fonts);
|
|
388
|
+
if (!labelToDraw(ctx, rect, labels, labelSize)) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const {color, hoverColor, align} = labels;
|
|
392
|
+
const optColor = (rect.active ? hoverColor : color) || color;
|
|
393
|
+
const colors = isArray(optColor) ? optColor : [optColor];
|
|
394
|
+
const xyPoint = calculateXYLabel(rect, labels, labelSize);
|
|
395
|
+
ctx.textAlign = align;
|
|
396
|
+
ctx.textBaseline = 'middle';
|
|
397
|
+
let lhs = 0;
|
|
398
|
+
contents.forEach(function(l, i) {
|
|
399
|
+
const c = colors[Math.min(i, colors.length - 1)];
|
|
400
|
+
const f = fonts[Math.min(i, fonts.length - 1)];
|
|
401
|
+
const lh = f.lineHeight;
|
|
402
|
+
ctx.font = f.string;
|
|
403
|
+
ctx.fillStyle = c;
|
|
404
|
+
ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs);
|
|
405
|
+
lhs += lh;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function drawDivider(ctx, rect, options, item) {
|
|
410
|
+
const dividers = options.dividers;
|
|
411
|
+
if (!dividers.display || !item._data.children.length) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const {x, y, w, h} = rect;
|
|
415
|
+
const {lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth} = dividers;
|
|
416
|
+
ctx.save();
|
|
417
|
+
ctx.strokeStyle = lineColor;
|
|
418
|
+
ctx.lineCap = lineCapStyle;
|
|
419
|
+
ctx.setLineDash(lineDash);
|
|
420
|
+
ctx.lineDashOffset = lineDashOffset;
|
|
421
|
+
ctx.lineWidth = lineWidth;
|
|
422
|
+
ctx.beginPath();
|
|
423
|
+
if (w > h) {
|
|
424
|
+
const w2 = w / 2;
|
|
425
|
+
ctx.moveTo(x + w2, y);
|
|
426
|
+
ctx.lineTo(x + w2, y + h);
|
|
427
|
+
} else {
|
|
428
|
+
const h2 = h / 2;
|
|
429
|
+
ctx.moveTo(x, y + h2);
|
|
430
|
+
ctx.lineTo(x + w, y + h2);
|
|
431
|
+
}
|
|
432
|
+
ctx.stroke();
|
|
433
|
+
ctx.restore();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function calculateXYLabel(rect, options, labelSize) {
|
|
437
|
+
const {align, position, padding} = options;
|
|
438
|
+
let x, y;
|
|
439
|
+
x = calculateX(rect, align, padding);
|
|
440
|
+
if (position === 'top') {
|
|
441
|
+
y = rect.y + padding;
|
|
442
|
+
} else if (position === 'bottom') {
|
|
443
|
+
y = rect.y + rect.h - padding - labelSize.height;
|
|
444
|
+
} else {
|
|
445
|
+
y = rect.y + (rect.h - labelSize.height) / 2 + padding;
|
|
446
|
+
}
|
|
447
|
+
return {x, y};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function calculateX(rect, align, padding) {
|
|
451
|
+
if (align === 'left') {
|
|
452
|
+
return rect.x + padding;
|
|
453
|
+
} else if (align === 'right') {
|
|
454
|
+
return rect.x + rect.w - padding;
|
|
455
|
+
}
|
|
456
|
+
return rect.x + rect.w / 2;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
class TreemapElement extends Element {
|
|
460
|
+
|
|
461
|
+
constructor(cfg) {
|
|
462
|
+
super();
|
|
463
|
+
|
|
464
|
+
this.options = undefined;
|
|
465
|
+
this.width = undefined;
|
|
466
|
+
this.height = undefined;
|
|
467
|
+
|
|
468
|
+
if (cfg) {
|
|
469
|
+
Object.assign(this, cfg);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
draw(ctx, data, levels = 0, dpr) {
|
|
474
|
+
if (!data) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const options = this.options;
|
|
478
|
+
const {inner, outer} = boundingRects(this, dpr);
|
|
479
|
+
|
|
480
|
+
const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
|
|
481
|
+
|
|
482
|
+
ctx.save();
|
|
483
|
+
|
|
484
|
+
if (outer.w !== inner.w || outer.h !== inner.h) {
|
|
485
|
+
ctx.beginPath();
|
|
486
|
+
addRectPath(ctx, outer);
|
|
487
|
+
ctx.clip();
|
|
488
|
+
addRectPath(ctx, inner);
|
|
489
|
+
ctx.fillStyle = options.borderColor;
|
|
490
|
+
ctx.fill('evenodd');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
ctx.beginPath();
|
|
494
|
+
addRectPath(ctx, inner);
|
|
495
|
+
ctx.fillStyle = options.backgroundColor;
|
|
496
|
+
ctx.fill();
|
|
497
|
+
|
|
498
|
+
drawDivider(ctx, inner, options, data);
|
|
499
|
+
drawText(ctx, inner, options, data, levels);
|
|
500
|
+
ctx.restore();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
inRange(mouseX, mouseY, useFinalPosition) {
|
|
504
|
+
return inRange(this, mouseX, mouseY, useFinalPosition);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
inXRange(mouseX, useFinalPosition) {
|
|
508
|
+
return inRange(this, mouseX, null, useFinalPosition);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
inYRange(mouseY, useFinalPosition) {
|
|
512
|
+
return inRange(this, null, mouseY, useFinalPosition);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
getCenterPoint(useFinalPosition) {
|
|
516
|
+
const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
|
|
517
|
+
return {
|
|
518
|
+
x: x + width / 2,
|
|
519
|
+
y: y + height / 2
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
tooltipPosition() {
|
|
524
|
+
return this.getCenterPoint();
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* @todo: remove this unused function in v3
|
|
529
|
+
*/
|
|
530
|
+
getRange(axis) {
|
|
531
|
+
return axis === 'x' ? this.width / 2 : this.height / 2;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
TreemapElement.id = 'treemap';
|
|
536
|
+
|
|
537
|
+
TreemapElement.defaults = {
|
|
538
|
+
label: undefined,
|
|
539
|
+
borderRadius: 0,
|
|
540
|
+
borderWidth: 0,
|
|
541
|
+
captions: {
|
|
542
|
+
align: undefined,
|
|
543
|
+
color: 'black',
|
|
544
|
+
display: true,
|
|
545
|
+
font: {},
|
|
546
|
+
formatter: (ctx) => ctx.raw.g || ctx.raw._data.label || '',
|
|
547
|
+
padding: 3
|
|
548
|
+
},
|
|
549
|
+
dividers: {
|
|
550
|
+
display: false,
|
|
551
|
+
lineCapStyle: 'butt',
|
|
552
|
+
lineColor: 'black',
|
|
553
|
+
lineDash: [],
|
|
554
|
+
lineDashOffset: 0,
|
|
555
|
+
lineWidth: 1,
|
|
556
|
+
},
|
|
557
|
+
labels: {
|
|
558
|
+
align: 'center',
|
|
559
|
+
color: 'black',
|
|
560
|
+
display: false,
|
|
561
|
+
font: {},
|
|
562
|
+
formatter(ctx) {
|
|
563
|
+
if (ctx.raw.g) {
|
|
564
|
+
return [ctx.raw.g, ctx.raw.v + ''];
|
|
565
|
+
}
|
|
566
|
+
return ctx.raw._data.label ? [ctx.raw._data.label, ctx.raw.v + ''] : ctx.raw.v + '';
|
|
567
|
+
},
|
|
568
|
+
overflow: 'cut',
|
|
569
|
+
position: 'middle',
|
|
570
|
+
padding: 3
|
|
571
|
+
},
|
|
572
|
+
rtl: false,
|
|
573
|
+
spacing: 0.5
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
TreemapElement.descriptors = {
|
|
577
|
+
labels: {
|
|
578
|
+
_fallback: true
|
|
579
|
+
},
|
|
580
|
+
captions: {
|
|
581
|
+
_fallback: true
|
|
582
|
+
},
|
|
583
|
+
_scriptable: true,
|
|
584
|
+
_indexable: false
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
TreemapElement.defaultRoutes = {
|
|
588
|
+
backgroundColor: 'backgroundColor',
|
|
589
|
+
borderColor: 'borderColor'
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
function getDims(itm, w2, s2, key, dpr, maxd2) {
|
|
124
593
|
const a = itm._normalized;
|
|
125
594
|
const ar = w2 * a / s2;
|
|
126
|
-
const d1 = Math.sqrt(a * ar);
|
|
127
|
-
const d2 = a / d1;
|
|
595
|
+
const d1 = rasterize(Math.sqrt(a * ar), dpr);
|
|
596
|
+
const d2 = Math.min(rasterize(a / d1, dpr), maxd2);
|
|
128
597
|
const w = key === '_ix' ? d1 : d2;
|
|
129
598
|
const h = key === '_ix' ? d2 : d1;
|
|
130
599
|
|
|
131
600
|
return {d1, d2, w, h};
|
|
132
601
|
}
|
|
133
602
|
|
|
134
|
-
const getX = (rect, w) =>
|
|
603
|
+
const getX = (rect, w) => rect.rtl ? rect.x + rect.iw - w : rect.x + rect._ix;
|
|
135
604
|
|
|
136
|
-
function buildRow(rect, itm, dims, sum) {
|
|
137
|
-
const r = {
|
|
605
|
+
function buildRow(rect, itm, dims, sum, dpr) {
|
|
606
|
+
const r = rasterizeRect({
|
|
138
607
|
x: getX(rect, dims.w),
|
|
139
|
-
y:
|
|
140
|
-
w:
|
|
141
|
-
h:
|
|
142
|
-
a:
|
|
608
|
+
y: rect.y + rect._iy,
|
|
609
|
+
w: dims.w,
|
|
610
|
+
h: dims.h,
|
|
611
|
+
a: itm._normalized,
|
|
143
612
|
v: itm.value,
|
|
144
613
|
s: sum,
|
|
145
614
|
_data: itm._data
|
|
146
|
-
};
|
|
615
|
+
}, dpr);
|
|
147
616
|
if (itm.group) {
|
|
148
617
|
r.g = itm.group;
|
|
149
618
|
r.l = itm.level;
|
|
@@ -153,16 +622,16 @@ function buildRow(rect, itm, dims, sum) {
|
|
|
153
622
|
}
|
|
154
623
|
|
|
155
624
|
class Rect {
|
|
156
|
-
constructor(r) {
|
|
157
|
-
|
|
625
|
+
constructor(r, dpr) {
|
|
626
|
+
this.dpr = dpr;
|
|
158
627
|
r = r || {w: 1, h: 1};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
628
|
+
this.rtl = !!r.rtl;
|
|
629
|
+
this.x = r.x || r.left || 0;
|
|
630
|
+
this.y = r.y || r.top || 0;
|
|
631
|
+
this._ix = 0;
|
|
632
|
+
this._iy = 0;
|
|
633
|
+
this.w = r.w || r.width || (r.right - r.left);
|
|
634
|
+
this.h = r.h || r.height || (r.bottom - r.top);
|
|
166
635
|
}
|
|
167
636
|
|
|
168
637
|
get area() {
|
|
@@ -183,34 +652,61 @@ class Rect {
|
|
|
183
652
|
}
|
|
184
653
|
|
|
185
654
|
get side() {
|
|
186
|
-
return this.dir === 'x' ? this.iw : this.ih;
|
|
655
|
+
return rasterize(this.dir === 'x' ? this.iw : this.ih, this.dpr);
|
|
187
656
|
}
|
|
188
657
|
|
|
189
658
|
map(arr) {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
659
|
+
const {dir, side, dpr} = this;
|
|
660
|
+
const {key, d2prop, id2prop, d2key} = getPropNames(dir);
|
|
192
661
|
const sum = arr.nsum;
|
|
193
662
|
const row = arr.get();
|
|
194
|
-
const dir = me.dir;
|
|
195
|
-
const side = me.side;
|
|
196
663
|
const w2 = side * side;
|
|
197
|
-
const
|
|
664
|
+
const availd2 = rasterize(this[id2prop], dpr);
|
|
198
665
|
const s2 = sum * sum;
|
|
666
|
+
const ret = [];
|
|
199
667
|
let maxd2 = 0;
|
|
200
668
|
let totd1 = 0;
|
|
201
669
|
for (const itm of row) {
|
|
202
|
-
const dims = getDims(itm, w2, s2, key);
|
|
670
|
+
const dims = getDims(itm, w2, s2, key, dpr, availd2);
|
|
203
671
|
totd1 += dims.d1;
|
|
204
|
-
maxd2 = Math.max(maxd2, dims.d2);
|
|
205
|
-
ret.push(buildRow(
|
|
206
|
-
|
|
672
|
+
maxd2 = Math.min(Math.max(maxd2, dims.d2), availd2);
|
|
673
|
+
ret.push(buildRow(this, itm, dims, arr.sum, dpr));
|
|
674
|
+
this[key] += dims.d1;
|
|
207
675
|
}
|
|
208
|
-
|
|
209
|
-
|
|
676
|
+
// make sure all rects are same size in d2 direction
|
|
677
|
+
for (const r of ret) {
|
|
678
|
+
const d2 = r[d2prop];
|
|
679
|
+
if (d2 !== maxd2) {
|
|
680
|
+
if (this.rtl && dir === 'y') {
|
|
681
|
+
// for RTL, x-coordinate needs to be adjusted too
|
|
682
|
+
r.x -= maxd2 - d2;
|
|
683
|
+
}
|
|
684
|
+
r[d2prop] = maxd2;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
this[d2key] += maxd2;
|
|
688
|
+
this[key] -= totd1;
|
|
210
689
|
return ret;
|
|
211
690
|
}
|
|
212
691
|
}
|
|
213
692
|
|
|
693
|
+
function getPropNames(dir) {
|
|
694
|
+
if (dir === 'x') {
|
|
695
|
+
return {
|
|
696
|
+
key: '_ix',
|
|
697
|
+
d2prop: 'h',
|
|
698
|
+
id2prop: 'ih',
|
|
699
|
+
d2key: '_iy'
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
key: '_iy',
|
|
704
|
+
d2prop: 'w',
|
|
705
|
+
id2prop: 'iw',
|
|
706
|
+
d2key: '_ix'
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
|
|
214
710
|
const min = Math.min;
|
|
215
711
|
const max = Math.max;
|
|
216
712
|
|
|
@@ -308,15 +804,16 @@ function compareAspectRatio(oldStat, newStat, args) {
|
|
|
308
804
|
*
|
|
309
805
|
* @param {number[]|object[]} values
|
|
310
806
|
* @param {object} rectangle
|
|
311
|
-
* @param {string} key
|
|
312
|
-
* @param {
|
|
313
|
-
* @param {*}
|
|
314
|
-
* @param {*}
|
|
807
|
+
* @param {string} [key]
|
|
808
|
+
* @param {number} [dpr=1]
|
|
809
|
+
* @param {*} [grp]
|
|
810
|
+
* @param {*} [lvl]
|
|
811
|
+
* @param {*} [gsum]
|
|
315
812
|
*/
|
|
316
|
-
function squarify(values, rectangle, key, grp, lvl, gsum) {
|
|
813
|
+
function squarify(values, rectangle, key, dpr = 1, grp, lvl, gsum) {
|
|
317
814
|
values = values || [];
|
|
318
815
|
const rows = [];
|
|
319
|
-
const rect = new Rect(rectangle);
|
|
816
|
+
const rect = new Rect(rectangle, dpr);
|
|
320
817
|
const row = new StatArray('value', rect.area / sum(values, key));
|
|
321
818
|
let length = rect.side;
|
|
322
819
|
const n = values.length;
|
|
@@ -353,7 +850,7 @@ function squarify(values, rectangle, key, grp, lvl, gsum) {
|
|
|
353
850
|
return flatten(rows);
|
|
354
851
|
}
|
|
355
852
|
|
|
356
|
-
var version = "2.
|
|
853
|
+
var version = "2.1.1";
|
|
357
854
|
|
|
358
855
|
function rectNotEqual(r1, r2) {
|
|
359
856
|
return !r1 || !r2
|
|
@@ -378,98 +875,41 @@ function arrayNotEqual(a1, a2) {
|
|
|
378
875
|
return false;
|
|
379
876
|
}
|
|
380
877
|
|
|
381
|
-
function
|
|
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) {
|
|
878
|
+
function buildData(dataset, mainRect, dpr) {
|
|
449
879
|
const key = dataset.key || '';
|
|
880
|
+
const treeLeafKey = dataset.treeLeafKey || '_leaf';
|
|
450
881
|
let tree = dataset.tree || [];
|
|
882
|
+
if (isObject(tree)) {
|
|
883
|
+
tree = normalizeTreeToArray(key, treeLeafKey, tree);
|
|
884
|
+
}
|
|
451
885
|
const groups = dataset.groups || [];
|
|
452
886
|
const glen = groups.length;
|
|
453
|
-
const sp = valueOrDefault(dataset.spacing, 0)
|
|
454
|
-
const
|
|
455
|
-
const font = toFont(
|
|
887
|
+
const sp = valueOrDefault(dataset.spacing, 0);
|
|
888
|
+
const captions = dataset.captions || {};
|
|
889
|
+
const font = toFont(captions.font);
|
|
456
890
|
const padding = valueOrDefault(captions.padding, 3);
|
|
457
891
|
|
|
458
892
|
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);
|
|
462
|
-
const gsq = squarify(gdata, rect, key, g, gidx, gs);
|
|
893
|
+
const g = getGroupKey(groups[gidx]);
|
|
894
|
+
const pg = (gidx > 0) && getGroupKey(groups[gidx - 1]);
|
|
895
|
+
const gdata = group(tree, g, key, treeLeafKey, pg, parent, groups.filter((item, index) => index <= gidx));
|
|
896
|
+
const gsq = squarify(gdata, rect, key, dpr, g, gidx, gs);
|
|
463
897
|
const ret = gsq.slice();
|
|
464
|
-
let subRect;
|
|
465
898
|
if (gidx < glen - 1) {
|
|
466
899
|
gsq.forEach((sq) => {
|
|
467
|
-
|
|
468
|
-
|
|
900
|
+
const bw = parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2);
|
|
901
|
+
const subRect = {
|
|
902
|
+
...rect,
|
|
903
|
+
x: sq.x + sp + bw.l,
|
|
904
|
+
y: sq.y + sp + bw.t,
|
|
905
|
+
w: sq.w - 2 * sp - bw.l - bw.r,
|
|
906
|
+
h: sq.h - 2 * sp - bw.t - bw.b,
|
|
907
|
+
};
|
|
908
|
+
if (shouldDrawCaption(subRect, captions)) {
|
|
469
909
|
subRect.y += font.lineHeight + padding * 2;
|
|
470
910
|
subRect.h -= font.lineHeight + padding * 2;
|
|
471
911
|
}
|
|
472
|
-
ret.push(...recur(gidx + 1, subRect, sq.g, sq.s));
|
|
912
|
+
ret.push(...recur(gidx + 1, rasterizeRect(subRect, dpr), sq.g, sq.s));
|
|
473
913
|
});
|
|
474
914
|
}
|
|
475
915
|
return ret;
|
|
@@ -481,54 +921,22 @@ function buildData(dataset, mainRect, captions) {
|
|
|
481
921
|
|
|
482
922
|
return glen
|
|
483
923
|
? recur(0, mainRect)
|
|
484
|
-
: squarify(tree, mainRect, key);
|
|
924
|
+
: squarify(tree, mainRect, key, dpr);
|
|
485
925
|
}
|
|
486
926
|
|
|
487
|
-
function
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
|
|
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};
|
|
927
|
+
function getMinMax(data, useTree) {
|
|
928
|
+
const vMax = useTree ? 1 : maxValue(data);
|
|
929
|
+
const vMin = useTree ? 0 : minValue(data, vMax);
|
|
930
|
+
return {vMin, vMax};
|
|
523
931
|
}
|
|
524
932
|
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return
|
|
933
|
+
function getArea({xScale, yScale}, data, rtl, useTree) {
|
|
934
|
+
const {vMin, vMax} = getMinMax(data, useTree);
|
|
935
|
+
const xMin = xScale.getPixelForValue(0);
|
|
936
|
+
const xMax = xScale.getPixelForValue(1);
|
|
937
|
+
const yMin = yScale.getPixelForValue(vMin);
|
|
938
|
+
const yMax = yScale.getPixelForValue(vMax);
|
|
939
|
+
return {x: xMin, y: yMax, w: xMax - xMin, h: yMin - yMax, rtl};
|
|
532
940
|
}
|
|
533
941
|
|
|
534
942
|
class TreemapController extends DatasetController {
|
|
@@ -538,6 +946,7 @@ class TreemapController extends DatasetController {
|
|
|
538
946
|
this._rect = undefined;
|
|
539
947
|
this._key = undefined;
|
|
540
948
|
this._groups = undefined;
|
|
949
|
+
this._useTree = undefined;
|
|
541
950
|
}
|
|
542
951
|
|
|
543
952
|
initialize() {
|
|
@@ -545,38 +954,52 @@ class TreemapController extends DatasetController {
|
|
|
545
954
|
super.initialize();
|
|
546
955
|
}
|
|
547
956
|
|
|
957
|
+
/**
|
|
958
|
+
* @todo: Remove with https://github.com/kurkle/chartjs-chart-treemap/issues/137
|
|
959
|
+
*/
|
|
960
|
+
updateRangeFromParsed(range, scale) {
|
|
961
|
+
if (range.updated) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
range.updated = true;
|
|
965
|
+
if (scale.axis === 'x') {
|
|
966
|
+
range.min = 0;
|
|
967
|
+
range.max = 1;
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
const dataset = this.getDataset();
|
|
971
|
+
const {vMin, vMax} = getMinMax(dataset.data, this._useTree);
|
|
972
|
+
range.min = vMin;
|
|
973
|
+
range.max = vMax;
|
|
974
|
+
}
|
|
975
|
+
|
|
548
976
|
update(mode) {
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
const
|
|
977
|
+
const meta = this.getMeta();
|
|
978
|
+
const dataset = this.getDataset();
|
|
979
|
+
const dpr = this.chart.currentDevicePixelRatio;
|
|
980
|
+
|
|
981
|
+
if (!defined(this._useTree)) {
|
|
982
|
+
this._useTree = !!dataset.tree;
|
|
983
|
+
}
|
|
552
984
|
const groups = dataset.groups || (dataset.groups = []);
|
|
553
|
-
const captions = dataset.captions ? dataset.captions : {};
|
|
554
|
-
const area = me.chart.chartArea;
|
|
555
985
|
const key = dataset.key || '';
|
|
556
986
|
const rtl = !!dataset.rtl;
|
|
557
987
|
|
|
558
|
-
const mainRect =
|
|
988
|
+
const mainRect = rasterizeRect(getArea(meta, dataset.data, rtl, this._useTree), dpr);
|
|
559
989
|
|
|
560
|
-
if (mode === 'reset' || rectNotEqual(
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
990
|
+
if (mode === 'reset' || rectNotEqual(this._rect, mainRect) || this._key !== key || arrayNotEqual(this._groups, groups)) {
|
|
991
|
+
this._rect = mainRect;
|
|
992
|
+
this._groups = groups.slice();
|
|
993
|
+
this._key = key;
|
|
564
994
|
|
|
565
|
-
dataset.data = buildData(dataset, mainRect,
|
|
995
|
+
dataset.data = buildData(dataset, mainRect, dpr);
|
|
566
996
|
// @ts-ignore using private stuff
|
|
567
|
-
|
|
997
|
+
this._dataCheck();
|
|
568
998
|
// @ts-ignore using private stuff
|
|
569
|
-
|
|
999
|
+
this._resyncElements();
|
|
570
1000
|
}
|
|
571
1001
|
|
|
572
|
-
|
|
573
|
-
}
|
|
574
|
-
|
|
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;
|
|
1002
|
+
this.updateElements(meta.data, 0, meta.data.length, mode);
|
|
580
1003
|
}
|
|
581
1004
|
|
|
582
1005
|
updateElements(rects, start, count, mode) {
|
|
@@ -609,41 +1032,21 @@ class TreemapController extends DatasetController {
|
|
|
609
1032
|
me.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
610
1033
|
}
|
|
611
1034
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
drawDivider(ctx, rect);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
1035
|
+
draw() {
|
|
1036
|
+
const {ctx, chartArea, currentDevicePixelRatio: dpr} = this.chart;
|
|
1037
|
+
const metadata = this.getMeta().data || [];
|
|
1038
|
+
const dataset = this.getDataset();
|
|
1039
|
+
const levels = (dataset.groups || []).length - 1;
|
|
1040
|
+
const data = dataset.data;
|
|
622
1041
|
|
|
623
|
-
|
|
1042
|
+
clipArea(ctx, chartArea);
|
|
624
1043
|
for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
|
|
625
1044
|
const rect = metadata[i];
|
|
626
|
-
const item = data[i];
|
|
627
1045
|
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
|
-
}
|
|
1046
|
+
rect.draw(ctx, data[i], levels, dpr);
|
|
633
1047
|
}
|
|
634
1048
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
draw() {
|
|
638
|
-
const me = this;
|
|
639
|
-
const ctx = me.chart.ctx;
|
|
640
|
-
const metadata = me.getMeta().data || [];
|
|
641
|
-
const dataset = me.getDataset();
|
|
642
|
-
const levels = (dataset.groups || []).length - 1;
|
|
643
|
-
const data = dataset.data || [];
|
|
644
|
-
|
|
645
|
-
me._drawRects(ctx, data, metadata, levels);
|
|
646
|
-
me._drawDividers(ctx, data, metadata);
|
|
1049
|
+
unclipArea(ctx);
|
|
647
1050
|
}
|
|
648
1051
|
}
|
|
649
1052
|
|
|
@@ -661,13 +1064,6 @@ TreemapController.defaults = {
|
|
|
661
1064
|
},
|
|
662
1065
|
},
|
|
663
1066
|
|
|
664
|
-
borderWidth: 0,
|
|
665
|
-
spacing: 0.5,
|
|
666
|
-
dividers: {
|
|
667
|
-
display: false,
|
|
668
|
-
lineWidth: 1,
|
|
669
|
-
}
|
|
670
|
-
|
|
671
1067
|
};
|
|
672
1068
|
|
|
673
1069
|
TreemapController.descriptors = {
|
|
@@ -678,6 +1074,7 @@ TreemapController.descriptors = {
|
|
|
678
1074
|
TreemapController.overrides = {
|
|
679
1075
|
interaction: {
|
|
680
1076
|
mode: 'point',
|
|
1077
|
+
includeInvisible: true,
|
|
681
1078
|
intersect: true
|
|
682
1079
|
},
|
|
683
1080
|
|
|
@@ -698,7 +1095,7 @@ TreemapController.overrides = {
|
|
|
698
1095
|
label(item) {
|
|
699
1096
|
const dataset = item.dataset;
|
|
700
1097
|
const dataItem = dataset.data[item.dataIndex];
|
|
701
|
-
const label = dataItem.g || dataset.label;
|
|
1098
|
+
const label = dataItem.g || dataItem._data.label || dataset.label;
|
|
702
1099
|
return (label ? label + ': ' : '') + dataItem.v;
|
|
703
1100
|
}
|
|
704
1101
|
}
|
|
@@ -717,7 +1114,7 @@ TreemapController.overrides = {
|
|
|
717
1114
|
};
|
|
718
1115
|
|
|
719
1116
|
TreemapController.beforeRegister = function() {
|
|
720
|
-
requireVersion('3.
|
|
1117
|
+
requireVersion('3.8', Chart.version);
|
|
721
1118
|
};
|
|
722
1119
|
|
|
723
1120
|
TreemapController.afterRegister = function() {
|
|
@@ -733,6 +1130,8 @@ TreemapController.afterRegister = function() {
|
|
|
733
1130
|
|
|
734
1131
|
return el.tooltipPosition();
|
|
735
1132
|
};
|
|
1133
|
+
} else {
|
|
1134
|
+
console.warn('Unable to register the treemap positioner because tooltip plugin is not registered');
|
|
736
1135
|
}
|
|
737
1136
|
};
|
|
738
1137
|
|
|
@@ -743,181 +1142,4 @@ TreemapController.afterUnregister = function() {
|
|
|
743
1142
|
}
|
|
744
1143
|
};
|
|
745
1144
|
|
|
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
1145
|
export { TreemapController, TreemapElement };
|