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
<!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>
(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;
}));