block by jeremycflin a4ddcc2a352b1b61da30ee1e65850006

Pie Chart with Image Pattern

Full Screen

Using SVG patterns to create small multiple pie charts with image fills. I dynamically calculate the spacing and size of the pattern to allow for space between each pie chart (using a small d3 plugin I have written called d3-patterngrid).

Visualising how many women earn over 140k at London universities.

TODO:

forked from tlfrd‘s block: Pie Chart with Image Pattern

index.html

<!DOCTYPE html>
<head>
  <meta charset="utf-8">
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script src="d3-patterngrid.js"></script>
  <style>
    .coin {
      stroke: black;
    }
    
    text {
      font-family: monospace;
    }
    
    .pie-segment {
      stroke: black;
      fill: white
    }
    
    .name-label, .fraction-label {
      font-size: 10px;
    }
    
    .name-label, .heading {
      font-weight: bold;
    }
    
    .sub-heading {
      font-size: 12px;
    }

  </style>
</head>

<body>
  <script>
    var width = 960,
        height = 500;
    
    var imageURL = "//i.imgur.com/diLMHJQ.png";
    var dataURL = "https://raw.githubusercontent.com/tlfrd/pay-ratios/master/data/over140k.json";
    
    var imageMode = true;
    
    var initialPosition = { x: 90, y: 95 };
    
    var circleSize = { width: 75, height: 75 };
    
    var spacing = {
      h: 30,
      v: 70
    };
    
    var labelPositionTop = -48,
        labelPositionBottom = 55;
    
    var gridLength = 8;
    
    var path = d3.arc()
    	.outerRadius(circleSize.width / 2)
    	.innerRadius(0)
    	.startAngle(0);
    
    var generateArc = function(fraction) {
      return path({endAngle: Math.PI * 2 * fraction})
    }
    
    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height)
    
    var defs = svg.append("defs");

    var coinPattern = patternGrid.circleLayout()
      .config({
        image: imageURL,
        radius: circleSize.width,
        padding: [spacing.h, spacing.v],
        margin: [initialPosition.y, initialPosition.x],
        id: "money"
      });
    
    // Load data
    
    var angleGrid = [];    
    
    d3.json(dataURL, function(error, data) {
      
      var over140 = data.number_over_140k;
      
      // Filter out universities where number of women is not provided
      over140 = over140.filter(function(a) {
        return a.women !== "-";
      })
      
    	over140.forEach(function(a) {
        var angle = a.women / a.over || 0;

        var angleObj = {
          name: a.name,
          angle: 1 - angle,
          women: a.women,
          total: a.over
        };
        angleGrid.push(angleObj);
      });
    
      // Sort the grid
      angleGrid.sort(function(a, b) {
        if (a.angle < b.angle) return -1;
        if (a.angle > b.angle) return 1;
        if (a.angle === b.angle) return 0;
      });
    
      // Assign positions
      angleGrid.forEach(function(a, i) {
        a.x = i % gridLength;
        a.y = Math.floor(i / gridLength);
      });  
      
      var circles = svg.append("g")
        .selectAll("circle")
        .data(angleGrid)
      .enter().append("circle")
        .attr("class", "coin")
        .attr("cx", function(d) { 
          return initialPosition.x + (circleSize.width + spacing.h) * d.x; 
        })
        .attr("cy", function(d) {
          return initialPosition.y + (circleSize.height + spacing.v) * d.y;
        })
        .attr("r", circleSize.width / 2)
        .attr("fill", "url(#money)");
    
    	var arcs = svg.append("g")
        .selectAll("path")
        .data(angleGrid.filter(d => d.angle))                      
       .enter().append("path")
        .attr("transform", function(d) {
          var xPos = initialPosition.x + (circleSize.width + spacing.h) * d.x;
          var yPos = initialPosition.y + (circleSize.height + spacing.v) * d.y;
          return "translate(" + xPos + ", " + yPos + ")";
        })
        .attr("class", "pie-segment")
        .attr("d", d => generateArc(d.angle));
      
          
      var namelabels = svg.append("g")
        .selectAll("text")
        .data(angleGrid)
      .enter().append("text")
        .attr("class", "name-label")
        .attr("x", function(d) { 
          return initialPosition.x + (circleSize.width + spacing.h) * d.x; 
        })
        .attr("y", function(d) {
          return initialPosition.y + (circleSize.height + spacing.v) * d.y;
        })
        .attr("dy", labelPositionTop)
        .attr("text-anchor", "middle")
        .text(d => d.name);

      var fractionclabels = svg.append("g")
        .selectAll("text")
        .data(angleGrid)
      .enter().append("text")
        .attr("class", "fraction-label")
        .attr("x", function(d) { 
          return initialPosition.x + (circleSize.width + spacing.h) * d.x; 
        })
        .attr("y", function(d) {
          return initialPosition.y + (circleSize.height + spacing.v) * d.y;
        })
        .attr("dy", labelPositionBottom)
        .attr("text-anchor", "middle")
        .text(d => d.women + " / " + d.total);
    });
    
    var legend = svg.append("g")
    	.attr("class", "legend")
      .attr("text-anchor", "end")
    	.attr("transform", "translate(" + [width - 100, height - 60] + ")");
    
    legend.append("text")
    	.attr("class", "heading")
    	.attr("dy", -20)
    	.text("London Universities");
    
    legend.append("text")
    	.attr("class", "sub-heading")
			.text("No. of Women / Total earning over 140k");
    
    svg.on("click", changeStyle);
    
    function changeStyle() {
      if (imageMode) {
        imageMode = false;
        svg.selectAll(".coin")
        	.style("fill", "white");
        svg.selectAll(".pie-segment")
        	.style("fill", "black");
      } else {
        imageMode = true;
        svg.selectAll(".coin")
        	.style("fill", "url(#money)");
        svg.selectAll(".pie-segment")
        	.style("fill", "white");
      }
    }
    
  </script>
</body>

d3-patterngrid.js

(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
	typeof define === 'function' && define.amd ? define(['exports'], factory) :
	(factory((global.patternGrid = global.patternGrid || {}), global.d3));
}(this, function (exports,d3) { 'use strict';

	function circlePatternGridLayout() {

    var circleRadius = undefined;
    var spacing = { horizontal: 0, vertical: 0 };
    var margin = { top: 0, left: 0 };
    var imageUrl = undefined;
    var id = "patternGrid";

    var pattern = d3.select("defs").append("pattern")
			.attr('patternUnits', 'userSpaceOnUse');

		function circleLayout() {
			setAttributes();
      return circleLayout;
		}

    function setAttributes() {
			pattern.select("image").remove();

      pattern
      	.attr("id", id)
        .attr("width", circleRadius + spacing.horizontal)
        .attr("height", circleRadius + spacing.vertical)
        .attr("x", margin.left + (circleRadius / 2) + spacing.horizontal)
        .attr("y", margin.top + (circleRadius / 2) + spacing.vertical)
        .append("image")
        .attr("xlink:href", imageUrl)
        .attr("width", circleRadius)
        .attr("height", circleRadius);
		}

		circleLayout.config = function(cfg) {
			if (cfg.image) {
				imageUrl = cfg.image;
			}
			if (cfg.radius) {
				circleRadius = cfg.radius;
			}
			if (cfg.padding) {
				spacing.horizontal = cfg.padding[0];
     		spacing.vertical = cfg.padding[1];
			}
			if (cfg.margin) {
				margin.top = cfg.margin[0];
				margin.left = cfg.margin[1];
			}
			if (cfg.id) {
				id = cfg.id;
			}
			return circleLayout();
		};

		return circleLayout;
	}

	var version = "0.0.1";

	exports.version = version;
  exports.circleLayout = circlePatternGridLayout;
}));