Skip to content

Instantly share code, notes, and snippets.

@63anp3ca
Created October 9, 2018 15:58
Show Gist options
  • Save 63anp3ca/2cb936ff06177e000e7867eed6a3e372 to your computer and use it in GitHub Desktop.
Save 63anp3ca/2cb936ff06177e000e7867eed6a3e372 to your computer and use it in GitHub Desktop.
d3-voronoi-map v2 usage
license: mit

This block illustrates the use of the d3-voronoi-map plugin. It enhances a previous block by using the version 2 of the plugin, which is capable of displaying the self-organizing arrangement of the Voronoï map. 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.

This block always produces the same Voronoï map on reload thanks to the initialPosition() API. This API allows to define the initial positions of each sites, before launching the iterative computation of the Voronoï map. By default, a random positioning is used, which leads to distinct final Vornoï maps on each reload. By setting the initial sites' positions in a repeatable way, reloadings produce always the same final Voronoï map.

In this particular block, controlling initial positions of sites also helps to make the two Voronoï maps (men/women) having the same layout (e.g. placing sites/cells of the same type at the same position), which eases comparison.

Acknowledgments to :

forked from Kcnarf's block: d3-voronoi-map : same final partitioning on reload

forked from Kcnarf's block: d3-voronoi-map v2 usage

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="http://d3js.org/d3.v5.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/v2.0.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 highlighterGroupId(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 menRadius, womenRadius,
menCirclingPolygon, womenCirclingPolygon,
menPolygons, womenPolygons;
//end: treemap conf.
//begin: reusable d3Selection
var svg, drawingArea, menContainer, womenContainer;
//end: reusable d3Selection
d3.csv("costOfBeingFat.csv").then(function(data) {
data.forEach(function(d) {csvParser(d)});
initData(data);
initLayout();
drawLegends(data);
var menData = data.filter( function(d){ return menCostAccessor(d)>0; }).reverse();
menSimulation = d3.voronoiMapSimulation(menData)
.clip(menCirclingPolygon)
.weight(menCostAccessor)
.initialPosition(d3.voronoiMapInitialPositionPie().startAngle(-Math.PI*3/5))
.on("tick", function() {
// function called after each iteration of computation
// called only in simulation mode, not in static mode (see below)
menPolygons = menSimulation.state().polygons;
drawTreemap("men");
})
.on("end", function() {
attachMouseListener(data);
});
var womenData = data.filter( function(d){ return womenCostAccessor(d)>0; }).reverse();
womenSimulation = d3.voronoiMapSimulation(womenData)
.clip(womenCirclingPolygon)
.weight(womenCostAccessor)
.initialPosition(d3.voronoiMapInitialPositionPie().startAngle(-Math.PI*3/4))
.on("tick", function() {
// function called after each iteration of computation
// called only in simulation mode, not in static mode (see below)
womenPolygons = womenSimulation.state().polygons;
drawTreemap("women");
attachMouseListener(data);
});
//begin: how to draw a static arrangement
var simulate = true;
if (!simulate) {
// firstly, stop simulations
menSimulation.stop();
womenSimulation.stop();
// secondly, manually call ticks until final arrangment is produced
while (!menSimulation.state().ended) {
menSimulation.tick();
// function defined by .on("tick", ...) above are not called
}
while (!womenSimulation.state().ended) {
womenSimulation.tick();
// function defined by .on("tick", ...) above are not called
}
// finally, polygons are available and can be used as desired
menPolygons = menSimulation.state().polygons;
womenPolygons = womenSimulation.state().polygons;
drawTreemap("men");
drawTreemap("women");
attachMouseListener(data);
}
//end: how to draw a static arrangement
});
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(data) {
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+")");
drawMenSymbol();
menContainer.append("text")
.classed("total-cost", true)
.attr("transform", "rotate(-45)translate(0,"+(-menRadius-6)+")")
.text("$"+menTotalCost);
menContainer.append("g")
.classed('cells', true);
menContainer.append("g")
.classed('costs', true);
menContainer.append("g")
.classed('highlighters', true);
womenContainer = drawingArea.append("g")
.classed("women-container", true)
.attr("transform", "translate("+womenTreemapCenter+")")
drawWomenSymbol();
womenContainer.append("text")
.classed("total-cost", true)
.attr("transform", "rotate(45)translate(0,"+(-womenRadius-6)+")")
.text("$"+menTotalCost);
womenContainer.append("g")
.classed('cells', true);
womenContainer.append("text")
.classed("total-cost", true);
womenContainer.append("g")
.classed('costs', true);
womenContainer.append("g")
.classed('highlighters', true);
drawTitle();
drawFooter();
}
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/89d9d2d575f5c4ad41235cad6b202742")
}
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", highlighterGroupId)
.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;
} else {
container = womenContainer;
polygons = womenPolygons;
costAccessor = womenCostAccessor;
}
var cells = container.select(".cells")
.selectAll(".cell")
.data(polygons);
cells.enter()
.append("path")
.classed("cell", true)
.merge(cells)
.attr("d", function(d){ return "M"+d.join(",")+"z"; })
.style("fill", function(d){
return d.site.originalObject.data.originalData.color;
});
var costs = container.select(".costs")
.selectAll(".cost")
.data(polygons);
costs.enter()
.append("text")
.classed("cost", true)
.merge(costs)
.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 highlighters = container.select(".highlighters")
.selectAll(".highlighter")
.data(polygons);
highlighters.enter()
.append("path")
.merge(highlighters)
.attr("class", function(d) {
return highlighterGroupId(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