Skip to content

Instantly share code, notes, and snippets.

@Kcnarf
Last active August 17, 2018 14:16
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save Kcnarf/238fa136f763f5ad908271a170ef60e2 to your computer and use it in GitHub Desktop.
d3-voronoi-map usage
license: mit

This block illustrates the use of the d3-voronoi-map plugin. This block is a remake of the HowMuch.net's post The Costs of Being Fat, in Actual Dollars.

The d3-voronoi-map plugin produces Voronoï maps (one-level treemap). Given a convex polygon (here, a 60-gon simulating a circle for each gender) and weighted data, it tesselates/partitions the polygon in several inner cells, such that the area of a cell represents the weight of the underlying datum.

An iteration on this block enhances the user experience by (i) always producing the same Voronoï map on reloads and (ii) having the same layout (e.g. placing sites/cells of the same type at the same position) which eases comparison.

Acknowledgments to :

id composition menCost womenCost color
0 Wage Discrimination 0 1855 #b5a8d8
1 Direct Medical 1474 1474 #bfe5df
2 Short-term Disability 389 309 #a3c5cb
3 Productivity (Presenteeism) 358 358 #abb6ab
4 Sick Leave (Absenteeism) 212 674 #b7d8a9
5 Life Insurance 121 121 #ffe7a4
6 Disability Pension Insurance 69 69 #f7c098
7 Gasoline for cars 23 21 #f3a39c
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>d3-voronoi-treemap usage</title>
<meta name="description" content="d3-voronoi-map plugin to remake 'The Costs of Being Fat, in Actual Dollars'">
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://rawcdn.githack.com/Kcnarf/d3-weighted-voronoi/v1.0.0/build/d3-weighted-voronoi.js"></script>
<script src="https://rawcdn.githack.com/Kcnarf/d3-voronoi-map/v1.2.0/build/d3-voronoi-map.js"></script>
<style>
svg {
background-color: rgb(250,250,250);
}
#title {
letter-spacing: 4px;
font-weight: 700;
font-size: x-large;
}
text.tiny {
font-size: 10pt;
}
text.light {
fill: lightgrey
}
.symbol {
fill: none;
stroke: lightgrey;
stroke-width: 14px;
}
.cell {
stroke: darkgrey;
stroke-width: 1px;
}
.cost {
text-anchor: middle;
}
.total-cost {
fill: lightgrey;
text-anchor: middle;
font-size: 20px;
font-weight: 700;
}
.legend-color {
stroke-width: 1px;
stroke:darkgrey;
}
.highlighter {
fill: transparent;
stroke: none;
}
.highlight {
stroke: black;
stroke-width: 2px;
}
</style>
</head>
<body>
<svg></svg>
<script>
//begin: constants
var _2PI = 2*Math.PI;
//end: constants
//begin: raw data global def
var menTotalCost = 0,
womenTotalCost = 0;
//end: raw data global def
//begin: data-related utils
function menCostAccessor(d){ return d.menCost; };
function womenCostAccessor(d){ return d.womenCost; };
function highlighterGroup(d){ return "group-"+d.id};
//end: data-related utils
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
margin = {top: 10, right: 10, bottom: 10, left: 10},
height = svgHeight - margin.top - margin.bottom,
width = svgWidth - margin.left - margin.right,
halfWidth = width/2,
halfHeight = height/2,
quarterWidth = width/4,
quarterHeight = height/4,
titleY = 20,
legendsMinY = height - 20,
menTreemapCenter = [300, 200],
womenTreemapCenter = [650, 200];
//end: layout conf.
//begin: treemap conf.
var baseRadius = 100;
var _voronoiMap = d3.voronoiMap();
var menRadius, womenRadius,
menCirclingPolygon, womenCirclingPolygon,
menPolygons, womenPolygons;
//end: treemap conf.
//begin: reusable d3Selection
var svg, drawingArea, menContainer, womenContainer;
//end: reusable d3Selection
d3.csv("costOfBeingFat.csv", csvParser, function(error, data) {
if (error) throw error;
initData();
initLayout();
drawLegends(data);
menPolygons = _voronoiMap
.clip(menCirclingPolygon)
.weight(menCostAccessor)
(data.filter( function(d){ return menCostAccessor(d)>0; })).polygons;
womenPolygons = _voronoiMap
.clip(womenCirclingPolygon)
.weight(womenCostAccessor)
(data.filter( function(d){ return womenCostAccessor(d)>0; })).polygons;
drawTreemap("men");
drawTreemap("women");
attachMouseListener(data);
});
function csvParser(d) {
d.id = +d.id;
d.composition = d.composition;
d.menCost = +d.menCost;
d.womenCost = +d.womenCost;
d.color = d.color;
menTotalCost += d.menCost;
womenTotalCost += d.womenCost;
return d;
};
function initData() {
menRadius = baseRadius;
womenRadius = baseRadius*Math.sqrt(womenTotalCost/menTotalCost);
menCirclingPolygon = computeCirclingPolygon(menRadius);
womenCirclingPolygon = computeCirclingPolygon(womenRadius);
}
function computeCirclingPolygon(radius) {
var points = 60,
increment = _2PI/points,
circlingPolygon = [];
for (var a=0, i=0; i<points; i++, a+=increment) {
circlingPolygon.push(
[radius*Math.cos(a), radius*Math.sin(a)]
)
}
return circlingPolygon;
};
function initLayout() {
svg = d3.select("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
drawingArea = svg.append("g")
.classed("drawingArea", true)
.attr("transform", "translate("+[margin.left,margin.top]+")");
menContainer = drawingArea.append("g")
.classed("men-container", true)
.attr("transform", "translate("+menTreemapCenter+")");
womenContainer = drawingArea.append("g")
.classed("women-container", true)
.attr("transform", "translate("+womenTreemapCenter+")")
drawTitle();
drawFooter();
drawMenSymbol();
drawWomenSymbol();
}
function drawTitle() {
drawingArea.append("text")
.attr("id", "title")
.attr("transform", "translate("+[halfWidth, titleY]+")")
.attr("text-anchor", "middle")
.text("The Individual Costs of Being Obese in the U.S. (2010)")
}
function drawFooter() {
drawingArea.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[0, height]+")")
.attr("text-anchor", "start")
.text("Remake of HowMuch.net's post 'The Costs of Being Fat, in Actual Dollars'")
drawingArea.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[halfWidth+45, height]+")")
.attr("text-anchor", "middle")
.text("by @_Kcnarf")
drawingArea.append("text")
.classed("tiny light", true)
.attr("transform", "translate("+[width, height]+")")
.attr("text-anchor", "end")
.text("bl.ocks.org/Kcnarf/238fa136f763f5ad908271a170ef60e2")
}
function drawLegends(data) {
var legendHeight = 13,
interLegend = 4,
colorWidth = legendHeight*4;
var legendContainer = drawingArea.append("g")
.classed("legend", true)
.attr("transform", "translate("+[0, legendsMinY]+")");
var legends = legendContainer.selectAll(".legend")
.data(data.reverse())
.enter();
var legend = legends.append("g")
.classed("legend", true)
.attr("transform", function(d,i){
return "translate("+[0, -i*(legendHeight+interLegend)]+")";
})
legend.append("rect")
.classed("legend-color", true)
.attr("y", -legendHeight)
.attr("width", colorWidth)
.attr("height", legendHeight)
.style("fill", function(d){ return d.color; });
legend.append("text")
.classed("tiny", true)
.attr("transform", "translate("+[colorWidth+5, -2]+")")
.text(function(d){ return d.composition; });
legend.append("rect")
.attr("class", highlighterGroup)
.classed("highlighter", true)
.attr("y", -legendHeight)
.attr("width", colorWidth)
.attr("height", legendHeight);
legendContainer.append("text")
.attr("transform", "translate("+[0, -data.length*(legendHeight+interLegend)-5]+")")
.text("Annual costs of being obese");
}
function drawMenSymbol() {
var delta = menRadius/10,
symbolLength = 40,
symbol = menContainer.append("g").classed("symbol", true);
symbol.append("circle")
.attr("r", menRadius-5);
symbol.append("path")
.attr("transform", "translate("+[delta,-delta]+")")
.attr("d", "M"+[0,0]+"L"+[menRadius,-menRadius]+
"M"+[menRadius-symbolLength,-menRadius]+"h"+symbolLength+",v"+symbolLength
);
}
function drawWomenSymbol() {
var delta = womenRadius,
symbolLength = 60,
midSymbolLength = symbolLength/2;
symbol = womenContainer.append("g").classed("symbol", true);
symbol.append("circle")
.attr("r", womenRadius-5);
symbol.append("path")
.attr("transform", "translate("+[0,delta]+")")
.attr("d", "M"+[0,0]+"v"+symbolLength+
"M"+[-midSymbolLength,midSymbolLength]+"h"+symbolLength
);
}
function drawTreemap(gender) {
var container, polygons, costAccessor, delta, totalCost, totalCostRotation;
if (gender==="men") {
container = menContainer;
polygons = menPolygons;
costAccessor = menCostAccessor;
delta = menRadius;
totalCost = "$"+menTotalCost;
totalCostRotation = -45;
} else {
container = womenContainer;
polygons = womenPolygons;
costAccessor = womenCostAccessor;
delta = womenRadius;
totalCost = "$"+womenTotalCost;
totalCostRotation = 45;
}
var cells = container.append("g")
.classed('cells', true)
.selectAll(".cell")
.data(polygons)
.enter()
.append("path")
.classed("cell", true)
.attr("d", function(d){ return "M"+d.join(",")+"z"; })
.style("fill", function(d){
return d.site.originalObject.data.originalData.color;
});
container.append("text")
.classed("total-cost", true)
.attr("transform", "rotate("+totalCostRotation+")translate(0,"+(-delta-6)+")")
.text(totalCost);
var costs = container.append("g")
.classed('costs', true)
.selectAll(".cost")
.data(polygons)
.enter()
.append("text")
.classed("cost", true)
.attr("transform", function(d){
return "translate("+[d.site.x, d.site.y+6]+")"; // +6 for centering
})
.text(function(d){
return "$"+costAccessor(d.site.originalObject.data.originalData);
})
var higlighters = container.append("g")
.classed('highlighters', true)
.selectAll(".highlighter")
.data(polygons)
.enter()
.append("path")
.attr("class", function(d) {
return highlighterGroup(d.site.originalObject.data.originalData);
})
.classed("highlighter", true)
.attr("d", function(d){ return "M"+d.join(",")+"z"; });
}
function attachMouseListener(data){
var id;
data.forEach(function(d){
id = d.id
d3.selectAll(".group-"+id)
.on("mouseenter", highlight(id, true))
.on("mouseleave", highlight(id, false));
})
}
function highlight(groupId, highlight){
return function() {
d3.selectAll(".group-"+groupId)
.classed("highlight", highlight);
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment