oca-ocb-report/odoo-bringout-oca-ocb-spreadsheet/spreadsheet/static/lib/chart_js_treemap.js
Ernad Husremovic 184bb0e321 19.0 vanilla
2026-03-09 09:32:02 +01:00

1298 lines
36 KiB
JavaScript

/*!
* chartjs-chart-treemap v3.1.0
* https://chartjs-chart-treemap.pages.dev/
* (c) 2025 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) {
return [];
}
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(displayMode, rect, options) {
if (!options || options.display === false) {
return false;
}
if (displayMode === "headerBoxes") {
return true;
}
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 getCaptionHeight(displayMode, rect, font, padding) {
if (displayMode !== "headerBoxes") {
return font.lineHeight + padding * 2;
}
const captionHeight = font.lineHeight + padding * 2;
return rect.h < 2 * captionHeight ? rect.h / 3 : captionHeight;
}
function drawText(ctx, rect, options, item) {
const { captions, labels, displayMode } = 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.isLeaf);
if (isLeaf && labels.display) {
drawLabel(ctx, rect, options);
} else if (!isLeaf && shouldDrawCaption(displayMode, rect, captions)) {
drawCaption(ctx, rect, options, item);
}
ctx.restore();
}
function drawCaption(ctx, rect, options, item) {
const { captions, spacing, rtl, displayMode } = 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 fonts = [oFont];
if (oFont.lineHeight > rect.h) {
return;
}
let text = formatter || item.g;
const captionSize = measureLabelSize(ctx, [formatter], fonts);
if (captionSize.width + 2 * padding > rect.w) {
text = sliceTextToFitWidth(ctx, text, rect.w - 2 * padding, fonts);
}
const lh = oFont.lineHeight / 2;
const x = calculateX(rect, oAlign, padding);
ctx.fillStyle = oColor;
ctx.font = oFont.string;
ctx.textAlign = oAlign;
ctx.textBaseline = "middle";
const y = displayMode === "headerBoxes" ? rect.y + rect.h / 2 : rect.y + padding + spacing + lh;
ctx.fillText(text, x, y);
}
function sliceTextToFitWidth(ctx, text, width, fonts) {
const ellipsis = "...";
const ellipsisWidth = measureLabelSize(ctx, [ellipsis], fonts).width;
if (ellipsisWidth >= width) {
return "";
}
let lowerBoundLen = 1;
let upperBoundLen = text.length;
let currentWidth;
while (lowerBoundLen <= upperBoundLen) {
const currentLen = Math.floor((lowerBoundLen + upperBoundLen) / 2);
const currentText = text.slice(0, currentLen);
currentWidth = measureLabelSize(ctx, [currentText], fonts).width;
if (currentWidth + ellipsisWidth > width) {
upperBoundLen = currentLen - 1;
} else {
lowerBoundLen = currentLen + 1;
}
}
const slicedText = text.slice(0, Math.max(0, lowerBoundLen - 1));
return slicedText ? slicedText + ellipsis : "";
}
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) {
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);
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 = {
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,
},
label: undefined,
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,
unsorted: false,
displayMode: "containerBoxes",
};
TreemapElement.descriptors = {
captions: {
_fallback: true,
},
labels: {
_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.unsorted = !!r.unsorted;
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]);
if (!rectangle?.unsorted) {
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 = "3.1.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 ||
r1.unsorted !== r2.unsorted
);
}
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 =
dataset.displayMode === "headerBoxes" ? 0 : helpers.valueOrDefault(dataset.spacing, 0);
const captions = dataset.captions || {};
const font = helpers.toFont(captions.font);
const padding = helpers.valueOrDefault(captions.padding, 3);
function recur(treeElements, gidx, rect, parent, gs) {
const g = getGroupKey(groups[gidx]);
const pg = gidx > 0 && getGroupKey(groups[gidx - 1]);
const gdata = group(
treeElements,
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 =
dataset.displayMode === "headerBoxes"
? { l: 0, r: 0, t: 0, b: 0 }
: 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(dataset.displayMode, subRect, captions)) {
const captionHeight = getCaptionHeight(dataset.displayMode, subRect, font, padding);
subRect.y += captionHeight;
subRect.h -= captionHeight;
}
const children = [];
gdata.forEach((gEl) => {
children.push(...recur(gEl.children, gidx + 1, subRect, sq.g, sq.s));
});
ret.push(...children);
sq.isLeaf = !children.length;
});
} else {
gsq.forEach((sq) => {
sq.isLeaf = true;
});
}
return ret;
}
const result = glen ? recur(tree, 0, mainRect) : squarify(tree, mainRect, keys);
return result
.map((d) => {
if (dataset.displayMode !== "headerBoxes" || d.isLeaf) {
return d;
}
if (!shouldDrawCaption(dataset.displayMode, d, captions)) {
return undefined;
}
const captionHeight = getCaptionHeight(dataset.displayMode, d, font, padding);
return { ...d, h: captionHeight };
})
.filter((d) => d);
}
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, unsorted: !!this.options.unsorted };
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 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]);
}
}
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;
});