Skip to content

Instantly share code, notes, and snippets.

@NPashaP
Last active May 13, 2023 06:25
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save NPashaP/9994181 to your computer and use it in GitHub Desktop.
Save NPashaP/9994181 to your computer and use it in GitHub Desktop.
3D Donut
license: gpl-3.0
!function(){
var Donut3D={};
function pieTop(d, rx, ry, ir ){
if(d.endAngle - d.startAngle == 0 ) return "M 0 0";
var sx = rx*Math.cos(d.startAngle),
sy = ry*Math.sin(d.startAngle),
ex = rx*Math.cos(d.endAngle),
ey = ry*Math.sin(d.endAngle);
var ret =[];
ret.push("M",sx,sy,"A",rx,ry,"0",(d.endAngle-d.startAngle > Math.PI? 1: 0),"1",ex,ey,"L",ir*ex,ir*ey);
ret.push("A",ir*rx,ir*ry,"0",(d.endAngle-d.startAngle > Math.PI? 1: 0), "0",ir*sx,ir*sy,"z");
return ret.join(" ");
}
function pieOuter(d, rx, ry, h ){
var startAngle = (d.startAngle > Math.PI ? Math.PI : d.startAngle);
var endAngle = (d.endAngle > Math.PI ? Math.PI : d.endAngle);
var sx = rx*Math.cos(startAngle),
sy = ry*Math.sin(startAngle),
ex = rx*Math.cos(endAngle),
ey = ry*Math.sin(endAngle);
var ret =[];
ret.push("M",sx,h+sy,"A",rx,ry,"0 0 1",ex,h+ey,"L",ex,ey,"A",rx,ry,"0 0 0",sx,sy,"z");
return ret.join(" ");
}
function pieInner(d, rx, ry, h, ir ){
var startAngle = (d.startAngle < Math.PI ? Math.PI : d.startAngle);
var endAngle = (d.endAngle < Math.PI ? Math.PI : d.endAngle);
var sx = ir*rx*Math.cos(startAngle),
sy = ir*ry*Math.sin(startAngle),
ex = ir*rx*Math.cos(endAngle),
ey = ir*ry*Math.sin(endAngle);
var ret =[];
ret.push("M",sx, sy,"A",ir*rx,ir*ry,"0 0 1",ex,ey, "L",ex,h+ey,"A",ir*rx, ir*ry,"0 0 0",sx,h+sy,"z");
return ret.join(" ");
}
function getPercent(d){
return (d.endAngle-d.startAngle > 0.2 ?
Math.round(1000*(d.endAngle-d.startAngle)/(Math.PI*2))/10+'%' : '');
}
Donut3D.transition = function(id, data, rx, ry, h, ir){
function arcTweenInner(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return pieInner(i(t), rx+0.5, ry+0.5, h, ir); };
}
function arcTweenTop(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return pieTop(i(t), rx, ry, ir); };
}
function arcTweenOuter(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return pieOuter(i(t), rx-.5, ry-.5, h); };
}
function textTweenX(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return 0.6*rx*Math.cos(0.5*(i(t).startAngle+i(t).endAngle)); };
}
function textTweenY(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) { return 0.6*rx*Math.sin(0.5*(i(t).startAngle+i(t).endAngle)); };
}
var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);
d3.select("#"+id).selectAll(".innerSlice").data(_data)
.transition().duration(750).attrTween("d", arcTweenInner);
d3.select("#"+id).selectAll(".topSlice").data(_data)
.transition().duration(750).attrTween("d", arcTweenTop);
d3.select("#"+id).selectAll(".outerSlice").data(_data)
.transition().duration(750).attrTween("d", arcTweenOuter);
d3.select("#"+id).selectAll(".percent").data(_data).transition().duration(750)
.attrTween("x",textTweenX).attrTween("y",textTweenY).text(getPercent);
}
Donut3D.draw=function(id, data, x /*center x*/, y/*center y*/,
rx/*radius x*/, ry/*radius y*/, h/*height*/, ir/*inner radius*/){
var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);
var slices = d3.select("#"+id).append("g").attr("transform", "translate(" + x + "," + y + ")")
.attr("class", "slices");
slices.selectAll(".innerSlice").data(_data).enter().append("path").attr("class", "innerSlice")
.style("fill", function(d) { return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){ return pieInner(d, rx+0.5,ry+0.5, h, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".topSlice").data(_data).enter().append("path").attr("class", "topSlice")
.style("fill", function(d) { return d.data.color; })
.style("stroke", function(d) { return d.data.color; })
.attr("d",function(d){ return pieTop(d, rx, ry, ir);})
.each(function(d){this._current=d;});
slices.selectAll(".outerSlice").data(_data).enter().append("path").attr("class", "outerSlice")
.style("fill", function(d) { return d3.hsl(d.data.color).darker(0.7); })
.attr("d",function(d){ return pieOuter(d, rx-.5,ry-.5, h);})
.each(function(d){this._current=d;});
slices.selectAll(".percent").data(_data).enter().append("text").attr("class", "percent")
.attr("x",function(d){ return 0.6*rx*Math.cos(0.5*(d.startAngle+d.endAngle));})
.attr("y",function(d){ return 0.6*ry*Math.sin(0.5*(d.startAngle+d.endAngle));})
.text(getPercent).each(function(d){this._current=d;});
}
this.Donut3D = Donut3D;
}();
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
width: 960px;
height: 500px;
position: relative;
}
path.slice{
stroke-width:2px;
}
polyline{
opacity: .3;
stroke: black;
stroke-width: 2px;
fill: none;
}
svg text.percent{
fill:white;
text-anchor:middle;
font-size:12px;
}
</style>
<body>
<button onClick="changeData()">Change Data</button>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="Donut3D.js"></script>
<script>
var salesData=[
{label:"Basic", color:"#3366CC"},
{label:"Plus", color:"#DC3912"},
{label:"Lite", color:"#FF9900"},
{label:"Elite", color:"#109618"},
{label:"Delux", color:"#990099"}
];
var svg = d3.select("body").append("svg").attr("width",700).attr("height",300);
svg.append("g").attr("id","salesDonut");
svg.append("g").attr("id","quotesDonut");
Donut3D.draw("salesDonut", randomData(), 150, 150, 130, 100, 30, 0.4);
Donut3D.draw("quotesDonut", randomData(), 450, 150, 130, 100, 30, 0);
function changeData(){
Donut3D.transition("salesDonut", randomData(), 130, 100, 30, 0.4);
Donut3D.transition("quotesDonut", randomData(), 130, 100, 30, 0);
}
function randomData(){
return salesData.map(function(d){
return {label:d.label, value:1000*Math.random(), color:d.color};});
}
</script>
</body>
@LynnQin
Copy link

LynnQin commented Aug 26, 2016

Hi NPashaP, Really great implementation. I'd also like to know about the license.

@saikiran9949
Copy link

i tried to implement tool tip with above code. but it seems to be not working. If Possible write code related Tooltip and legend.

@saikiran9949
Copy link

if values more than 5 entire d3 chart is disturbing.
EX; Donut3D.draw("quotesDonut", randomData(), 450, 150, 250, 150, 50, 10,9);
try with above code

@henrix343
Copy link

Nice! Remember that to use this chart you must download the OLDER version of d3.js, this does not work on v4.

@fislamcsr
Copy link

fislamcsr commented Aug 21, 2019

Nice! Remember that to use this chart you must download the OLDER version of d3.js, this does not work on v4.

For the latest version of d3.js v5 change d3.layout.pie() to d3.pie() at two lines (77 and 95) in Donut3D.js as follows:

77: var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);

77: var _data = d3.pie().sort(null).value(function(d) {return d.value;})(data);

95: var _data = d3.layout.pie().sort(null).value(function(d) {return d.value;})(data);

95: var _data = d3.pie().sort(null).value(function(d) {return d.value;})(data);

Do not forget to change the d3.js version v3 to v5 or v4 at line 28 in index.html as follows:
28: <script src="http://d3js.org/d3.v3.min.js"></script>
28: <script src="http://d3js.org/d3.v5.min.js"></script> or <script src="http://d3js.org/d3.v4.min.js"></script>

This is because of the namespace flattening since v4, please refer to the Changes in D3 5.0 https://github.com/d3/d3/blob/master/CHANGES.md#shapes-d3-shape

@speciale
Copy link

speciale commented Jan 9, 2020

You can make it responsive too
var svg = d3.select(".donut").append("svg").attr("width",100+"%").attr("height",100+"%").attr("viewBox","0 0 520 390");

@amitogilvy
Copy link

Where is the label in this graph??? how can identify which % is of which label???

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment