I have created a d3 donut chart. Here is my code:
var width = 480;
var height = 480;
var radius = Math.min(width, height) / 2;
var doughnutWidth = 30;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d[1]; });
var dataset = settings.dataset;
console.log(dataset);
var svg = d3.select("body")
.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))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data[0]);
})
I have a simple form on my web page which displays a dropdown menu with several options. Every time a user changes a value on the form a new dataset is sent to my script (settings.dataset) and the donut is redrawn on the page.
Problem is, some of the values from the previous dataset remain in the DOM. In the console output below, you can see that the second dataset only has two elements. The third one is from the previous dataset. This is messing up the chart, as it is displaying a value that shouldn't be there.
My question: how do I clear the old values? I've read up on .exit() and .remove(), but I can't get my head around these methods.
Create one function that (re)draws the pie when it's created and when it's updated.
New data should be added to pie using enter() and old data should be removed using exit().remove()
It is as simple as this:
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTween);
path.exit().remove()
Full working code -> JSFIDDLE
There are two steps to implement the 'redraw' effect you want:
First, I suppose you want the svg canvas to be drawn only once when the page is loaded for the first time, and then update the chart in the svg instead of remove and redraw the svg:
var svg = d3.select("body")
.selectAll("svg")
.data([settings.dataset]);
// put data in an array with only one element
// this ensures there is only one consistent svg on the page when data is updated(when the script gets executed)
svg.enter().append("svg")
Second, understanding enter(), exit(), there are many great tutorials about this. In your case, I would suggest to draw the donut something like this:
var path = svg.selectAll(".donut")
.data(settings.data)
// bind data to elements, now you have elements belong to one of those
// three 'states', enter, exit, or update
// for `enter` selection, append elements
path.enter().append("path").attr("d", arc).attr("fill", "teal")
// for `update` selection, which means they are already binded with data
path.transition().attrTween("d", someFunction) // apply transition here if you want some animation for data change
// for `exit` selection, they shouldn't be on the svg canvas since there is no corresponding data, you can then remove them
path.exit().remove()
//remove and create svg
d3.select("svg").remove();
var svg = d3.select("body").append("svg").attr("width","960").attr("height", "600"),
inner = svg.append("g");
Related
I have this d3 code for drawing the pie chart in d3.js
/** START OF PIE CHART */
var svgCirWidth = 600, svgCirHeight = 300, radius = Math.min(svgCirWidth, svgCirHeight) / 2;
const pieContainer = d3.select("#pieChart")
.append("svg")
.attr("width", svgCirWidth)
.attr("height", svgCirHeight);
//create group element to hold pie chart
var g = pieContainer.append("g")
.attr("transform", "translate(" + 250 + "," + radius + ")");
var color = d3.scaleOrdinal(d3.schemeCategory10);
var pie = d3.pie().value(function (d) {
return d.total_up_percentage;
});
var path = d3.arc()
.outerRadius(radius)
.innerRadius(0);
var arc = g.selectAll("arc")
.data(pie(data))
.enter() //means keeps looping in the data
.append("g");
arc.append("path")
.attr("d", path)
.attr("fill", function (d) {
return color(d.data.total_up_percentage);
})
.append("text")
.text("afdaf");
var label = d3.arc()
.outerRadius(radius)
.innerRadius(0);
arc.append("text")
.attr("transform", (d) => {
return "translate(" + label.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text((d) => {
return d.data.region_iso_code + ":" + d.data.total_up_percentage + "%"
});
and this is the result of my pie
as you can see the text overlaps each other. I was wondering how can i rotate the text so it can be much more easier to read. I've tried editing the transform in the console but it won't work it just makes the text go up or down. Also I was wondering what happened to the color of my pie. It stuck on orange. It says on the documentation i read about this schemeCategory10 is that it is a 10 color code scheme. Yet it won't show the rest of the color. Is there any other way to change color?
When using an ordinal scale you should never rely on the scale's ability to infer the domain from usage: a good practice is always to explicitly set the domain.
By setting the domain you'd quickly see that this is indeed the expected behaviour: all orange slices have the same value, which is 100.
If you want different colors for those same values, use the indices instead:
.attr("fill", function (_, i) {
return color(i);
})
PS: regarding the texts, please avoid asking 2 or more different issues in a single question. Edit your question leaving just 1 issue, you can always post a new question with the other issues.
Can someone help me to create below image using d3js. I able to create pie chart as required but stuck to render outer text with arrows and all.
Wheel with outer text
As of know I have achieved circle creation using below code.
var svg = d3.select("svg");
var margin = {top: 40, right: 45, bottom: 30, left: 40};
console.log(svg);
var width = svg.attr('width');
var height = svg.attr('height');
var radius = Math.min(width, height)/2;
var g = svg.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var hoverStyle = {
zindex: '2px'
};
var hoverExitStyle = {
zindex: "0px"
}
var animateSpeed = 500;
// Define a Pie
var pie = d3.pie()
.sort(null)
.value(function(d) {return d.number});
// define pie section
var path = d3.arc()
.outerRadius(radius - 10)
.innerRadius(0);
//
var label = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40);
// Get pie sections based on the data.
var pieSections = pie(data);
var arc = g.selectAll('.arc')
.data(pieSections)
.enter().append("g")
.attr("class", "arc")
.append('a')
.attr("href", function(d) { return d.data.url; });
arc.append("path")
.attr("d", path).transition()
.attr("fill", function(d) { return d.data.color; });
arc.append("text")
.attr("transform", function(d) { return "translate(" + label.centroid(d) + ")"; })
.attr("dy", "0.35em")
.text(function(d) { return d.data.title; });
The text and the arrows are two separate concerns that probably merit their own questions.
Curved text
To do text on a path with d3, you might want to look at the textpath documentation. It's going to be a little tricky; basically, you'll want to create a second d3.arc() generator with a slightly longer outer radius. Use the longer one to set the d attribute of path elements (that you need to create) inside the SVG's defs object, and reference those path elements' ids from textpath elements (that you also need to create).
Curved arrows
To accomplish this exactly like the image, you're probably going to need to some manual construction (including figuring out the math!) of the d attribute yourself to add appropriate arrowheads (see the SVG path syntax). If you're doing a static image, it might be faster to just create the lines (again, using a longer-radius d3.arc() generator), and export the SVG with something like SVG crowbar to a drawing program like Illustrator or Inkscape, and add the arrowheads there.
I do have a d3.scaleTime() as x-axis and JSON data with dates, which should be represented as an rect on this x-axis, in order to mark the date as "reserved".
The simple way is
this.svg.selectAll('rect').data(dates).enter().each(function(d) {
d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', x(d.date));
}
But I would like to group rects, if they have consecutive dates, in order to be able to move the group around on the x-axis.
What I did / tried is in the enter / each function to check, if there is already a group with a date "next" to the current date.
If not, create a new group and append the current date-rect.
If there is a group, get the group and append the date-rect to the existing group.
But now, the problem is, that the rects don't keep their data and / or I can't add the rect to the group properly.
Some (pseudo)code:
var rects = this.svg.selectAll('rect') .data(dates, function(dataElement) {
return dataElement.id;
});
// add new date rects
rects.enter().each(function(dataElement) {
var isNewDateGroup = true / false; // detect if date is "next" to another one
if (isNewDateGroup)
{
var group = svg.append("g");
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick);
group.append(function() {
// works correctly, appends the rect and sets the data
return dateRect.select('rect').node();
});
// translate group to desired x(d.date)
}
else
{
var group = getGroupForDate();
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', offsetToLastRectInGroup);
group.append(function() {
// does not work correctly, does not append the rect and does not set the data
return dateRect.select('rect').node();
});
}
});
Basically, the enter function does not work, if I append more then one SVG element, since the data gets confused and the appending as well.
Any hints, how I can group my rects?
The answer on this question helped me to solve my problem:
Since .select() propagate the data to the selected elements, I just have to get rid of the selection:
var dateRect = d3.select(this)
.append("rect")
.attr('height', desiredHeight)
.attr('width', desiredWidthFromTickToTick)
.attr('x', offsetToLastRectInGroup);
group.append(function() {
return dateRect.node();
});
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
I'm trying to build an interactive population pyramid. I want to update the width of the bars.
Something is not working with my selections though. Female and male bars charts are updated to the same width, because they receive the same datum. I suspect that it has to do with the way I select or join the data. I've tried several combinations, now I'm stuck reading the doc on select and selectAll.
Any comments on what goes wrong here are highly appreciated.
The code (here is the fiddle):
var pyramidCanvas = d3.select("body")
.append("svg")
.attr("width",200)
.attr("height",100);
var pyramidHeight = 100;
var pyramidWidth = 100;
var pyramidBarsRight, pyramidBarsLeft;
function setupPyramid(){
var data = [300,20,40,50,10];
var pyramidHeight = 100;
var pyramidWidth = 100;
var xScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d;})])
.range([0,pyramidWidth]);
var barHeight = pyramidHeight/data.length;
console.log(barHeight);
pyramidBarsLeft = pyramidCanvas.selectAll("g-female")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i){return "translate(0,"+ (i * 10)+ ")" ;});
pyramidBarsLeft.append("rect")
.attr("class","female-bar")
.attr("x",function(d){return pyramidWidth-xScale(d);})
.attr("y",function(d,i){
return i*10;
})
.attr("width",function(d){console.log(d);return xScale(d);})
.attr("height",10)
.style("fill","pink");
pyramidBarsRight = pyramidCanvas.selectAll("g-male")
.data(data)
.enter()
.append("g")
.attr("transform",function(d,i){return "translate(0,"+ (i * 10)+ ")" ;});
pyramidBarsRight.append("rect")
.attr("class","male-bar")
.attr("x",pyramidWidth)
.attr("y",function(d,i){
return i*10;
})
.attr("width",function(d){console.log(d);return xScale(d);})
.attr("height",10)
.style("fill","steelblue");
}
function updatePyramid(data){
var femData = data.slice(0,5);
var malData = data.slice(5,9);
console.log(femData);
console.log(malData);
console.log(pyramidBarsRight.selectAll("rect"));
var xScale = d3.scale.linear()
.domain([0,d3.max(data,function(d){return d;})])
.range([0,pyramidWidth]);
pyramidBarsLeft
.selectAll(".female-bar")
.data(femData)
.transition()
.duration(500)
.attr("x",function(d){return pyramidWidth-xScale(d);})
.attr("width",function(d){console.log(d);return xScale(d);})
pyramidBarsRight
.selectAll(".male-bar")
.data(malData)
.transition()
.duration(500)
.attr("width",function(d){console.log(d);return xScale(d);});
}
setupPyramid();
updatePyramid([20,10,50,100,600,30,22,54,91,19,10]);
You shouldn't save the result of an enter selection and use it like that. It works fine it you select from the canvas, see here. The reason is that elements in the enter selection merge into the update selection once DOM elements have been appended.