Skip to content

Instantly share code, notes, and snippets.

Last active December 3, 2018 08:14
Show Gist options
  • Save Kcnarf/e649c8723eff3fd64a23f75901910930 to your computer and use it in GitHub Desktop.
Save Kcnarf/e649c8723eff3fd64a23f75901910930 to your computer and use it in GitHub Desktop.
d3-voronoi-map : same final partitioning on reload
license: mit

This block illustrates the use of the d3-voronoi-map plugin. It enhances a previous block by always producing the same Voronoï map on reloads (more explanations below, before the Acknowledgments section). This block is a remake of the'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.

An iteration on this block enhances uses the version 2 of the plugin, which displays the live self-arrangement of the Voronoï map.

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>
<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="" charset="utf-8"></script>
<script src=""></script>
<script src="">
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;
.highlighter {
fill: transparent;
stroke: none;
.highlight {
stroke: black;
stroke-width: 2px;
//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-"};
//end: data-related utils
//begin: layout conf.
var svgWidth = 960,
svgHeight = 500,
margin = {top: 10, right: 10, bottom: 10, left: 10},
height = svgHeight - - 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").then(function(data) {
data.forEach(function(d) {csvParser(d)});
menPolygons = _voronoiMap
//for demo purpose, below line of code produces the same repeatable Voronoï map with the provided pie-based initial position policy
(data.filter( function(d){ return menCostAccessor(d)>0; }).reverse()).polygons;
womenPolygons = _voronoiMap
//for demo purpose, below line of code produces the same repeatable Voronoï map with the provided pie-based initial position policy
(data.filter( function(d){ return womenCostAccessor(d)>0; }).reverse()).polygons;
function csvParser(d) { =;
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) {
[radius*Math.cos(a), radius*Math.sin(a)]
return circlingPolygon;
function computeInitialPiePositioning(data) {
var initAngle = -3*Math.PI/4;
delta = _2PI/data.length,
halfMenRadius = menRadius/2,
halfWomenRadius = womenRadius/2;
var d, cos, sin;
for (var i=0; i<data.length; i++) {
d = data[i];
cos = Math.cos(initAngle + i*delta);
sin = Math.sin(initAngle + i*delta);
d.menInitPlacement = [
// without random(), error "Cannot read property 'site' of undefined" is raised
// hyp: random() may remove colinearity of sites
d.womenInitPlacement = [
// no need of random() here, don't know why !!!
function pieBasedPosition(gender) {
if (gender === "men") {
return function(d, i, arr, weightedVoronoi) { return d.menInitPlacement; };
} else {
return function(d, i, arr, weightedVoronoi) {return d.womenInitPlacement; };
function initLayout() {
svg ="svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
drawingArea = svg.append("g")
.classed("drawingArea", true)
.attr("transform", "translate("+[margin.left,]+")");
menContainer = drawingArea.append("g")
.classed("men-container", true)
.attr("transform", "translate("+menTreemapCenter+")");
womenContainer = drawingArea.append("g")
.classed("women-container", true)
.attr("transform", "translate("+womenTreemapCenter+")")
function drawTitle() {
.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() {
.classed("tiny light", true)
.attr("transform", "translate("+[0, height]+")")
.attr("text-anchor", "start")
.text("Remake of's post 'The Costs of Being Fat, in Actual Dollars'")
.classed("tiny light", true)
.attr("transform", "translate("+[halfWidth+45, height]+")")
.attr("text-anchor", "middle")
.text("by @_Kcnarf")
.classed("tiny light", true)
.attr("transform", "translate("+[width, height]+")")
.attr("text-anchor", "end")
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")
var legend = legends.append("g")
.classed("legend", true)
.attr("transform", function(d,i){
return "translate("+[0, -i*(legendHeight+interLegend)]+")";
.classed("legend-color", true)
.attr("y", -legendHeight)
.attr("width", colorWidth)
.attr("height", legendHeight)
.style("fill", function(d){ return d.color; });
.classed("tiny", true)
.attr("transform", "translate("+[colorWidth+5, -2]+")")
.text(function(d){ return d.composition; });
.attr("class", highlighterGroup)
.classed("highlighter", true)
.attr("y", -legendHeight)
.attr("width", colorWidth)
.attr("height", legendHeight);
.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);
.attr("r", menRadius-5);
.attr("transform", "translate("+[delta,-delta]+")")
.attr("d", "M"+[0,0]+"L"+[menRadius,-menRadius]+
function drawWomenSymbol() {
var delta = womenRadius,
symbolLength = 60,
midSymbolLength = symbolLength/2;
symbol = womenContainer.append("g").classed("symbol", true);
.attr("r", womenRadius-5);
.attr("transform", "translate("+[0,delta]+")")
.attr("d", "M"+[0,0]+"v"+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)
.classed("cell", true)
.attr("d", function(d){ return "M"+d.join(",")+"z"; })
.style("fill", function(d){
.classed("total-cost", true)
.attr("transform", "rotate("+totalCostRotation+")translate(0,"+(-delta-6)+")")
var costs = container.append("g")
.classed('costs', true)
.classed("cost", true)
.attr("transform", function(d){
return "translate("+[,]+")"; // +6 for centering
return "$"+costAccessor(;
var higlighters = container.append("g")
.classed('highlighters', true)
.attr("class", function(d) {
return highlighterGroup(;
.classed("highlighter", true)
.attr("d", function(d){ return "M"+d.join(",")+"z"; });
function attachMouseListener(data){
var id;
id =
.on("mouseenter", highlight(id, true))
.on("mouseleave", highlight(id, false));
function highlight(groupId, highlight){
return function() {
.classed("highlight", highlight);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment