arctween and dataset change in donut chart with d3,js - javascript

With the click of a button, I want to add a new dataset to my doughnut chart and have it transition the new dataset. The code I've written sort of does that but it runs into an issue when the number of individual data within the dataset is different from the previous i.e. going from [1,2] to [1,2,3,4].
I think the issue is that I need to create a new path whenever there the new dataset has more data, and remove paths whenever it has less. However, when I try to append data in my click function, it will append it without removing the old paths and will overlap on the chart.
Here is a version without appending, where the arctween will work but there will be empty pie arcs because I don't append path (arctween works half the time):
http://jsfiddle.net/njrPF/1/
var pieW = 500;
var pieH = 500;
var innerRadius = 100;
var outerRadius = 200;
var results_pie = d3.layout.pie()
.sort(null);
var pie_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg_pie = d3.select("#pieTotal")
.attr("width", pieW)
.attr("height", pieH)
.append("g")
.attr("transform", "translate(" + pieW / 2 + "," + pieH / 2 + ")")
.attr("class", "piechart");
var pie_path = svg_pie.selectAll("path").data(results_pie([1, 2]))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
var pie_votes = [1, 2];
var pie_colors = ["#0f0", "#f00"];
$(svg_pie).bind("monitor", worker);
$(svg_pie).trigger("monitor");
function worker(event) {
pie_path = pie_path.data(results_pie(pie_votes))
.attr("fill", function (d, i) {
return pie_colors[i];
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
setTimeout(function () {
$(svg_pie).trigger("monitor");
}, 500);
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return pie_arc(i(t));
};
}
$('button').click(function () {
pie_votes = [];
pie_colors = [];
for (var i = 0; i < Math.floor(Math.random() * 6); i++) {
//sets new values on pie arcs
pie_votes.push(Math.floor(Math.random() * 10));
pie_colors.push("#" + (Math.floor(Math.random() * 16777215)).toString(16));
}
pie_path = pie_path.data(results_pie(pie_votes))
.attr("fill", function (d, i) {
return pie_colors[i]
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
});
Here is a version where I try to append new paths but they overlap:
http://jsfiddle.net/njrPF/3/
var pieW = 500;
var pieH = 500;
var innerRadius = 100;
var outerRadius = 200;
var results_pie = d3.layout.pie()
.sort(null);
var pie_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var svg_pie = d3.select("#pieTotal")
.attr("width", pieW)
.attr("height", pieH)
.append("g")
.attr("transform", "translate(" + pieW / 2 + "," + pieH / 2 + ")")
.attr("class", "piechart");
var pie_path = svg_pie.selectAll("path").data(results_pie([1, 2]))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return pie_arc(i(t));
};
}
$('button').click(function () {
pie_votes = [];
pie_colors = [];
for (var i = 0; i < Math.floor(Math.random() * 10); i++) {
//sets new values on pie arcs
pie_votes.push(Math.floor(Math.random() * 10));
pie_colors.push("#" + (Math.floor(Math.random() * 16777215)).toString(16));
}
pie_path = pie_path.data(results_pie(pie_votes))
.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d; }) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
pie_path.attr("fill", function (d, i) {
return pie_colors[i]
});
pie_path.transition().duration(500).attrTween("d", arcTween).each('end', function (d) {
if (d.value <= 0) {
this.remove();
}
});
});
Thanks in advance.

You need to handle the enter and exit selections as well as the update selection. See for example this tutorial. The relevant code in your case would be
pie_path = pie_path.data(results_pie(pie_votes));
pie_path.enter().append("path")
.attr("d", pie_arc)
.each(function (d) {
this._current = d;
}) // store the initial values
.attr("class", "vote_arc")
.attr("value", function (d, i) {
return (i - 1);
});
pie_path.exit().remove();
Complete example here.

Related

how to create expandable sunburst with d3

jsfiddle.net/spacehaz/jpzaj92o/31
I want to create sunburst and give it an event listener with a callback, that will expand it, not zoom! so the idea is to expand all deeper levels to 100% (and to hide all other branches starting from the root) ! I found the solution to do a zoom, but that is not a solution, i need to expand it, not to zoom! here is the code I have right now:
var app = {}
function stash (d) {
d.x0 = d.x
d.dx0 = d.dx
}
function getSvg (width, height, margin) {
return d3.select('#sunburst').append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')')
}
function prepareArcForSunburst (component) {
var arc = d3
.svg
.arc()
.startAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, component.x(d.x)))
})
.endAngle(function (d) {
return Math.max(0, Math.min(2 * Math.PI, component.x(d.x + d.dx)))
})
.innerRadius(function (d) {
return Math.max(0, component.y(d.y))
})
.outerRadius(function (d) {
return Math.max(0, component.y(d.y + d.dy))
})
return arc
}
function preparePartitionOptionsForSunburst () {
var partition = d3
.layout
.partition()
.sort(null)
.value(function (d) {
return 1
})
return partition
}
function compileDataForSunburst (component, partition, data) {
var dataCompiled = component
.svg
.datum(data)
.selectAll('path')
.data(partition.nodes)
return dataCompiled
}
function drawChart (data) {
var color = d3.scale.category20c();
this.initiallyDrawed = true
var margin = { left: 20 }
var component = app
component.width = 300
component.height = component.width
component.data = data
component.radius = Math.min(component.width, component.height) / 2
component.x = d3.scale.linear()
.range([0, 2 * Math.PI])
component.y = d3.scale.sqrt()
.range([0, component.radius])
component.svg = getSvg(component.width, component.height, margin)
component.partition = preparePartitionOptionsForSunburst()
// данные по углам и радиусам каждой ячейки
component.arc = prepareArcForSunburst(component)
var dataCompiled = compileDataForSunburst(component, component.partition, data)
var arcTweenZoom = function (d) {
var component = app
var xd = d3.interpolate(component.x.domain(), [d.x, d.x + d.dx])
var yd = d3.interpolate(component.y.domain(), [d.y, 1])
var yr = d3.interpolate(component.y.range(), [d.y ? 20 : 0, component.radius])
return function (d, i) {
return i ? function (t) {
return component.arc(d)
} : function (t) {
component.x.domain(xd(t))
component.y.domain(yd(t)).range(yr(t))
return component.arc(d)
}
}
}
var magnify = function (d) {
var component = app
component.path
.transition()
.duration(1000)
.attrTween('d', arcTweenZoom(d))
}
component.path = dataCompiled
.enter()
.append('path')
.attr('d', component.arc)
.style('stroke', '#fff')
.on('click', magnify)
.style('fill', function (d) {
return color((d.children ? d : d.parent).name);
})
.style('fill-rule', 'evenodd')
.each(stash)
d3.select(self.frameElement).style('height', component.height + 'px')
}
and here are screen shots. Here is the initial state with hovered item i would like to expand:
and here is the state I would like to reach with transition(!!)

Failed to generating two pie charts on the same page in d3.js

Failed Example
Woking Example
I'm switching for this pie chart for the animation effect. I wrap the code in a function and use $.each() to loop over the elements to generate two charts on the same page, but unlike the working example, which is my original code for generating pie charts , I can't get it to work. Can anyone figure out what the problem is?
var colors = ["#DFC267","#90C0E2","#DF5A6E","#FFA854","#749D79","#BFE5E2","#d3d3d3"];
function pie(dataset,el){
console.log(dataset);
var data = [];
var color = d3.scale.ordinal().range(colors);
r = 115,
labelr = r + 30,
pi = 2 * Math.PI,
svg = d3.select(el).append('svg').
attr('width', 350).
attr('height', 350),
group = svg.append('g').
attr('transform', 'translate(155, 170)')
,
arc = d3.svg.arc().
innerRadius(r - 50).
outerRadius(r)
,
pie = d3.layout.pie()
.value(function(d) { return d.result; }),
format = d3.format('.3r'),
arcs = group.selectAll('.arc').
data(pie(d3.values(dataset))).
enter().
append('g').
attr('class', 'arc')
;
arcs.append('path').
transition().
delay(function(d, i) { return i * 500; }).
duration(750).
attrTween('d', function(d) {
var i = d3.interpolate(d.startAngle + 0, d.endAngle);
return function(t) {
d.endAngle = i(t);
return arc(d);
};
}).
style('fill', function(d, i) { return color(i); }).
style('stroke', '#fff').
style('stroke-width', '2px')
;
arcs.append('text').
attr('transform', function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
h = Math.sqrt(x*x + y*y);
return "translate(" + (x/h * labelr) + ',' +
(y/h * labelr) + ")";
}).
attr('text-anchor', 'middle').
attr('font-size', '1em').
attr('fill', '#222').
text(function (d) {
var total = d3.sum(dataset.map(function(d) {
return d.result;
}));
var percent = Math.round(1000 * d.value / total) / 10;
return percent + ' %';
});
var tooltip = d3.select(el).append('div').attr('class', 'tooltip');
arcs.on('mousemove', function(d) { console.log(d3.event);
tooltip.style("top", d3.event.y - r+ "px").style("left", d3.event.x + "px")
});
arcs.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return d.result;
}));
tooltip.style('display', 'block')
.style("opacity", 1)
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'count')
tooltip.select('.label').html(d.data.item);
tooltip.select('.count').html(d.data.result);
});
arcs.on('mouseout', function() {
tooltip.style('display', 'none');
});
}
$('.j_chart').each(function(k,i){
var dataset = $(this).find('.j_data').text();
console.log(dataset);
pie(JSON.parse(dataset),'#j_'+k);
})
This is the working code:
var colors = ["#DFC267","#90C0E2","#DF5A6E","#FFA854","#749D79","#BFE5E2","#d3d3d3"];
function pie(dataset,el){ console.log(dataset)
'use strict';
var width = 280;
var height = 280;
var radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal().range(colors);
var svg = d3.select(el)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.svg.arc()
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) { return d.result; })
.sort(null);
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.item);
});
var tooltip = d3.select(el).append('div').attr('class', 'tooltip');
path.on('mousemove', function(d) { console.log(d3.event);
tooltip.style("top", d3.event.y - radius + "px").style("left", d3.event.x + "px")
});
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return d.result;
}));
var percent = Math.round(1000 * d.data.result / total) / 10;
tooltip
.style('display', 'block')
.style("opacity", 1)
.append('div')
.attr('class', 'label')
.append('div')
.attr('class', 'count')
.append('div')
.attr('class', 'percent');
tooltip.select('.label').html(d.data.item);
tooltip.select('.count').html(d.data.result);
tooltip.select('.percent').html(percent + '%');
});
path.on('mouseout', function() {
tooltip.style('display', 'none');
});
}
the problem is that you have a function called "pie" and inside that function, you do
pie = d3.layout.pie()
thus, overwriting the function definition and the final result is that it is called only for the first item. try renaming the function to something else, like pie_func. check this fiddle with the correction: http://jsfiddle.net/cesarpachon/a4r2q4pw/

D3 enter-exit-update and pie charts

So, I've been updating a functioning but not elegant D3 chart using https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter9/pie-chart.html
My class looks like this:
function doughnutChart(selector_div) {
"use strict";
var _chart = {};
var _width = 200, _height = 200,
_data = [],
_svg, _bodyG, _pieG,
_radius = 100,
_inner_radius = 50;
_chart.render = function() {
if (!_svg) {
_svg = d3.select(selector_div).append("svg")
.attr("height", _height)
.attr("width", _width);
}
renderBody(_svg);
};
function renderBody(svg) {
if (!_bodyG) {
_bodyG = svg.append("g")
.attr("class", "body");
}
renderDoughnut();
}
function renderDoughnut() {
var pie = d3.layout.pie()
.sort(function (d) {
return d.id;
})
.value(function (d) {
return d.count + d.abnormal;
});
var arc = d3.svg.arc()
.outerRadius(_radius)
.innerRadius(_inner_radius);
if (!_pieG) {
_pieG = _bodyG.append("g")
.attr("class", "pie")
.attr("transform", "translate("
+ _radius
+ ","
+ _radius + ")");
}
renderSlices(pie, arc);
renderLabels(pie, arc);
}
}
function renderSlices(pie, arc) {
var slices = _pieG.selectAll("path.arc")
.data(pie(_data));
slices.enter()
.append("path")
.attr("class", "arc")
.attr("fill", function(d) {
return d.data.visualisation_colour;
});
slices.transition()
.attrTween("d", function(d) {
var currentArc = this.__current__;
if (!currentArc) {
currentArc = {startAngle: 0, endAngle: 0};
}
var interpolate = d3.interpolate(currentArc, d);
this.__current__ = interpolate(1);
return function(t) {
return arc(interpolate(t));
};
});
}
function renderLabels() {
_pieG.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.attr("class", "inside")
.text(function(d) {
var total = 0;
for (var j = 0; j < _data.length; j++) {
total = total + _data[j].count + _data[j].abnormal;
}
return total;
});
}
_chart.data = function(d) {
if (!arguments.length) {
return _data;
}
_data = d;
return _chart;
};
return _chart;
}
When I use:
var chart = doughnutChart("#chart").data(data);
chart.render()
I get a nice chart rendered. But the update doesn't work:
data = $.map(cell_types, function(key, value) {
return key;
});
chart.render();
The main issue is:
How do I update this chart? I'm not sure how to get updated data into the chart. Calling render() again does not update the data despite the data variable being updated, and I can't seem to pass new data in. The book's example doesn't appear to have this issue, as testing that works without issue.
There is actually a simple error in syntax here:
if (!_pieG) {
_pieG = _bodyG.append("g")
.attr("class", "pie")
.attr("transform", "translate("
+ _radius
+ ","
+ _radius + ")");
renderSlices(pie, arc);
renderLabels(pie, arc);
}
Should actually be:
if (!_pieG) {
_pieG = _bodyG.append("g")
.attr("class", "pie")
.attr("transform", "translate("
+ _radius
+ ","
+ _radius + ")");
}
renderSlices(pie, arc);
renderLabels(pie, arc);
And then it updates correctly.

D3 js multi level pie chart animation

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);
}
});

d3.js dynamic data with labels

I've managed to get the following working (pie chart changing dynamically based on a slider value):
var x = function() {
return $("#slider").val();
}
var data = function() {
return [x(), ((100-x())/2), ((100-x())/2)];
}
var w = 100,
h = 100,
r = 50,
color = d3.scale.category20(),
pie = d3.layout.pie().sort(null),
arc = d3.svg.arc().outerRadius(r);
var svg = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + w / 2 + "," + h / 2 + ")");
var arcs = svg.selectAll("path")
.data(pie(data()))
.enter().append("svg:path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; });
var redraw = function() {
newdata = data(); // swap the data
arcs = arcs.data(pie(newdata)); // recompute the angles and rebind the data
arcs.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
};
// Store the currently-displayed angles in this._current.
// Then, interpolate from this._current to the new angles.
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
I can't seem to modify it to include dynamic labels (use an object for data rather than an array {'label': 'label1', 'value': value}. How would I modify the above code to add labels?
This should work, keeping the labels and the pie chart as separate entities:
var x = function() {
return $("#slider").val();
}
var data = function() {
return [x(), ((100-x())/2), ((100-x())/2)];
}
var labels = function() {
var label1 = "LABEL 1: " + x();
var label2 = "LABEL 2: " + ((100-x())/2);
var label3 = "LABEL 3: " + ((100-x())/2);
return [label1, label2, label3];
}
var w = 200,
h = 100,
r = 50,
color = d3.scale.category20(),
pie = d3.layout.pie().sort(null),
arc = d3.svg.arc().outerRadius(r);
var svg = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + 50 + "," + h / 2 + ")");
var arcs = svg.selectAll("path")
.data(pie(data()))
.enter().append("svg:path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; });
var label_group = svg.selectAll("text")
.data(labels())
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("x", 60)
.attr("y", function(d, i) { return (i * 20) - 16; });
var redraw = function() {
newdata = data(); // swap the data
arcs = arcs.data(pie(newdata)); // recompute the angles and rebind the data
arcs.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
label_group = label_group.data(labels());
label_group.transition().delay(300).text(function(d) {
return d;
});

Categories

Resources