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.
Last active
September 30, 2018 19:17
-
-
Save ErikOnBike/b542468559630c116e44a3578d9660d9 to your computer and use it in GitHub Desktop.
Donut Chart Design
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 | |
height: 500 | |
scrolling: yes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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