const width = 500, height = 400, margin = { top: 0, right: 0, bottom: 0, left: 0 }, chartWidth = width - margin.left - margin.right, chartHeight = height - margin.top - margin.bottom; const colorScale = d3 .scaleLinear() .interpolate(d3.interpolateRgb.gamma(2.2)) .range([d3.rgb(0, 0, 0), d3.rgb(115, 192, 60)]); const treemap = d3 .treemap() .tile(d3.treemapSquarify) .size([chartWidth, chartHeight]) .round(true) .paddingOuter(d => (d.depth === 0 ? 0 : 1)); const svg = d3 .select('#container') .append('svg') .attr('width', width) .attr('height', height); const chart = svg .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); const getStartAndEndOfEachWord = str => { const regex = /\w+/g; const result = []; let match; while ((match = regex.exec(str))) { const start = match.index; const end = start + match[0].length; result.push([start, end]); } return result; }; // horrible implementation of https://en.wikipedia.org/wiki/Composition_(combinatorics) const getCompositions = list => { const bits = list.length - 1; const number_of_compositions = Math.pow(2, bits); return new Array(number_of_compositions).fill(0).map((_, i) => { const composition = i.toString(2).padStart(bits, '0').split('').reduce(( acc, bit, j ) => { const item = list[j + 1]; if (item) { if (bit === '0') { // add it to the last list acc[acc.length - 1].push(item); } else { // add it as a new list acc.push([item]); } } return acc; }, [[list[0]]]); return composition; }); }; d3.csv('.post-data.txt', (err, data) => { colorScale.domain(d3.extent(data, d => d.comments)); const nest = d3.nest().key(d => d.category); const nestedData = nest.entries(data); const root = d3 .hierarchy({ values: nestedData }, d => d.values) .sum(d => d.views) .sort((a, b) => b.views - a.views); treemap(root); const category = chart .selectAll('.category') .data(root.children) .enter() .append('g') .attr('class', 'category'); category.append('title').text(d => d.data.key); const post = category .selectAll('.post') .data(d => d.children) .enter() .append('rect') .attr('class', 'post') .attr('x', d => d.x0) .attr('y', d => d.y0) .attr('width', d => d.x1 - d.x0) .attr('height', d => d.y1 - d.y0) .attr('fill', d => colorScale(d.data.comments)); const categoryText = category .append('g') .attr('width', d => d.x1 - d.x0) .attr('height', d => d.y1 - d.y0) .attr('transform', d => `translate(${d.x0}, ${d.y0})`) .append('text') .attr('class', 'category-text') .text(d => d.data.key) .attr('dy', '1em'); // use the temporary text that was just rendered to calculate, wrap and resize the text parts categoryText .selectAll('tspan') .data((d, i, nodes) => { const text = d.data.key; const textNode = nodes[i]; // find the indices where words are, e.g. "hi you" => [[0, 1], [3, 5]] const indices = getStartAndEndOfEachWord(text); // create all possible ways this can be split into lines, e.g. [[[[[0, 1]], [[3, 5]]], [[0, 1], [3, 5]]] const compositions = getCompositions(indices); const textHeight = textNode.getBBox().height; const PADDING = 4; const containerWidth = d.x1 - d.x0 - PADDING * 2; const containerHeight = d.y1 - d.y0 - PADDING * 2; compositions.forEach(lines => { lines.forEach(words => { const startOfFirstWord = words[0][0]; const endOfLastWord = words[words.length - 1][1]; const numberOfCharacters = endOfLastWord - startOfFirstWord; const width = textNode.getSubStringLength( startOfFirstWord, numberOfCharacters ); words.width = width; words.maximumHorizontalZoom = containerWidth / Math.ceil(width); }); const height = textHeight * lines.length; lines.height = height; lines.maximumVerticalZoom = containerHeight / Math.ceil(height); lines.maximumHorizontalZoom = Math.min( ...lines.map(x => x.maximumHorizontalZoom) ); lines.maximumZoom = Math.min( lines.maximumVerticalZoom, lines.maximumHorizontalZoom ); }); compositions.sort((a, b) => b.maximumZoom - a.maximumZoom); const best = compositions[0]; // directly set the zoom to the textNode d3 .select(textNode) .html('') // dirty trick to clear the children .attr( 'transform', `translate(${PADDING},${PADDING}), scale(${best.maximumZoom}, ${best.maximumZoom})` ); return best.map(words => { const startOfFirstWord = words[0][0]; const endOfLastWord = words[words.length - 1][1]; return text.substring(startOfFirstWord, endOfLastWord); }); }) .enter() .append('tspan') .attr('x', 0) .attr('dy', '1em') .text(d => d); });