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>
@langdonx
Copy link

There seems to be an odd bug that I've gotten around with a hack. I believe the bug is within d3.layout.pie, but I'm not entirely sure. You get really strange rendering with the following set of data:

function randomData(){
    return [
        { color: '#009900', value: 100 },
        { color: '#FF0000', value: 0 },
        { color: '#CC0000', value: 0 },
        { color: '#990000', value: 0 }
    ];
}

And the weird hack to fix it, is inside of function pieTop (in Donut3D.js):

// super weird hack to hopefully prevent whiteness
d.endAngle = parseFloat(d.endAngle.toFixed(3));

I assumed it might have some side effects, but I haven't seen any yet.

Thanks for your work, I really love this implementation!

@jaredevantabor
Copy link

Hey @NPashaP this is really awesome, thanks for sharing! I have a quick question about your license, Is your Gist shared under any particular license?

@hello-alf
Copy link

I have an issue when have 100% of data IE9 not have color??

@ironmouser
Copy link

Hi NPashaP. Great Gist! What is the license associated with your code? Thanks.

@anzeanzic
Copy link

anzeanzic commented Jul 4, 2016

Hi, great piece of code for creating a 3D pie chart. Is the code shared under some licence? Is it free to use?

@sagarCLMTest
Copy link

Hi NPashaP, Great implementation, but just want to confirm that, Is it free to use? Is the code shared under some license?

@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