I have a donut chart that is being used as a way to show progression. I don't have a way to show you the donut chart, but the code is simple enough to copy and paste.
I added the code to show you an example. I've tried various unreasonable methods to make the transition work the first time. But for some reason it's still not working. All examples online are pretty similar so I'm not really sure why this is happening.
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
/*path.enter().append("path")
.attr("fill", function (d, i) {
return color[i];
})
.attr("d", arc);*/
path.transition().duration(100).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
If we look at your tween function we'll see a problem:
var i = d3.interpolate(this._current, a);
this._current = i(0);
this.current is undefined when you first start transtioning - so how is D3 to interpolate between undefined and an object contianing arc properties? It doesn't. Resulting in the non-transition you are seeing. Set this._current when appending the arcs:
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
Now when you update the circle, there is a valid start point for the interpolator and you should see a transition:
var data = [95, 5];
var pie = d3.pie().sort(null);
var svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height"),
radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(60)
.outerRadius(radius);
function createdonut() {
g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
//Inner SVG Circle
svg.append("svg:circle")
.attr("cx", width / 2)
.attr("cy", height / 2)
.attr("r", 60)
.style("fill", "#ead4d4")
.append("g");
var color = d3.scaleOrdinal(['#4daf4a', '#377eb8', '#ff7f00', '#984ea3', '#e41a1c']);
//Generate groups
var arcs = g.selectAll("arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc")
//Draw arc paths
arcs.append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function(d) {
this._current = d;
})
.on('mouseover', mouseover);
function mouseover(d, i) {
$('#percentage').html(i.data + ' units');
}
}
function updateDoNut(update) {
data[0] = data[0] - update;
data[1] = data[1] + update;
var path = d3.select("svg").selectAll("path").data(pie(data));
path.transition().duration(2000).attrTween("d", arcTween);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
createdonut();
//updateDoNut(0);
var inter = setInterval(function () { updateDoNut(5); }, 3000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v6.min.js"></script>
<div id="my_dataviz">
<svg width="300" height="200"> </svg>
<div id="percentage">0 units</div>
</div>
Why doesn't this interpolation between undefined and an object generate an error? Well D3-interpolate will try to interpolate very hard. In this case, between undefined and an object, it'll use d3-interpolateObject, which will interpolate as follows:
For each property in b, if there exists a corresponding property in a,
a generic interpolator is created for the two elements using
interpolate. If there is no such property, the static value from b is
used in the template. (docs)
So, as there are no properties in undefined, the interpolator just uses a static value for every point in the interpolation, hence the lack of a transition on the first update: every interpolated point is the same: the end point values.
Related
I've recently began trying to teach myself D3, and I'm to get my head round the enter, update, exit paradigm.
Below I have an example of some progress circles I'm trying to work with;
http://plnkr.co/edit/OoIL8v6FemzjzoloJxtQ?p=preview
Now, as the aim here is to update the circle path without deleting them, I believe I shouldn't be using the exit function? In which case, I was under the impression that I could update my data source inside a new function and then call for the path transition, and I would get my updated value. However, this is not the case.
I was wondering if someone could help me out and show me where I'm going wrong?
var dataset = [{
"vendor-name": "HP",
"overall-score": 45
}, {
"vendor-name": "CQ",
"overall-score": 86
}];
var dataset2 = [{
"vendor-name": "HP",
"overall-score": 22
}, {
"vendor-name": "CQ",
"overall-score": 46
}];
var width = 105,
height = 105,
innerRadius = 85;
var drawArc = d3.svg.arc()
.innerRadius(innerRadius / 2)
.outerRadius(width / 2)
.startAngle(0);
var vis = d3.select("#chart").selectAll("svg")
.data(dataset)
.enter()
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
vis.append("circle")
.attr("fill", "#ffffff")
.attr("stroke", "#dfe5e6")
.attr("stroke-width", 1)
.attr('r', width / 2);
vis.append("path")
.attr("fill", "#21addd")
.attr('class', 'arc')
.each(function(d) {
d.endAngle = 0;
})
.attr('d', drawArc)
.transition()
.duration(1200)
.ease('linear')
.call(arcTween);
vis.append('text')
.text(0)
.attr("class", "perc")
.attr("text-anchor", "middle")
.attr('font-size', '36px')
.attr("y", +10)
.transition()
.duration(1200)
.tween(".percentage", function(d) {
var i = d3.interpolate(this.textContent, d['overall-score']),
prec = (d.value + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round + "%";
};
});
function updateChart() {
vis = vis.data(dataset2)
vis.selectAll("path")
.transition()
.duration(1200)
.ease('linear')
.call(arcTween);
vis.selectAll('text')
.transition()
.duration(1200)
.tween(".percentage", function(d) {
var i = d3.interpolate(this.textContent, d['overall-score']),
prec = (d.value + "").split("."),
round = (prec.length > 1) ? Math.pow(10, prec[1].length) : 1;
return function(t) {
this.textContent = Math.round(i(t) * round) / round + "%";
};
});
}
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(0, 360 * (d['overall-score'] / 100) * Math.PI / 180);
return function(t) {
d.endAngle = interpolate(t)
return drawArc(d);
};
});
}
Any help/advice is much appreciated!
Thanks all
You need to refresh your data through the DOM - svg > g > path :
// SET DATA TO SVG
var svg = d3.selectAll("svg")
.data(selectedDataset)
// SET DATA TO G
var g = svg.selectAll('g')
.data(function(d){return [d];})
// SET DATA TO PATH
var path = g.selectAll('path')
.data(function(d){ return [d]; });
Storing the d3 DOM data bind object for each step you can have control of the enter(), extit(), and transition() elements. Put changing attributes of elements in the transition() function:
// PATH ENTER
path.enter()
.append("path")
.attr("fill", "#21addd")
.attr('class', 'arc')
// PATH TRANSITION
path.transition()
.duration(1200)
.ease('linear')
.attr('d', function(d){ console.log(d);drawArc(d)})
.call(arcTween);
http://plnkr.co/edit/gm2zpDdBdQZ62YHhDbLb?p=preview
I am using angular and d3 to create a donut (in a directive).
I can quite simply give the filled area a colour (in this plunker it is blue). But what i want to do is have the SVG change its colours smoothly from:
0% - 33.3% - red
33.4% - 66.66% - orange
66.7% - 100% green
Directive:
app.directive('donutDirective', function() {
return {
restrict: 'E',
scope: {
radius: '=',
percent: '=',
text: '=',
},
link: function(scope, element, attrs) {
var radius = scope.radius,
percent = scope.percent,
percentLabel = scope.text,
format = d3.format(".0%"),
progress = 0;
var svg = d3.select(element[0])
.append('svg')
.style('width', radius/2+'px')
.style('height', radius/2+'px');
var donutScale = d3.scale.linear()
.domain([0, 100])
.range([0, 2 * Math.PI]);
//var color = "#5599aa";
var color = "#018BBB";
var data = [
[0,100,"#b8b5b8"],
[0,0,color]
];
var arc = d3.svg.arc()
.innerRadius(radius/6)
.outerRadius(radius/4)
.startAngle(function(d){return donutScale(d[0]);})
.endAngle(function(d){return donutScale(d[1]);});
var text = svg.append("text")
.attr("x",radius/4)
.attr("y",radius/4)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.attr("font-size","14px")
.style("fill","black")
.attr("text-anchor", "middle")
.text(percentLabel);
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d){return d[2];})
.attr("d", arc)
.each(function(d) {
this._current = d;
// console.log(this._current)
;});
// update the data!
data = [
[0,100,"#b8b5b8"],
[0,percent,color]
];
path
.data(data)
.attr("transform", "translate("+radius/4+","+radius/4+")")
.transition(200).duration(2150).ease('linear')
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
// console.log(this._current);
return function(t) {
text.text( format(i2(t) / 100) );
return arc(i(t));
};
});
}
};
});
Plunker: http://plnkr.co/edit/8qGMeQkmM08CZxZIVRei?p=preview
First give Id to the path like this:
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d){return d[2];})
.attr("d", arc)
.attr("id", function(d,i){return "id"+i;})//give id
Then inside the tween pass the condition and change the color of the path
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
return function(t) {
if(i2(t) < 33.3)
d3.selectAll("#id1").style("fill", "red")
else if(i2(t) < 66.6)
d3.selectAll("#id1").style("fill", "orange")
else if(i2(t) > 66.6)
d3.selectAll("#id1").style("fill", "green")
text.text( format(i2(t) / 100) );
return arc(i(t));
};
});
Working code here
EDIT
Inside your directive you can make gradient inside your defs like this:
var defs = svg.append("defs");
var gradient1 = defs.append("linearGradient").attr("id", "gradient1");
gradient1.append("stop").attr("offset", "0%").attr("stop-color", "red");
gradient1.append("stop").attr("offset", "25%").attr("stop-color", "orange");
gradient1.append("stop").attr("offset", "75%").attr("stop-color", "green");
Then in the path you can define the gradient like this:
var path = svg.selectAll("path")
.data(data)
.enter()
.append("path")
.style("fill", function(d, i) {
if (i == 0) {
return d[2];
} else {
return "url(#gradient1)";
}
})
Working code here
Hope this helps!
i want to do is have the SVG change its colours smoothly from:
0% - 33.3% - red
33.4% - 66.66% - orange
66.7% - 100% green
Assuming that you want a color transition/scale like this one:
See working code for this: http://codepen.io/anon/pen/vLVmyV
You can smothly make the color transition using a d3 linear scale like this:
//Create a color Scale to smothly change the color of the donut
var colorScale = d3.scale.linear().domain([0,33.3,66.66,100]).range(['#cc0000','#ffa500','#ffa500','#00cc00']);
Then, when you update the path (with the attrTween) to make the filling animation, take only the Path the represents the filled part of the donut, lets call it colorPath and change the fill of it adding the following like in the tween:
//Set the color to the path depending on its percentage
//using the colorScale we just created before
colorPath.style('fill',colorScale(i2(t)))
Your attrTween will look like this:
colorPath.data([[0,percent,color]])
.transition(200).duration(2150).ease('linear')
.attrTween("d", function (a) {
var i = d3.interpolate(this._current, a);
var i2 = d3.interpolate(progress, percent)
this._current = i(0);
// console.log(this._current);
return function(t) {
text.text( format(i2(t) / 100) );
colorPath.style('fill',colorScale(i2(t)))
return arc(i(t));
};
});
Please note that we only update the data for the colorPath: colorPath.data([[0,percent,color]])
The whole working example is right here: http://plnkr.co/edit/ox82vGxhcaoXJpVpUel1?p=preview
Hi there I currently have a template helper that returns me an array with various values used to generate different rows in a table in my HTML.
<template name="stop">
{{#each thumb}}
<tr>
<td>
<h2> Do you like this product? </h2>
<h2>{{text}}</h2>
<svg id="donutChart"> </svg>
</td>
</tr>
{{/each}}
</template>
It also contains a svg tag which I also want to generate a graph for each element generated as a table row and this is what the template helper looks like.
Template.stop.helpers({
'thumb': function(data) {
var result = tweetImages.findOne();
var newResult = [];
for (var i = 0; i < result.data.length; i++) {
newResult[i] = {
data: result.data[i],
text: result.text[i]
};
}
console.log(newResult)
return newResult;
}
I'm trying to create a pie reactive pie chart for each element in the table however I don't seem to be able to access the svg in the stop template.
The d3 code works fine outside that table but cant seem to be generated for each element of the table because it can't access the svg element.
Template.donutChart.rendered = function() {
//my d3 code is here
//Width and height
var w = 300;
var h = 300;
var center = w / 2;
var outerRadius = w / 2;
var innerRadius = 0;
var radius = 150;
var arc = d3.svg.arc()
.innerRadius(40)
.outerRadius(radius + 10 - 25);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.data;
});
//Create SVG element
var svg = d3.select("#donutChart")
.attr("width", w)
.attr("height", h)
.attr("transform", "translate(" + 200 + "," + 100 + ")");
// GROUP FOR CENTER TEXT
var center_group = svg.append("svg:g")
.attr("class", "ctrGroup")
.attr("transform", "translate(" + (w / 2) + "," + (h / 2) + ")");
// CENTER LABEL
var pieLabel = center_group.append("svg:text")
.attr("dy", ".35em").attr("class", "chartLabel")
.attr("text-anchor", "middle")
.text("Clothes")
.attr("fill", "white");
Deps.autorun(function() {
var modifier = {
fields: {
value: 1
}
};
Deps.autorun(function() {
var arcs = svg.selectAll("g.arc")
.data(pie(players))
var arcOutter = d3.svg.arc()
.innerRadius(outerRadius - 10)
.outerRadius(outerRadius);
var arcPhantom = d3.svg.arc()
.innerRadius(-180)
.outerRadius(outerRadius + 180);
var newGroups =
arcs
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + 150 + "," + 150 + ")")
//Set up outter arc groups
var outterArcs = svg.selectAll("g.outter-arc")
.data(pie(players))
.enter()
.append("g")
.attr("class", "outter-arc")
.attr("transform", "translate(" + 150 + ", " + 150 + ")");
//Set up phantom arc groups
var phantomArcs = svg.selectAll("g.phantom-arc")
.data(pie(players))
.enter()
.append("g")
.attr("class", "phantom-arc")
.attr("transform", "translate(" + center + ", " + center + ")");
outterArcs.append("path")
.attr("fill", function(d, i) {
return slickColor[i];
})
.attr("fill-opacity", 0.85)
.attr("d", arcOutter).style('stroke', '#0ca7d2')
.style('stroke-width', 2)
//Draw phantom arc paths
phantomArcs.append("path")
.attr("fill", 'white')
.attr("fill-opacity", 0.1)
.attr("d", arcPhantom).style('stroke', '#0ca7d2')
.style('stroke-width', 5);
//Draw arc paths
newGroups
.append("path")
.attr("fill", function(d, i) {
return slickColor[i];
})
.attr("d", arc);
//Labels
newGroups
.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d) {
return d.value;
})
.style("font-size", function(d) {
return 24;
})
svg.selectAll("g.phantom-arc")
.transition()
.select('path')
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
});
arcs
.transition()
.select('path')
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
});
arcs
.transition()
.select('text')
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.text(function(d) {
return d.value;
})
.attr("fill", function(d, i) {
return textColor[i];
})
arcs
.exit()
.remove();
});
});
}
}
I can't seem to find much information on using d3.js or SVG's within a templates #each wrapper. Any help would be truly appreciated.
I would suggest wrapping your d3 code in a Deps.autorun() function as what's most likely happening is your data isn't available yet when your pie is bound to the data function and thus doesn't render anything.
Template.donutChart.rendered = function() {
Tracker.autorun(function () {
//all d3 code
});
}
You look like you're using autoruns further down but not for the bit that gets your data.
The code below draws 1 pie chart and a legend on the left side of the screen. Right now, I am trying to draw another pie chart with legend right next to the one on the left (same row). I've tried using multiple divs in the html to make this work, but I want a more pure d3 solution in which the duplication happens in the d3 code rather than in the html or css.
var w = 200;
var h = 200;
var r = h / 2;
var color = d3.scale.category20c();
var vis = d3.select(divId).append("svg:svg").data([descArray]).attr("width",w).attr("height", h).append("svg:g").attr("transform", "translate(" + r + "," + r + ")");
var pie = d3.layout.pie().value(function (d, i) {
return countArray[i];
});
// declare an arc generator function
var arc = d3.svg.arc().outerRadius(r);
// select paths, use arc generator to draw
var arcs = vis.selectAll("g.slice").data(pie).enter().append("svg:g").attr("class", "slice");
arcs.append("svg:path")
.on("click", function(d) {//clicking on individual arcs
arcs.selectAll("path").style("opacity", 1);//resets all arcs' opacity to 1
d3.select(this).style("opacity", 0.5);//sets clicked arc's opacity down
alert(d.data + " " + d.value);
})
.style("fill", function(d,i) { return color(i); })
.transition().delay(function(d, i) { return i * 100; }).duration(1000)
.attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle+0.7, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
})
.attr("fill", function (d, i) {
return color(i);
});
var legend = d3.select(divId).append("svg")
.attr("class", "legend")
.attr("width", r * 4)
.attr("height", r * 4)
.selectAll("g")
.data(color.domain().slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(230," + i * 27 + ")"; });
legend.append("rect")
.on("click", function(d) {
alert(d.data + " " + d.value);
})
.attr("width", 18)
.attr("height", 18)
.style("fill", function (d, i) {
return color(i);
})
put them in seperate divs but in the same SVG element
Presuming vis is your svgElement:
var firstChart = vis.append(div). // then put your first chart here
var secondChart = vis.append(div). // then put your second chart here
I created a multi-level pie chart but i am having trouble animate it on load.
Here is the JS that i tryied.The animation works fine on the first circle of the chart , but it hides the other 2.
Any help would be appreciated.Thanks:)
<script>
var dataset = {
final: [7000],
process: [1000, 1000, 1000, 7000],
initial: [10000],
};
var width = 660,
height = 500,
cwidth = 75;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("class","wrapper")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
var gs = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return Object.keys(dataset)[i];
});
var gsLabels = svg.selectAll("g.wrapper").data(d3.values(dataset)).enter()
.append("g")
.attr("id",function(d,i){
return "label_" + Object.keys(dataset)[i];
});
var count = 0;
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
if(Object.keys(dataset)[j] === "final"){
return arc.innerRadius(cwidth*j).outerRadius(cwidth*(j+1))(d);
}
else{
return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d);
}
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle+0.1, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
}
});
</script>
The main problem is that you're using the same arc generator for all of the different pie segments. That means that after the transition, all the segments will have the same inner and outer radii -- they are there, you just can't see them because they're obscured by the outer blue segment.
To fix this, use different arc generators for the different levels. You also need to initialise the d attribute to zero width (i.e. start and end angle the same) for the animation to work properly.
I've implemented a solution for this here where I'm saving an arc generator for each pie chart segment with the data assigned to that segment. This is a bit wasteful, as a single generator for each level would be enough, but faster to implement. The relevant code is below.
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) {
d._tmp = d.endAngle;
d.endAngle = d.startAngle;
if(Object.keys(dataset)[j] === "final"){
d.arc = d3.svg.arc().innerRadius(cwidth*j).outerRadius(cwidth*(j+1));
}
else{
d.arc = d3.svg.arc().innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1));
}
return d.arc(d);
})
.transition().delay(function(d, i, j) {
return i * 500;
}).duration(500)
.attrTween('d', function(d,x,y) {
var i = d3.interpolate(d.startAngle, d._tmp);
return function(t) {
d.endAngle = i(t);
return d.arc(d);
}
});