Skip to content

Instantly share code, notes, and snippets.

@bwswedberg
Last active March 13, 2019 20:33
Show Gist options
  • Save bwswedberg/bec2f9531578441a82413ca2854b1593 to your computer and use it in GitHub Desktop.
Save bwswedberg/bec2f9531578441a82413ca2854b1593 to your computer and use it in GitHub Desktop.
Filled Donut Chart

Experimental chart where the fill of the donut is somewhat proportional to a value--much like a column in a bar chart. This chart could be improved by making the circle fill proportional to the circle. The circle fill is a clipped rectangle, and the height of the fill is proportional to the rectangle--NOT the circle, which is somewhat misleading.

Related:

(function(d3) {
d3.sineWave = function() {
var amplitude = 1;
var wavelength = 2;
var phase = 2
var samplesCount = 20;
var xTransform = function(d) { return d; };
var yTransform = function(d) { return d; };
function sineWave() { }
sineWave.amplitude = function(_) {
if (!arguments.length) return amplitude;
amplitude = _;
return sineWave;
}
sineWave.wavelength = function(_) {
if (!arguments.length) return wavelength;
wavelength = _;
return sineWave;
}
sineWave.phase = function(_) {
if (!arguments.length) return phase;
phase = _;
return sineWave;
}
sineWave.samplesCount = function(_) {
if (!arguments.length) return samplesCount;
samplesCount = _;
return sineWave;
}
/**
* Transforms x value post calculation. Transformation function is passed a x value that ranges between 0 and 1 inclusively.
* @param {function} _ Transforming function
*/
sineWave.xTransform = function(_) {
if (!arguments.length) return xTransform;
xTransform = _;
return sineWave;
}
/**
* Transforms y value post calculation. Transformation function reflects sine parameters. If amplitude is 1 values will range from -1 to 1.
* @param {function} _ Transforming function
*/
sineWave.yTransform = function(_) {
if (!arguments.length) return yTransform;
yTransform = _;
return sineWave;
}
sineWave.samples = function() {
return d3.range(samplesCount).map(function(d) {
var t = d / (samplesCount - 1);
var y = Math.sin(2 * Math.PI * t * wavelength + phase) * amplitude;
return [xTransform(t), yTransform(y)];
});
}
return sineWave;
}
}(window.d3 ));
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
display: block;
width: 100%;
}
text {
text-anchor: middle;
font-size: 112px;
font-weight: bold;
font-family: sans-serif;
fill: rgba(30,30,30,0.75);
}
</style>
<svg viewBox="0 0 960 500"></svg>
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="./d3-sine-wave.js"></script>
<script>
var svg = d3.select('svg')
var viewBox = svg.attr('viewBox').split(' ');
var width = +viewBox[2];
var height = +viewBox[3];
var donutWidth = 30;
var donutPadding = 2;
var value = 0.5;
var valueScale = d3.scaleLinear().domain([0, 1]).range([height - donutWidth, donutWidth]);
var line = d3.line()
.curve(d3.curveBasis);
var waves = [
d3.sineWave()
.amplitude(10)
.wavelength(2)
.phase(0)
.xTransform(function(d) { return d * width; })
.yTransform(function(d) { return d + valueScale(value); }),
d3.sineWave()
.amplitude(10)
.wavelength(1.5)
.phase(3)
.xTransform(function(d) { return d * width; })
.yTransform(function(d) { return d + valueScale(value) + 15; }),
d3.sineWave()
.amplitude(10)
.wavelength(1)
.phase(6)
.xTransform(function(d) { return d * width; })
.yTransform(function(d) { return d + valueScale(value) + 30; })
];
var arc = d3.arc()
.innerRadius(Math.min(width / 2 - donutWidth + donutPadding, height / 2 - donutWidth + donutPadding))
.outerRadius(Math.min(width / 2, height / 2))
.startAngle(0)
.endAngle(Math.PI * 2);
var defs = svg.append('defs');
defs.append('clipPath')
.attr('id', 'donut-hole-clip-path')
.append('circle')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('r', Math.min(width / 2 - donutWidth, height / 2 - donutWidth));
defs.append('clipPath')
.attr('id', 'donut-clip-path')
.append('path')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
.attr('d', arc());
var sine = svg.append('g')
.attr('class', 'sine')
.attr('clip-path', 'url(#donut-hole-clip-path');
var donut = svg.append('g')
.attr('class', 'donut');
donut.append('rect')
.attr('width', width)
.attr('height', height)
.attr('clip-path', 'url(#donut-clip-path)')
.style('fill', '#ccc');
var donutScale = d3.scaleLinear().domain([0, 1]).range([donutWidth + donutPadding, height - donutWidth - donutPadding]);
var filledDonut = donut.append('rect')
.attr('y', donutScale(1 - value))
.attr('height', donutScale(value))
.attr('width', width)
.attr('clip-path', 'url(#donut-clip-path)')
.style('fill', d3.color('steelblue').darker(2));
var textValue = svg.append('text')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
.attr('dy', '0.2em')
.text(100 * value + '%');
update(value);
var init = false;
function update(val) {
value = val;
waves.forEach(function(wave) {
wave.phase(10 * Math.random() - 5 + wave.phase());
});
var path = sine.selectAll('path')
.data([
{ fill: d3.color('steelblue').brighter(), samples: waves[0].samples() },
{ fill: d3.color('steelblue'), samples: waves[1].samples() },
{ fill: d3.color('steelblue').darker(), samples: waves[2].samples() }
]);
var t = d3.transition()
.delay(init ? 1000 : 0)
.duration(3000)
.on('end', update.bind(null, Math.random()));
path.enter().append('path')
.style('fill', function(d) { return d.fill; })
.merge(path)
.transition(t)
.ease(d3.easeCubicInOut)
.attr('d', function(d) {
return line(d.samples) + 'L' + width + ',' + height + 'L0,' + height + 'L' + d.samples[0][0] + ',' + d.samples[0][1];
});
filledDonut.transition(t)
.attr('y', donutScale(1 - value))
.attr('height', donutScale(value));
textValue.transition(t)
.on('start', function() {
d3.active(this)
.tween('text', function() {
var format = d3.format('.0%');
var interpolate = d3.interpolateNumber(textValue.text().replace(/%/g, "") / 100, value);
return function(time) {
textValue.text(format(interpolate(time)));
};
});
});
init = true;
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment