chartjs-chart-treemap
Version:
Chart.js module for creating treemap charts
1,188 lines (1,046 loc) • 29.6 kB
JavaScript
/*!
* chartjs-chart-treemap v2.3.0
* https://chartjs-chart-treemap.pages.dev/
* (c) 2023 Jukka Kurkela
* Released under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('chart.js'), require('chart.js/helpers')) :
typeof define === 'function' && define.amd ? define(['exports', 'chart.js', 'chart.js/helpers'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["chartjs-chart-treemap"] = {}, global.Chart, global.Chart.helpers));
})(this, (function (exports, chart_js, helpers) { 'use strict';
const isOlderPart = (act, req) => req > act || (act.length > req.length && act.slice(0, req.length) === req);
const getGroupKey = (lvl) => '' + lvl;
function scanTreeObject(keys, treeLeafKey, obj, tree = [], lvl = 0, result = []) {
const objIndex = lvl - 1;
if (keys[0] in obj && lvl > 0) {
const record = tree.reduce(function(reduced, item, i) {
if (i !== objIndex) {
reduced[getGroupKey(i)] = item;
}
return reduced;
}, {});
record[treeLeafKey] = tree[objIndex];
keys.forEach(function(k) {
record[k] = obj[k];
});
result.push(record);
} else {
for (const childKey of Object.keys(obj)) {
const child = obj[childKey];
if (helpers.isObject(child)) {
tree.push(childKey);
scanTreeObject(keys, treeLeafKey, child, tree, lvl + 1, result);
}
}
}
tree.splice(objIndex, 1);
return result;
}
function normalizeTreeToArray(keys, treeLeafKey, obj) {
const data = scanTreeObject(keys, treeLeafKey, obj);
if (!data.length) {
return data;
}
const max = data.reduce(function(maxVal, element) {
// minus 2 because _leaf and value properties are added
// on top to groups ones
const ikeys = Object.keys(element).length - 2;
return maxVal > ikeys ? maxVal : ikeys;
});
data.forEach(function(element) {
for (let i = 0; i < max; i++) {
const groupKey = getGroupKey(i);
if (!element[groupKey]) {
element[groupKey] = '';
}
}
});
return data;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
function flatten(input) {
const stack = [...input];
const res = [];
while (stack.length) {
// pop value from stack
const next = stack.pop();
if (Array.isArray(next)) {
// push back array items, won't modify the original input
stack.push(...next);
} else {
res.push(next);
}
}
// reverse to restore input order
return res.reverse();
}
function getPath(groups, value, defaultValue) {
if (!groups.length) {
return;
}
const path = [];
for (const grp of groups) {
const item = value[grp];
if (item === '') {
path.push(defaultValue);
break;
}
path.push(item);
}
return path.length ? path.join('.') : defaultValue;
}
/**
* @param {[]} values
* @param {string} grp
* @param {[string]} keys
* @param {string} treeeLeafKey
* @param {string} [mainGrp]
* @param {*} [mainValue]
* @param {[]} groups
*/
function group(values, grp, keys, treeLeafKey, mainGrp, mainValue, groups = []) {
const key = keys[0];
const addKeys = keys.slice(1);
const tmp = Object.create(null);
const data = Object.create(null);
const ret = [];
let g, i, n;
for (i = 0, n = values.length; i < n; ++i) {
const v = values[i];
if (mainGrp && v[mainGrp] !== mainValue) {
continue;
}
g = v[grp] || v[treeLeafKey] || '';
if (!(g in tmp)) {
const tmpRef = tmp[g] = {value: 0};
addKeys.forEach(function(k) {
tmpRef[k] = 0;
});
data[g] = [];
}
tmp[g].value += +v[key];
tmp[g].label = v[grp] || '';
const tmpRef = tmp[g];
addKeys.forEach(function(k) {
tmpRef[k] += v[k];
});
tmp[g].path = getPath(groups, v, g);
data[g].push(v);
}
Object.keys(tmp).forEach((k) => {
const v = {children: data[k]};
v[key] = +tmp[k].value;
addKeys.forEach(function(ak) {
v[ak] = +tmp[k][ak];
});
v[grp] = tmp[k].label;
v.label = k;
v.path = tmp[k].path;
if (mainGrp) {
v[mainGrp] = mainValue;
}
ret.push(v);
});
return ret;
}
function index(values, key) {
let n = values.length;
let i;
if (!n) {
return key;
}
const obj = helpers.isObject(values[0]);
key = obj ? key : 'v';
for (i = 0, n = values.length; i < n; ++i) {
if (obj) {
values[i]._idx = i;
} else {
values[i] = {v: values[i], _idx: i};
}
}
return key;
}
function sort(values, key) {
if (key) {
values.sort((a, b) => +b[key] - +a[key]);
} else {
values.sort((a, b) => +b - +a);
}
}
function sum(values, key) {
let s, i, n;
for (s = 0, i = 0, n = values.length; i < n; ++i) {
s += key ? +values[i][key] : +values[i];
}
return s;
}
/**
* @param {string} pkg
* @param {string} min
* @param {string} ver
* @param {boolean} [strict=true]
* @returns {boolean}
*/
function requireVersion(pkg, min, ver, strict = true) {
const parts = ver.split('.');
let i = 0;
for (const req of min.split('.')) {
const act = parts[i++];
if (parseInt(req, 10) < parseInt(act, 10)) {
break;
}
if (isOlderPart(act, req)) {
if (strict) {
throw new Error(`${pkg} v${ver} is not supported. v${min} or newer is required.`);
} else {
return false;
}
}
}
return true;
}
const widthCache = new Map();
/**
* Helper function to get the bounds of the rect
* @param {TreemapElement} rect the rect
* @param {boolean} [useFinalPosition]
* @return {object} bounds of the rect
* @private
*/
function getBounds(rect, useFinalPosition) {
const {x, y, width, height} = rect.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
return {left: x, top: y, right: x + width, bottom: y + height};
}
function limit(value, min, max) {
return Math.max(Math.min(value, max), min);
}
function parseBorderWidth(value, maxW, maxH) {
const o = helpers.toTRBL(value);
return {
t: limit(o.top, 0, maxH),
r: limit(o.right, 0, maxW),
b: limit(o.bottom, 0, maxH),
l: limit(o.left, 0, maxW)
};
}
function parseBorderRadius(value, maxW, maxH) {
const o = helpers.toTRBLCorners(value);
const maxR = Math.min(maxW, maxH);
return {
topLeft: limit(o.topLeft, 0, maxR),
topRight: limit(o.topRight, 0, maxR),
bottomLeft: limit(o.bottomLeft, 0, maxR),
bottomRight: limit(o.bottomRight, 0, maxR)
};
}
function boundingRects(rect) {
const bounds = getBounds(rect);
const width = bounds.right - bounds.left;
const height = bounds.bottom - bounds.top;
const border = parseBorderWidth(rect.options.borderWidth, width / 2, height / 2);
const radius = parseBorderRadius(rect.options.borderRadius, width / 2, height / 2);
const outer = {
x: bounds.left,
y: bounds.top,
w: width,
h: height,
active: rect.active,
radius
};
return {
outer,
inner: {
x: outer.x + border.l,
y: outer.y + border.t,
w: outer.w - border.l - border.r,
h: outer.h - border.t - border.b,
active: rect.active,
radius: {
topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
}
}
};
}
function inRange(rect, x, y, useFinalPosition) {
const skipX = x === null;
const skipY = y === null;
const bounds = !rect || (skipX && skipY) ? false : getBounds(rect, useFinalPosition);
return bounds
&& (skipX || x >= bounds.left && x <= bounds.right)
&& (skipY || y >= bounds.top && y <= bounds.bottom);
}
function hasRadius(radius) {
return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
}
/**
* Add a path of a rectangle to the current sub-path
* @param {CanvasRenderingContext2D} ctx Context
* @param {*} rect Bounding rect
*/
function addNormalRectPath(ctx, rect) {
ctx.rect(rect.x, rect.y, rect.w, rect.h);
}
function shouldDrawCaption(rect, options) {
if (!options || options.display === false) {
return false;
}
const {w, h} = rect;
const font = helpers.toFont(options.font);
const min = font.lineHeight;
const padding = limit(helpers.valueOrDefault(options.padding, 3) * 2, 0, Math.min(w, h));
return (w - padding) > min && (h - padding) > min;
}
function drawText(ctx, rect, options, item, levels) {
const {captions, labels} = options;
ctx.save();
ctx.beginPath();
ctx.rect(rect.x, rect.y, rect.w, rect.h);
ctx.clip();
const isLeaf = item && (!helpers.defined(item.l) || item.l === levels);
if (isLeaf && labels.display) {
drawLabel(ctx, rect, options);
} else if (!isLeaf && shouldDrawCaption(rect, captions)) {
drawCaption(ctx, rect, options, item);
}
ctx.restore();
}
function drawCaption(ctx, rect, options, item) {
const {captions, spacing, rtl} = options;
const {color, hoverColor, font, hoverFont, padding, align, formatter} = captions;
const oColor = (rect.active ? hoverColor : color) || color;
const oAlign = align || (rtl ? 'right' : 'left');
const optFont = (rect.active ? hoverFont : font) || font;
const oFont = helpers.toFont(optFont);
const lh = oFont.lineHeight / 2;
const x = calculateX(rect, oAlign, padding);
ctx.fillStyle = oColor;
ctx.font = oFont.string;
ctx.textAlign = oAlign;
ctx.textBaseline = 'middle';
ctx.fillText(formatter || item.g, x, rect.y + padding + spacing + lh);
}
function measureLabelSize(ctx, lines, fonts) {
const fontsKey = fonts.reduce(function(prev, item) {
prev += item.string;
return prev;
}, '');
const mapKey = lines.join() + fontsKey + (ctx._measureText ? '-spriting' : '');
if (!widthCache.has(mapKey)) {
ctx.save();
const count = lines.length;
let width = 0;
let height = 0;
for (let i = 0; i < count; i++) {
const font = fonts[Math.min(i, fonts.length - 1)];
ctx.font = font.string;
const text = lines[i];
width = Math.max(width, ctx.measureText(text).width);
height += font.lineHeight;
}
ctx.restore();
widthCache.set(mapKey, {width, height});
}
return widthCache.get(mapKey);
}
function toFonts(fonts, fitRatio) {
return fonts.map(function(f) {
f.size = Math.floor(f.size * fitRatio);
f.lineHeight = undefined;
return helpers.toFont(f);
});
}
function labelToDraw(ctx, rect, options, labelSize) {
const {overflow, padding} = options;
const {width, height} = labelSize;
if (overflow === 'hidden') {
return !((width + padding * 2) > rect.w || (height + padding * 2) > rect.h);
} else if (overflow === 'fit') {
const ratio = Math.min(rect.w / (width + padding * 2), rect.h / (height + padding * 2));
if (ratio < 1) {
return ratio;
}
}
return true;
}
function getFontFromOptions(rect, labels) {
const {font, hoverFont} = labels;
const optFont = (rect.active ? hoverFont : font) || font;
return helpers.isArray(optFont) ? optFont.map(f => helpers.toFont(f)) : [helpers.toFont(optFont)];
}
function drawLabel(ctx, rect, options) {
const labels = options.labels;
const content = labels.formatter;
if (!content) {
return;
}
const contents = helpers.isArray(content) ? content : [content];
let fonts = getFontFromOptions(rect, labels);
let labelSize = measureLabelSize(ctx, contents, fonts);
const lblToDraw = labelToDraw(ctx, rect, labels, labelSize);
if (!lblToDraw) {
return;
}
if (helpers.isNumber(lblToDraw)) {
labelSize = {width: labelSize.width * lblToDraw, height: labelSize.height * lblToDraw};
fonts = toFonts(fonts, lblToDraw);
}
const {color, hoverColor, align} = labels;
const optColor = (rect.active ? hoverColor : color) || color;
const colors = helpers.isArray(optColor) ? optColor : [optColor];
const xyPoint = calculateXYLabel(rect, labels, labelSize);
ctx.textAlign = align;
ctx.textBaseline = 'middle';
let lhs = 0;
contents.forEach(function(l, i) {
const c = colors[Math.min(i, colors.length - 1)];
const f = fonts[Math.min(i, fonts.length - 1)];
const lh = f.lineHeight;
ctx.font = f.string;
ctx.fillStyle = c;
ctx.fillText(l, xyPoint.x, xyPoint.y + lh / 2 + lhs);
lhs += lh;
});
}
function drawDivider(ctx, rect, options, item) {
const dividers = options.dividers;
if (!dividers.display || !item._data.children.length) {
return;
}
const {x, y, w, h} = rect;
const {lineColor, lineCapStyle, lineDash, lineDashOffset, lineWidth} = dividers;
ctx.save();
ctx.strokeStyle = lineColor;
ctx.lineCap = lineCapStyle;
ctx.setLineDash(lineDash);
ctx.lineDashOffset = lineDashOffset;
ctx.lineWidth = lineWidth;
ctx.beginPath();
if (w > h) {
const w2 = w / 2;
ctx.moveTo(x + w2, y);
ctx.lineTo(x + w2, y + h);
} else {
const h2 = h / 2;
ctx.moveTo(x, y + h2);
ctx.lineTo(x + w, y + h2);
}
ctx.stroke();
ctx.restore();
}
function calculateXYLabel(rect, options, labelSize) {
const {align, position, padding} = options;
let x, y;
x = calculateX(rect, align, padding);
if (position === 'top') {
y = rect.y + padding;
} else if (position === 'bottom') {
y = rect.y + rect.h - padding - labelSize.height;
} else {
y = rect.y + (rect.h - labelSize.height) / 2 + padding;
}
return {x, y};
}
function calculateX(rect, align, padding) {
if (align === 'left') {
return rect.x + padding;
} else if (align === 'right') {
return rect.x + rect.w - padding;
}
return rect.x + rect.w / 2;
}
class TreemapElement extends chart_js.Element {
constructor(cfg) {
super();
this.options = undefined;
this.width = undefined;
this.height = undefined;
if (cfg) {
Object.assign(this, cfg);
}
}
draw(ctx, data, levels = 0) {
if (!data) {
return;
}
const options = this.options;
const {inner, outer} = boundingRects(this);
const addRectPath = hasRadius(outer.radius) ? helpers.addRoundedRectPath : addNormalRectPath;
ctx.save();
if (outer.w !== inner.w || outer.h !== inner.h) {
ctx.beginPath();
addRectPath(ctx, outer);
ctx.clip();
addRectPath(ctx, inner);
ctx.fillStyle = options.borderColor;
ctx.fill('evenodd');
}
ctx.beginPath();
addRectPath(ctx, inner);
ctx.fillStyle = options.backgroundColor;
ctx.fill();
drawDivider(ctx, inner, options, data);
drawText(ctx, inner, options, data, levels);
ctx.restore();
}
inRange(mouseX, mouseY, useFinalPosition) {
return inRange(this, mouseX, mouseY, useFinalPosition);
}
inXRange(mouseX, useFinalPosition) {
return inRange(this, mouseX, null, useFinalPosition);
}
inYRange(mouseY, useFinalPosition) {
return inRange(this, null, mouseY, useFinalPosition);
}
getCenterPoint(useFinalPosition) {
const {x, y, width, height} = this.getProps(['x', 'y', 'width', 'height'], useFinalPosition);
return {
x: x + width / 2,
y: y + height / 2
};
}
tooltipPosition() {
return this.getCenterPoint();
}
/**
* @todo: remove this unused function in v3
*/
getRange(axis) {
return axis === 'x' ? this.width / 2 : this.height / 2;
}
}
TreemapElement.id = 'treemap';
TreemapElement.defaults = {
label: undefined,
borderRadius: 0,
borderWidth: 0,
captions: {
align: undefined,
color: 'black',
display: true,
font: {},
formatter: (ctx) => ctx.raw.g || ctx.raw._data.label || '',
padding: 3
},
dividers: {
display: false,
lineCapStyle: 'butt',
lineColor: 'black',
lineDash: [],
lineDashOffset: 0,
lineWidth: 1,
},
labels: {
align: 'center',
color: 'black',
display: false,
font: {},
formatter(ctx) {
if (ctx.raw.g) {
return [ctx.raw.g, ctx.raw.v + ''];
}
return ctx.raw._data.label ? [ctx.raw._data.label, ctx.raw.v + ''] : ctx.raw.v + '';
},
overflow: 'cut',
position: 'middle',
padding: 3
},
rtl: false,
spacing: 0.5
};
TreemapElement.descriptors = {
labels: {
_fallback: true
},
captions: {
_fallback: true
},
_scriptable: true,
_indexable: false
};
TreemapElement.defaultRoutes = {
backgroundColor: 'backgroundColor',
borderColor: 'borderColor'
};
function getDims(itm, w2, s2, key) {
const a = itm._normalized;
const ar = w2 * a / s2;
const d1 = Math.sqrt(a * ar);
const d2 = a / d1;
const w = key === '_ix' ? d1 : d2;
const h = key === '_ix' ? d2 : d1;
return {d1, d2, w, h};
}
const getX = (rect, w) => rect.rtl ? rect.x + rect.iw - w : rect.x + rect._ix;
function buildRow(rect, itm, dims, sum) {
const r = {
x: getX(rect, dims.w),
y: rect.y + rect._iy,
w: dims.w,
h: dims.h,
a: itm._normalized,
v: itm.value,
vs: itm.values,
s: sum,
_data: itm._data
};
if (itm.group) {
r.g = itm.group;
r.l = itm.level;
r.gs = itm.groupSum;
}
return r;
}
class Rect {
constructor(r) {
r = r || {w: 1, h: 1};
this.rtl = !!r.rtl;
this.x = r.x || r.left || 0;
this.y = r.y || r.top || 0;
this._ix = 0;
this._iy = 0;
this.w = r.w || r.width || (r.right - r.left);
this.h = r.h || r.height || (r.bottom - r.top);
}
get area() {
return this.w * this.h;
}
get iw() {
return this.w - this._ix;
}
get ih() {
return this.h - this._iy;
}
get dir() {
const ih = this.ih;
return ih <= this.iw && ih > 0 ? 'y' : 'x';
}
get side() {
return this.dir === 'x' ? this.iw : this.ih;
}
map(arr) {
const {dir, side} = this;
const key = dir === 'x' ? '_ix' : '_iy';
const sum = arr.nsum;
const row = arr.get();
const w2 = side * side;
const s2 = sum * sum;
const ret = [];
let maxd2 = 0;
let totd1 = 0;
for (const itm of row) {
const dims = getDims(itm, w2, s2, key);
totd1 += dims.d1;
maxd2 = Math.max(maxd2, dims.d2);
ret.push(buildRow(this, itm, dims, arr.sum));
this[key] += dims.d1;
}
this[dir === 'x' ? '_iy' : '_ix'] += maxd2;
this[key] -= totd1;
return ret;
}
}
const min = Math.min;
const max = Math.max;
function getStat(sa) {
return {
min: sa.min,
max: sa.max,
sum: sa.sum,
nmin: sa.nmin,
nmax: sa.nmax,
nsum: sa.nsum
};
}
function getNewStat(sa, o) {
const v = +o[sa.key];
const n = v * sa.ratio;
o._normalized = n;
return {
min: min(sa.min, v),
max: max(sa.max, v),
sum: sa.sum + v,
nmin: min(sa.nmin, n),
nmax: max(sa.nmax, n),
nsum: sa.nsum + n
};
}
function setStat(sa, stat) {
Object.assign(sa, stat);
}
function push(sa, o, stat) {
sa._arr.push(o);
setStat(sa, stat);
}
class StatArray {
constructor(key, ratio) {
const me = this;
me.key = key;
me.ratio = ratio;
me.reset();
}
get length() {
return this._arr.length;
}
reset() {
const me = this;
me._arr = [];
me._hist = [];
me.sum = 0;
me.nsum = 0;
me.min = Infinity;
me.max = -Infinity;
me.nmin = Infinity;
me.nmax = -Infinity;
}
push(o) {
push(this, o, getNewStat(this, o));
}
pushIf(o, fn, ...args) {
const nstat = getNewStat(this, o);
if (!fn(getStat(this), nstat, args)) {
return o;
}
push(this, o, nstat);
}
get() {
return this._arr;
}
}
function compareAspectRatio(oldStat, newStat, args) {
if (oldStat.sum === 0) {
return true;
}
const [length] = args;
const os2 = oldStat.nsum * oldStat.nsum;
const ns2 = newStat.nsum * newStat.nsum;
const l2 = length * length;
const or = Math.max(l2 * oldStat.nmax / os2, os2 / (l2 * oldStat.nmin));
const nr = Math.max(l2 * newStat.nmax / ns2, ns2 / (l2 * newStat.nmin));
return nr <= or;
}
/**
*
* @param {number[]|object[]} values
* @param {object} rectangle
* @param {string} [key]
* @param {string} [grp]
* @param {number} [lvl]
* @param {number} [gsum]
*/
function squarify(values, rectangle, keys = [], grp, lvl, gsum) {
values = values || [];
const rows = [];
const rect = new Rect(rectangle);
const row = new StatArray('value', rect.area / sum(values, keys[0]));
let length = rect.side;
const n = values.length;
let i, o;
if (!n) {
return rows;
}
const tmp = values.slice();
let key = index(tmp, keys[0]);
sort(tmp, key);
const val = (idx) => key ? +tmp[idx][key] : +tmp[idx];
const gval = (idx) => grp && tmp[idx][grp];
for (i = 0; i < n; ++i) {
o = {value: val(i), groupSum: gsum, _data: values[tmp[i]._idx], level: undefined, group: undefined};
if (grp) {
o.level = lvl;
o.group = gval(i);
const tmpRef = tmp[i];
o.values = keys.reduce(function(obj, k) {
obj[k] = +tmpRef[k];
return obj;
}, {});
}
o = row.pushIf(o, compareAspectRatio, length);
if (o) {
rows.push(rect.map(row));
length = rect.side;
row.reset();
row.push(o);
}
}
if (row.length) {
rows.push(rect.map(row));
}
return flatten(rows);
}
var version = "2.3.0";
function scaleRect(sq, xScale, yScale, sp) {
const sp2 = sp * 2;
const x = xScale.getPixelForValue(sq.x);
const y = yScale.getPixelForValue(sq.y);
const w = xScale.getPixelForValue(sq.x + sq.w) - x;
const h = yScale.getPixelForValue(sq.y + sq.h) - y;
return {
x: x + sp,
y: y + sp,
width: w - sp2,
height: h - sp2,
hidden: sp2 > w || sp2 > h,
};
}
function rectNotEqual(r1, r2) {
return !r1 || !r2
|| r1.x !== r2.x
|| r1.y !== r2.y
|| r1.w !== r2.w
|| r1.h !== r2.h
|| r1.rtl !== r2.rtl;
}
function arrayNotEqual(a, b) {
let i, n;
if (!a || !b) {
return true;
}
if (a === b) {
return false;
}
if (a.length !== b.length) {
return true;
}
for (i = 0, n = a.length; i < n; ++i) {
if (a[i] !== b[i]) {
return true;
}
}
return false;
}
function buildData(tree, dataset, keys, mainRect) {
const treeLeafKey = dataset.treeLeafKey || '_leaf';
if (helpers.isObject(tree)) {
tree = normalizeTreeToArray(keys, treeLeafKey, tree);
}
const groups = dataset.groups || [];
const glen = groups.length;
const sp = helpers.valueOrDefault(dataset.spacing, 0);
const captions = dataset.captions || {};
const font = helpers.toFont(captions.font);
const padding = helpers.valueOrDefault(captions.padding, 3);
function recur(gidx, rect, parent, gs) {
const g = getGroupKey(groups[gidx]);
const pg = (gidx > 0) && getGroupKey(groups[gidx - 1]);
const gdata = group(tree, g, keys, treeLeafKey, pg, parent, groups.filter((item, index) => index <= gidx));
const gsq = squarify(gdata, rect, keys, g, gidx, gs);
const ret = gsq.slice();
if (gidx < glen - 1) {
gsq.forEach((sq) => {
const bw = parseBorderWidth(dataset.borderWidth, sq.w / 2, sq.h / 2);
const subRect = {
...rect,
x: sq.x + sp + bw.l,
y: sq.y + sp + bw.t,
w: sq.w - 2 * sp - bw.l - bw.r,
h: sq.h - 2 * sp - bw.t - bw.b,
};
if (shouldDrawCaption(subRect, captions)) {
subRect.y += font.lineHeight + padding * 2;
subRect.h -= font.lineHeight + padding * 2;
}
ret.push(...recur(gidx + 1, subRect, sq.g, sq.s));
});
}
return ret;
}
return glen
? recur(0, mainRect)
: squarify(tree, mainRect, keys);
}
class TreemapController extends chart_js.DatasetController {
constructor(chart, datasetIndex) {
super(chart, datasetIndex);
this._groups = undefined;
this._keys = undefined;
this._rect = undefined;
this._rectChanged = true;
}
initialize() {
this.enableOptionSharing = true;
super.initialize();
}
getMinMax(scale) {
return {
min: 0,
max: scale.axis === 'x' ? scale.right - scale.left : scale.bottom - scale.top
};
}
configure() {
super.configure();
const {xScale, yScale} = this.getMeta();
if (!xScale || !yScale) {
// configure is called once before `linkScales`, and at that call we don't have any scales linked yet
return;
}
const w = xScale.right - xScale.left;
const h = yScale.bottom - yScale.top;
const rect = {x: 0, y: 0, w, h, rtl: !!this.options.rtl};
if (rectNotEqual(this._rect, rect)) {
this._rect = rect;
this._rectChanged = true;
}
if (this._rectChanged) {
xScale.max = w;
xScale.configure();
yScale.max = h;
yScale.configure();
}
}
update(mode) {
const dataset = this.getDataset();
const {data} = this.getMeta();
const groups = dataset.groups || [];
const keys = [dataset.key || ''].concat(dataset.sumKeys || []);
const tree = dataset.tree = dataset.tree || dataset.data || [];
if (mode === 'reset') {
// reset is called before 2nd configure and is only called if animations are enabled. So wen need an extra configure call here.
this.configure();
}
if (this._rectChanged || arrayNotEqual(this._keys, keys) || arrayNotEqual(this._groups, groups) || this._prevTree !== tree) {
this._groups = groups.slice();
this._keys = keys.slice();
this._prevTree = tree;
this._rectChanged = false;
dataset.data = buildData(tree, dataset, this._keys, this._rect);
// @ts-ignore using private stuff
this._dataCheck();
// @ts-ignore using private stuff
this._resyncElements();
}
this.updateElements(data, 0, data.length, mode);
}
updateElements(rects, start, count, mode) {
const reset = mode === 'reset';
const dataset = this.getDataset();
const firstOpts = this._rect.options = this.resolveDataElementOptions(start, mode);
const sharedOptions = this.getSharedOptions(firstOpts);
const includeOptions = this.includeOptions(mode, sharedOptions);
const {xScale, yScale} = this.getMeta(this.index);
for (let i = start; i < start + count; i++) {
const options = sharedOptions || this.resolveDataElementOptions(i, mode);
const properties = scaleRect(dataset.data[i], xScale, yScale, options.spacing);
if (reset) {
properties.width = 0;
properties.height = 0;
}
if (includeOptions) {
properties.options = options;
}
this.updateElement(rects[i], i, properties, mode);
}
this.updateSharedOptions(sharedOptions, mode, firstOpts);
}
draw() {
const {ctx, chartArea} = this.chart;
const metadata = this.getMeta().data || [];
const dataset = this.getDataset();
const levels = (dataset.groups || []).length - 1;
const data = dataset.data;
helpers.clipArea(ctx, chartArea);
for (let i = 0, ilen = metadata.length; i < ilen; ++i) {
const rect = metadata[i];
if (!rect.hidden) {
rect.draw(ctx, data[i], levels);
}
}
helpers.unclipArea(ctx);
}
}
TreemapController.id = 'treemap';
TreemapController.version = version;
TreemapController.defaults = {
dataElementType: 'treemap',
animations: {
numbers: {
type: 'number',
properties: ['x', 'y', 'width', 'height']
},
},
};
TreemapController.descriptors = {
_scriptable: true,
_indexable: false
};
TreemapController.overrides = {
interaction: {
mode: 'point',
includeInvisible: true,
intersect: true
},
hover: {},
plugins: {
tooltip: {
position: 'treemap',
intersect: true,
callbacks: {
title(items) {
if (items.length) {
const item = items[0];
return item.dataset.key || '';
}
return '';
},
label(item) {
const dataset = item.dataset;
const dataItem = dataset.data[item.dataIndex];
const label = dataItem.g || dataItem._data.label || dataset.label;
return (label ? label + ': ' : '') + dataItem.v;
}
}
},
},
scales: {
x: {
type: 'linear',
alignToPixels: true,
bounds: 'data',
display: false
},
y: {
type: 'linear',
alignToPixels: true,
bounds: 'data',
display: false,
reverse: true
}
},
};
TreemapController.beforeRegister = function() {
requireVersion('chart.js', '3.8', chart_js.Chart.version);
};
TreemapController.afterRegister = function() {
const tooltipPlugin = chart_js.registry.plugins.get('tooltip');
if (tooltipPlugin) {
tooltipPlugin.positioners.treemap = function(active) {
if (!active.length) {
return false;
}
const item = active[active.length - 1];
const el = item.element;
return el.tooltipPosition();
};
} else {
console.warn('Unable to register the treemap positioner because tooltip plugin is not registered');
}
};
TreemapController.afterUnregister = function() {
const tooltipPlugin = chart_js.registry.plugins.get('tooltip');
if (tooltipPlugin) {
delete tooltipPlugin.positioners.treemap;
}
};
chart_js.Chart.register(TreemapController, TreemapElement);
exports.flatten = flatten;
exports.getGroupKey = getGroupKey;
exports.group = group;
exports.index = index;
exports.normalizeTreeToArray = normalizeTreeToArray;
exports.requireVersion = requireVersion;
exports.sort = sort;
exports.sum = sum;
}));