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