block by jeremycflin 2a9e709023fa86ac802f8957f5c67da8

d3-voronoi-map usage

Full Screen

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.

Acknowledgments to :

forked from Kcnarf‘s block: d3-voronoi-map usage

index.html

<!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="//d3js.org/d3.v4.min.js" charset="utf-8"></script>
    <script src="https://raw.githack.com/Kcnarf/d3-weighted-voronoi/master/build/d3-weighted-voronoi.js"></script>
    <script src="https://raw.githack.com/Kcnarf/d3-voronoi-map/master/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>

costOfBeingFat.csv

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"