Skip to content

Instantly share code, notes, and snippets.

@ErikOnBike
Last active September 30, 2018 19:17
Show Gist options
  • Save ErikOnBike/b542468559630c116e44a3578d9660d9 to your computer and use it in GitHub Desktop.
Save ErikOnBike/b542468559630c116e44a3578d9660d9 to your computer and use it in GitHub Desktop.
Donut Chart Design
license: gpl-3.0
height: 500
scrolling: yes

This example is created as a response to a question on the D3-forum over at Google Groups. The design and example data is by Jordan Boone.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
background-color: #525354;
max-width: 90vw; /* 90% of view width */
max-height: 90vh; /* 90% of view height */
}
text {
font-family: sans-serif;
font-size: 12px;
fill: #ffffff;
}
circle {
stroke: none;
fill: #606162;
}
.instrument path {
stroke: #525354;
stroke-width: 1;
}
.group path {
stroke: #ffffff;
stroke-width: 1;
fill: none;
}
g.name {
transform: rotate(-90deg); /* Align text with angle 0 */
}
.name text {
text-anchor: end; /* Horizontal align */
alignment-baseline: middle; /* Vertical align */
}
.group text {
text-anchor: middle;
}
.family1 {
fill: #accf5d;
}
.family2 {
fill: #e17173;
}
.family3 {
fill: #65c4be;
}
.family4 {
fill: #f59640;
}
</style>
<svg viewBox="0 0 500 500">
<!-- Perform all drawing and calculations from center of SVG -->
<g transform="translate(250,250)">
<!-- Add background circle -->
<circle data-attr-r="{{backgroundRadius}}"></circle>
<!-- Repeat for every instrument -->
<g data-repeat="{{d}}">
<!-- Rotate instrument into position (all calculations are relative to angle 0 from here on) -->
<g class="instrument" data-attr-transform="{{`rotate(${partWidthInDegrees * i})`}}">
<!-- Repeat for every difficulty level -->
<g data-attr-class="{{'family' + families[d.family]}}" data-repeat="{{d3.range(1, d.difficulty + 1)}}">
<!-- Draw single wedge at current difficulty level -->
<path data-attr-d="{{wedgeArc(d)}}"></path>
</g>
<!-- Draw instrument name -->
<g class="name">
<text data-attr-x="{{textPosition}}" data-attr-transform="{{`rotate(${partWidthInDegrees / 2})`}}">{{d.name}}</text>
</g>
</g>
</g>
<!-- Repeat for every group -->
<g data-repeat="{{groups(d)}}">
<g class="group">
<path id="{{d.id}}" data-attr-d="{{groupArc(d)}}"></path>
<text dy="-10">
<!-- Align the following text along the arc above (reference it using xlink:href) -->
<!-- Safari needs the xlink namespace to function correctly -->
<textPath xlink:data-attr-href="{{'#' + d.id}}" startOffset="50%">{{d.name}}</textPath>
</text>
</g>
</g>
</g>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-template-plugin/build/d3-template.min.js"></script>
<script>
var width = 500; // viewbox sets fixed width/height
var height = 500;
var margin = {top: 20, right: 20, bottom: 20, left: 20};
// Calculate some sizes
var maxRadius = Math.min(
(width - margin.left - margin.right) / 2,
(height - margin.top - margin.bottom) / 2
);
var minLevelRadius = maxRadius * .2; // At 20% of max radius
var maxLevelRadius = maxRadius * .75; // At 75% of max radius
var groupRadius = maxRadius * .9; // At 90% of max radius
var backgroundRadius = maxLevelRadius * 1.05; // Extend a bit outside level radius
var textPosition = maxLevelRadius * 1.1; // Extend a bit outside level radius
var groupPaddingAngle = 0.04;
// Global variables and functions used within template (values will be assigned later)
var partWidthInDegrees = 0;
var partWidthInRadians = 0;
var families = {};
var radiusScale = null;
var wedgeArcGenerator = null;
var groupTextArcGenerator = null;
function groups(d) {
// Create an array of groups. Every group consists of an id, the name of the
// group, the number of instruments within the group and the start (index)
// of the group. The count specifies the size of the arc shown 'above' the group.
// The start index specifies where the arc should start.
// The id is used to reference the arc (to allow the text to be put onto the arc
// of the group).
return d.reduce(function(groups, instrument, i) {
// Check if current instrument has same group as last time
if(groups.length > 0 && groups[groups.length - 1].name === instrument.group) {
// Increase group counter
groups[groups.length - 1].count++;
} else {
// Add new group
groups.push({
id: 'group' + groups.length,
name: instrument.group,
count: 1,
start: i
});
}
return groups;
}, []);
}
function groupArc(d) {
// Create path data for group arc
// HACK: Replace everything from first straight line onward. This leaves the
// single arc 'curve' as data. This hack assumes some knowledge about the
// way the data is generated and might therefore fail if the arc generator
// is implemented differently.
// Alternative would be to do the math here directly.
var arcData = groupTextArcGenerator({
startAngle: d.start * partWidthInRadians,
endAngle: (d.start + d.count) * partWidthInRadians
});
// Replace all line (and other) commands
arcData = arcData
.replace(/[Ll][^Aa]*/, "")
;
return arcData;
}
function wedgeArc(d) {
return wedgeArcGenerator({
innerRadius: radiusScale(d),
outerRadius: radiusScale(d) + radiusScale.bandwidth()
});
}
// Read in data and render onto template
d3.csv("instruments.csv", function(error, instruments) {
// Convert/extract instruments data
var familyColorIndex = 1;
instruments.forEach(function(instrument) {
// Convert string to integer
instrument.difficulty = +instrument.difficulty;
// Give every family unique color index
if(!families[instrument.family]) {
families[instrument.family] = familyColorIndex;
familyColorIndex++;
}
});
// Sort instruments (group first, then family, then name)
// This assumes a family is always part of the same group
// (since family specifies color and colors should be next to each other for easthetics)
instruments.sort(function(a, b) {
var result = a.group.localeCompare(b.group);
if(result === 0) {
result = a.family.localeCompare(b.family);
if(result === 0) {
result = a.name.localeCompare(b.name);
}
}
return result;
});
// Create scale for radius wedges (donut parts)
// See band scales: https://github.com/d3/d3-scale#band-scales
var difficultyExtent = d3.extent(instruments, function(instrument) { return instrument.difficulty; });
var difficultyRange = d3.range(difficultyExtent[0], difficultyExtent[1] + 1); // + 1 since 'stop' is not in range
radiusScale = d3.scaleBand()
.domain(difficultyRange)
.range([minLevelRadius, maxLevelRadius])
.paddingInner(.2)
;
// Create arc generator for wedges (donut parts)
// See arcs: https://github.com/d3/d3-shape#arcs
partWidthInDegrees = 360 / instruments.length;
partWidthInRadians = 2 * Math.PI / instruments.length;
wedgeArcGenerator = d3.arc()
.startAngle(0)
.endAngle(partWidthInRadians)
;
// Create arc generator for group text
groupTextArcGenerator = d3.arc()
.innerRadius(0) // This will in fact create pie piece (see "groupArc" function above for further explanation)
.outerRadius(groupRadius)
.padAngle(groupPaddingAngle)
;
// Create template and render data onto template
d3.select("svg")
.template()
.render(instruments)
;
});
</script>
name difficulty family group
Timpani 1 percussion drum
Chimes 3 percussion bell
Bodhran 2 percussion drum
Classic guitar 4 string strummed
Bouzouki 4 string strummed
Cittern 3 string strummed
Lute 3 string strummed
Cornet 3 wind trumpets
Bugle 3 wind trumpets
Long trumpet 2 wind trumpets
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment