An implementation in D3 of Figure 5-19 "Revised treemap from R to Illustrator" in "Visualize This" by Nathan Yau.
The color scale looks way more washed out than the one in the book unfortunately
license: mit |
id,views,comments,category | |
5019,148896,28,Artistic Visualization | |
1416,81374,26,Visualization | |
1416,81374,26,Featured | |
3485,80819,37,Featured | |
3485,80819,37,Mapping | |
3485,80819,37,Data Sources | |
500,76495,10,Statistical Visualization | |
500,76495,10,Mapping | |
500,76495,10,Network Visualization | |
4092,66650,70,Ugly Visualization | |
4092,66650,70,Mistaken Data | |
2432,42512,17,Infographics | |
4449,36166,10,Featured | |
4449,36166,10,Visualization | |
836,34972,56,Projects | |
836,34972,56,Featured | |
836,34972,56,Mapping | |
836,34972,56,Data Sources | |
4869,33705,90,Infographics | |
3100,32143,45,Data Sources | |
3100,32143,45,Featured | |
4122,31076,8,Mapping | |
1079,29646,41,Software | |
1079,29646,41,Featured | |
975,25247,11,Self-surveillance | |
2545,25190,12,Miscellaneous Visualization | |
979,25062,25,Featured | |
979,25062,25,Self-surveillance | |
540,24302,8,Featured | |
540,24302,8,Visualization | |
3581,23311,87,Mapping | |
3581,23311,87,Tutorials | |
3581,23311,87,Featured | |
1551,21632,22,Mapping | |
1245,20746,14,Featured | |
1245,20746,14,Visualization | |
889,20703,26,Featured | |
889,20703,26,Projects | |
889,20703,26,Statistical Visualization | |
763,20446,5,Featured | |
763,20446,5,Visualization | |
1141,20016,37,Infographics | |
4627,19763,11,Featured | |
4627,19763,11,Data Design Tips | |
1585,19640,25,Featured | |
1585,19640,25,Data Design Tips | |
1585,19640,25,Statistics | |
409,19266,12,Statistical Visualization | |
4976,18950,7,Miscellaneous Visualization | |
663,18887,24,Infographics | |
1458,18839,13,Infographics | |
2525,18697,2,Network Visualization | |
800,18612,25,Quotes | |
498,18182,5,Data Sources | |
681,16850,13,Miscellaneous | |
2591,16669,10,Infographics | |
949,16292,2,Miscellaneous | |
1541,15914,45,Visualization | |
1541,15914,45,Statistics | |
1541,15914,45,Featured | |
459,15752,14,Infographics | |
655,15599,10,Featured | |
655,15599,10,Software | |
3805,15536,42,Infographics | |
7,15260,2,Visualization | |
4075,15138,58,Mapping | |
3935,15104,14,Data Design Tips | |
3935,15104,14,Featured | |
2551,14846,39,Data Design Tips | |
2551,14846,39,Featured | |
706,14123,16,Statistics | |
4276,13670,14,Miscellaneous Visualization | |
1223,12560,7,Statistical Visualization | |
1223,12560,7,Data Design Tips | |
637,12341,13,Visualization | |
842,12155,4,Mapping | |
842,12155,4,Economics | |
364,12012,9,Featured | |
3544,11948,6,Visualization | |
645,11790,10,Data Sources | |
517,11709,1,Artistic Visualization | |
1303,11651,8,Infographics | |
682,11266,2,Artistic Visualization | |
2711,11230,2,Artistic Visualization | |
3228,11081,8,Mapping | |
1062,10820,7,Mapping | |
4884,10810,37,Statistical Visualization | |
4884,10810,37,Tutorials | |
4884,10810,37,Featured | |
952,10748,12,Mapping | |
952,10748,12,Projects | |
952,10748,12,Featured | |
1343,10746,18,Statistical Visualization | |
717,10649,6,Visualization | |
1293,10460,3,Mapping | |
3459,10286,6,Infographics | |
2441,10114,12,Miscellaneous Visualization | |
1016,9954,9,Miscellaneous | |
2734,9086,8,Statistical Visualization | |
687,8993,0,Infographics |
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); | |
}); |
<!DOCTYPE html> | |
<body> | |
<style> | |
body { | |
width: 100%; | |
font-family: Georgia; | |
font-size: 14px; | |
color: #333; | |
} | |
#container { | |
width: 500px; | |
margin: 0 auto; | |
position: relative; | |
} | |
#header h1 { | |
text-transform: uppercase; | |
font-size: 18px; | |
} | |
#header { | |
line-height: 1.4em; | |
margin-bottom: 16px; | |
} | |
.category-text { | |
font-family: Arial; | |
fill: #fff; | |
} | |
</style> | |
<div id="container"> | |
<div id="header"> | |
<h1>FlowingData map</h1> | |
Below are popular posts on FlowingData. Each rectangle represents a post. Size represents number of views and brigther green indicates more comments | |
</div> | |
</div> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<script src="app.js"></script> | |
</body> |