I'm trying to create a reusable pie chart with dynamic transitions as a learning task. I'm working off of the d3.js resuable components e-book by Chris Viau.
The problem I'm having is basically its not updating, but creating multiple pie charts. I'm wondering if I'm not understanding how d3.dispatch works or whether I've messed something up in the way the pie char should work. It creates multiple circles instead of dynamically updating a single pie chart with random values.
here is my jsfiddle.
http://jsfiddle.net/seoulbrother/Upcr5/
thanks!
js code below:
d3.edge = {};
d3.edge.donut = function module() {
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var dispatch = d3.dispatch("customHover");
function graph(_selection) {
_selection.each(function(_data) {
var pie = d3.layout.pie()
.value(function(_data) { return _data; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
if (!svg){
var svg = d3.select(this).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
var path = svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.ease("elastic")
.duration(750)
.attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
}
d3.rebind(graph, dispatch, "on");
return graph;
}
donut = d3.edge.donut();
var data = [1, 2, 3, 4];
var container = d3.select("#viz").datum(data).call(donut);
function update(_data) {
data = d3.range(~~(Math.random() * 20)).map(function(d, i) {
return ~~(Math.random() * 100);
});
container.datum(data).transition().ease("linear").call(donut);
}
update();
setTimeout( update, 1000);
The main reason for multiple SVGs appearing is that you're not checking if there is one already correctly. You're relying on the variable svg being defined, but define it only after checking whether it is defined.
The better way is to select the element you're looking for and check whether that selection is empty:
var svg = d3.select(this).select("svg > g");
if (svg.empty()){ // etc
In addition, you need to handle the update and exit selections in your code in addition to the enter selection. Complete jsfiddle here.
Related
I got the following D3 v4 pie chart, every time I try updating it the data doesn't update correctly. I have been reading around tried following some other example, but just can't seem to get it to work. Current update function looks like this:
function PieGenUpdater(data, colourRangeIn) {
var dataset = data;
var width = 400;
var height = 400;
var radius = Math.min(width, height) / 2;
var arc = d3.arc()
.innerRadius(radius/1.5)
.outerRadius(radius);
var pie = d3.pie()
.value(function(d) { return d.percent; })
.sort(null);
var svg = d3.select('#c-pie');
var path = svg.selectAll('path').data(pie(dataset));
path.enter()
.append("path")
.attr('fill', function(d, i) {
return d.data.color;
})
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTweenCoverage);
path.exit().remove();
// Store the displayed angles in _current.
// Then, interpolate from _current to the new angles.
// During the transition, _current is updated in-place by d3.interpolate.
function arcTweenCoverage(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
}
https://jsfiddle.net/mahsan/zup6kafk/
Any help is greatly appreciated.
Here is the 4 years too late answer...
In PieGenUpdater change
var path = svg.selectAll('path').data(pie(dataset));
to
var path = svg.select('g').selectAll('path').data(pie(dataset));
In your update function you were adding the additional paths from dataset 1 directly under element instead of the element
I am creating pie chart using d3.js. I would like to create 3 pies with single svg element with animation.
This is working fine for me. But do creating different I am reducing the radius each time using a loop. But the radius not getting changed.
How to solve this?
my code (sample) :
var array1 = [
0,200
]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function tweenPie(finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', tweenPie)
}
}
Live Demo
There is a single arc variable that is being used in the tweenPie method and in the for loop. Each time through the for loop, the arc variable is set to a new value. The tweenPie method is called for each pie chart after the for loop exits. As a result, all the pie charts are using the same tweenPie method which is using the arc created in the last for loop.
For each pie chart, you need to create a separate tweenPie method with its own arc. For example...
var array1 = [ 0, 200 ]
window.onload = function () {
var width = 660,
height = 200,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var arc = null;
var pie = d3.layout.pie()
.value(function(d) {
return d; })
.sort(null);
function getTweenPie(arc) {
return function (finish) {
var start = {
startAngle: 0,
endAngle: 0
};
var i = d3.interpolate(start, finish);
return function(d) { return arc(i(d)); };
}
}
var svg1 = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
for( var i = 0; i < 3; i++) {
arc = d3.svg.arc()
.innerRadius(radius - (5*i)) //each time size differs
.outerRadius(radius - (6)*i); //each time size differs
svg1.append('g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
.datum(array1).selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.transition()
.duration(5000)
.attrTween('d', getTweenPie(arc))
}
}
I'm trying to make radial bar chart using d3js, but I'm having some trouble with the data model. I have a fiddle here showing what I want to achieve. At the moment the size of the bars are randomly created, but I want to be able to provide my own data (values between 1 and 6) into the chart, but I'm having trouble understanding the data model/structure of d3js, so help would be appreciated!
$(function(){
var $container = $('.chart-container'),
τ = 2 * Math.PI,
width = $container.width(),
height = $container.height(),
outerRadius = Math.min(width,height)/2.5,
innerRadius = 10,
fontSize = (Math.min(width,height)/4);
var dataset = {
weeks: [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
};
var color = d3.scale.ordinal() .range(['rgb(247,251,255)','rgb(222,235,247)','rgb(198,219,239)','rgb(158,202,225)','rgb(107,174,214)','rgb(66,146,198)','rgb(33,113,181)','rgb(8,81,156)','rgb(8,48,107)']);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select('.chart-container').append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox','0 0 '+Math.min(width,height) +' '+Math.min(width,height) )
.attr('preserveAspectRatio','xMinYMin')
.append("g")
.attr("transform", "translate(" + Math.min(width,height) / 2 + "," + Math.min(width,height) / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g").attr("class", "arc");
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) { return arc.innerRadius(10).outerRadius(20*getRandomInt (1, 6))(d); });
});
function getRandomInt (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Change your dataset variable and colours here, just pre-populate your required arrays and inject them into D3. i.e. d3.values(dataset)
var dataset = {
weeks: [5,10]
};
var color = d3.scale.ordinal().range(['#ccc','#c33']);
I have problem adding text in my histogram. I can do this in more simple example.
I try to do this:
// try to add bar value
var barnum = g.selectAll('text')
.data(layout)
.enter()
.append("text")
.attr('y',-10)
.attr('x',10)
.attr("text-anchor", "middle")
.style("fill","black")
.text('testtest')
.style("pointer-events", "none")
;
barnum.transition();
I can't see any text in my figure. The code include definition is here:
var dateFormat = d3.time.format("%Y-%m-%d");
var g;
var data;
var margin = {top: 30, right: 30, bottom: 80, left: 80},
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var cx = 10;
var numberBins = 5;
var dispatch = d3.dispatch(chart, "hover");
function chart(container) {
g = container;
update();
}
chart.update = update;
function update() {
// create hist layout
var hist = d3.layout.histogram()
.value(function(d) { return d.selectvar })
.range([d3.min(data, function(d){ return d.selectvar }) , d3.max(data, function(d){ return d.selectvar }) ])
.bins(numberBins);
var layout = hist(data);
var maxLength = d3.max(layout, function(d) { return d.length });
var widthScale = d3.scale.linear()
.domain([0, maxLength])
.range([0, width])
var yScale = d3.scale.ordinal()
.domain(d3.range(numberBins))
.rangeBands([height, 0], 0)
var colorScale = d3.scale.category20();
// create svg
var rects = g.selectAll("rect")
.data(layout)
rects.enter().append("rect")
rects .transition()
.duration(500)
.attr({
y: function(d,i) {
return yScale(i)
},
x: 50,
height: yScale.rangeBand(),
width: function(d,i) {
return widthScale(d.length)
},
fill: function(d, i) { return colorScale(i) }
});
rects.exit().transition().remove();
// try to add bar value
var barnum = g.selectAll('text')
.data(layout)
.enter()
.append("text")
.attr('y',-10)
.attr('x',10)
.attr("text-anchor", "middle")
.style("fill","black")
.text('testtest')
.style("pointer-events", "none")
;
barnum.transition();
is there something wrong with my way to create svg element? I found out some successful case use append('g') from the beginning. New to d3.js! thank you.
You're using d3.dispatch, which is documented on a page titled Internals. That doesn't mean you shouldn't use it, but rather, it shouldn't be your first choice.
You're correct that there's "something wrong with my way to create svg element" -- you're not creating one! Try:
var svg = d3.select("body").append("svg");
var g = svg.append("g");
At this point, you need to have a good understanding of the DOM, the SVG standard, CSS selectors, and D3's selection API to make things work. You don't tell D3 to put labels on your bars and that's it. You have to instruct it what elements to create, and where, keeping track of translates and offsets and stuff like that. You're best off copying and studying one of Mike Bostock's many examples.
D3 is not learned quickly. You need to invest time learning it before you can make any chart you like.
I'm drawing a d3 donut. Now I want to add as many donuts as entries in database. If I add something to database, automatic updating fails. I have to reload My code in the Browser - then I see the new donut. Isnt Meteor.autorun updating automatically?
Code is:
Template.donuts.rendered = function (){
var self = this;
self.node = self.find("p");
// Data
var dataset = {
apples: [2, 2, 2, 2, 2]
};
//Width and height
var width = 100,
height = 100,
radius = Math.min(width, height) / 2;
// render
self.handle = Meteor.autorun(function () {
var color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 5);
var svg = d3.select(self.node).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
});
}; //Template.donuts
it is called via handlebars
<template name="donuts">
{{#each nodes}}
<p></p>
{{/each}}
</template>
What am I doing wrong. Thank you for your time.
Your rendered hook is on the wrong level. Right now you're connecting it to the template that contains the donuts, when it looks like you want to have each donut be rendered in a certain way. First, start by reorganising your templates:
<template name="donuts">
{{#each nodes}}
{{> node}}
{{/each}}
</template>
<template name="node"><p></p></template>
Now you can tell a node what to do when it's rendered:
Template.node.rendered = function() {
// d3 code
}
The rendered call will be automatically run whenever the node is re-rendered, which will happen if you change a dependency. If nodes is a reactive source like a mongodb cursor, this will work immediately. Otherwise, please add more code so we can figure out what else is going on.
Meteor.autorun() will run whenever its dependencies change. You need a reactive datasource inside the function.
Found a more elegant solution:
// Donuts //
function donutinit() {
var dataset = {
apples: [2, 2, 2, 2, 2]
};
//Width and height
var width = 100,
height = 100,
radius = Math.min(width, height) / 2;
// render
var color = d3.scale.category10();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 20)
.outerRadius(radius - 5);
var svg = d3.select("#donut_canvas").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.apples))
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
};
Template.donut.rendered = function() {
donutinit();
};
After that iterate with handlebars over #donut_canvas. The Meteor.autorun or Meteor.rendered gave me unpredictable amounts of donuts - it rendered additional donuts. I had to reload it then.
Answer is inspired from here: Google map contained in meteor Template is rendered twice
Thank you for your time.