function drawTreeMap(svg, data, options = {}) {
options = addDefaults(options, {
width: 1600,
height: 900,
rRounded: 32,
padding: 32,
funcDataToColor: function(data, i) {
return d3.schemeCategory10[i % 10];
},
funcDrawUnit: function(svg, [x0, y0], [itemWidth, itemHeight], d, i, j) {
const r = Math.min(itemWidth, itemHeight) / 2;
const [cx, cy] = [x0 + r, y0 + r];
drawCircle(svg, [cx, cy], r, { stroke: 'black' });
drawText(svg, [cx, cy], `${i}.${j}`, { fill: 'black', fontSize: r / 2 });
},
minFontSize: 16,
maxFontSize: 32,
maxCharsPerLine: 12
});
function valueIgnoringOther(d) {
if (d.name === 'Other') {
return 0;
}
return d.value;
}
const treeMapData = d3
.treemap()
.padding(options.padding)
.round(true)
.size([options.width, options.height])(
d3
.hierarchy(data)
.sum(d => d.value)
.sort((a, b) => valueIgnoringOther(b) - valueIgnoringOther(a))
)
.leaves();
treeMapData.forEach(function(d, i) {
const color = options.funcDataToColor(d.data, i);
const [rectWidth, rectHeight] = [d.x1 - d.x0, d.y1 - d.y0];
const text = `${d.data.name}(${d.data.value})`;
// const text = `${d.data.name}`;
const pTitleWidth = 0.6;
const isMultiLine = text.length > options.maxCharsPerLine;
const effectiveTextLength = isMultiLine
? text.length
: options.maxCharsPerLine;
const fontSize = Math.min(
options.maxFontSize,
Math.max(
options.minFontSize,
(rectWidth / effectiveTextLength) * pTitleWidth
)
);
const [titleWidth, titleHeight] = [
fontSize * effectiveTextLength * pTitleWidth * 0.8,
fontSize
];
const strokeWidth = Math.max(2, Math.min(4, fontSize / 4));
drawRect(svg, [d.x0, d.y0], [rectWidth, rectHeight], {
stroke: color,
strokeWidth: strokeWidth,
fill: 'white',
rx: options.rRounded,
ry: options.rRounded
});
const dataCount = d.data.value;
const availableRectHeight = rectHeight - fontSize / 2;
const area = rectWidth * availableRectHeight;
const k = Math.sqrt(dataCount / area);
const nx = Math.ceil(rectWidth * k);
const ny = Math.ceil(dataCount / nx);
const [itemWidth, itemHeight] = [rectWidth / nx, availableRectHeight / ny];
range(0, ny).forEach(function(iy) {
const y0 = d.y0 + iy * itemHeight + fontSize / 2;
const padding =
dataCount - iy * nx < nx ? (nx - dataCount + iy * nx) / 2 : 0;
range(0, nx).forEach(function(ix) {
const j = iy * nx + ix;
const x0 = d.x0 + (ix + padding) * itemWidth + padding;
if (j < dataCount) {
options.funcDrawUnit(svg, [x0, y0], [itemHeight, itemWidth], d, i, j);
}
});
});
drawRect(
svg,
[d.x0 + rectWidth / 2 - titleWidth / 2, d.y0 - titleHeight / 2],
[titleWidth, titleHeight],
{
stroke: 'none',
fill: 'white',
rx: options.rRounded,
ry: options.rRounded
}
);
const funcDrawText = isMultiLine ? drawMultiText : drawText;
funcDrawText(svg, [(d.x0 + d.x1) / 2, d.y0], text, {
fill: color,
fontSize: fontSize,
alignmentBaseline: 'middle',
maxCharsPerLine: options.maxCharsPerLine
});
});
}