I am trying to generate pie chart in d3 from external data, I could not find error in the below code as it is not throwing any error.
Here is what my code
//file path is correct
d3.json("errortable12.json", function(data){
//var obj = JSON.parse(data);
var r= 300;
var color =d3.scale.ordinal()
.range(["red","blue","orange"])
console.log(data);
var canvas = d3.select("body").append("svg")
.attr("width",1500)
.attr("height",1500);
var group = canvas.append("g")
.attr("transform","translate(300,300)");
var arc = d3.svg.arc()
.innerRadius(200)
.outerRadius(r);
var pie = d3.layout.pie()
.value(function(d) { return d.age});
var arcs = group.selectAll(".arc")
.data(pie(data))
.enter()
.append("g")
.attr("class","arc");
arcs.append("path")
.attr("d.age",arc);
arcs.append("path")
.attr("d.age",arc)
.attr("fill",function(d){return color(d.data);});
Two problems. First, "d.age" is not an SVG attribute, just use "d". Also, no need to append two paths, just call that once. Second, passing d.data to color is not valid, just use i.
arcs.append("path")
.attr("d.age",arc); //<-- d.age is not a valid SVG attribute
arcs.append("path")
.attr("d.age",arc) //<-- why repeat this twice?
.attr("fill",function(d){return color(d.data);});
Should just be:
arcs.append("path")
.attr("d", arc)
.attr("fill", function(d,i) {
// return color(d.data);
return color(i); //<-- just use i
});
Full code here.
Related
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've placed several pie charts on a map and want to adjust their size based on a corresponding value from a csv-file ("Total", in this example). But no matter how I adjust the radius, the pies won't show. Is there something important I missed?
My code so far:
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function drawPies (data) {
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return +d});
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(function(d) {
return d.Total; });
var pies = svg.selectAll('.pie')
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
return "translate(" + projection([d.lon, d.lat])[0] + "," + projection([d.lon, d.lat])[1] + ")";
});
var color = d3.scale.ordinal()
.range(["#98abc5", "#7b6888", "#a05d56", "#d0743c",])
.domain(d3.range(0,4));
pies.selectAll('.slice')
.data(function(d){
return pie([d.Group1, d.Group2, d.Group3, d.Group4]); })
.enter()
.append('path')
.attr('d', arc)
.style('fill', function(d,i){
return color(i);
});
Here is the link to the complete code.
I could not run in a correct way your code so I moved a few things to get it working under a plnkr.
// You had all the async calls to remote data files nested which I
// recommend not doing. I separated your GeoJSON rendering and your
// pie rendering into two distinct functions.
// Start GeoJSON rendering
d3.csv("Jugendarbeitslosigkeit.csv", function(data) {
//Load in GeoJSON data
d3.json("PolenReg2.json", function(json) {
data.forEach(function(d, i) {
// ...more code
// This is a tricky part
// Since we separated the polygon and pie rendering
// and the polygon calls will take longer due to size
// the group containing the polygons will be rendered
// last, thus rendering the group after your pie group.
// This will make your pies stay behind the polygon paths
// that's why we use the insert. In order to position
// the polygons layer below the pies.
svg
.insert('g', ':first-child')
// ... more code
// End GeoJSON rendering
// Start Pie rendering
d3.csv("Bevoelkerung-Altersstruktur-2010-Summe.csv", function(err, data) {
// Set our large pie function
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return +d.key;
});
// ... more code
// End Pie rendering
The important part is here:
var pies = svg
.append('g') // Add a group with the class 'big-pies'
.attr('class', 'big-pies')
.selectAll('.pie') // Join data to create groups by each polygon (as far as I understand)
.data(data)
.enter()
.append('g')
.attr('class', 'pie')
.attr("transform", function(d) {
var proj = projection([d.lon, d.lat]);
return "translate(" + proj[0] + "," + proj[1] + ")";
})
.selectAll('.slice') // Start pie - data join
.data(function(d) {
// set slice data with additional total value
// so that we can use this value at the attr d
// function
return pie([{
key: d.Kinder,
tot: d.total
}, {
key: d.Jugendliche,
tot: d.total
}, {
key: d.Erwachsene,
tot: d.total
}, {
key: d.Rentner,
tot: d.total
}]);
})
.enter()
.append('path')
.attr('d', function(d, i) {
// return the arc function with outer radius increased by the total value
return d3.svg.arc().innerRadius(0).outerRadius(d.data.tot * 2).call(d, d)
})
.style('fill', function(d, i) {
return c10(i);
});
Plnkr: https://plnkr.co/edit/CwiFnNmfIleo5zZ6BseW?p=preview
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");
There is probably a simple answer to this question... . I'm using d3 to create a globe, showing all countries. I also have a div with the name of all the countries in it. When I click on a country name, I want the globe to spin to that country. But I'm having trouble getting the syntax right. Can anyone help, please?
var feature;
var projection = d3.geo.azimuthal()
.scale(zoom)
.origin([-71.03,42.37])
.mode("orthographic")
.translate([380, 450]);
var circle = d3.geo.greatCircle()
.origin(projection.origin());
var scale = {
orthographic: 380,
stereographic: 380,
gnomonic: 380,
equidistant: 380 / Math.PI * 2,
equalarea: 380 / Math.SQRT2
};
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#globe").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("dblclick", dblclick)
.on("mousedown", mousedown);
var g = svg.append("g");
d3.json("simplified.geojson", function(collection) {
g.append("g")
.attr("id", "countries")
g.append("g")
.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.ISO3; })
.attr("fill", function(d) { return d.properties.FILL; }) //change color and make clickable if data on this country exists
.on("mouseover", pathOver)
.on("mouseout", pathOut)
.on( "dblclick", dblclick)
.on("mousewheel.zoom", null)
.on("click", click);
feature = svg.selectAll("path");
feature.append("svg:title")
.text(function(d) { return d.properties.NAME; });
//here is where I want to be able to click a country name in the div and have the globe rotate to that country:
$('.represented').click(function(){
var countryabbrev = $(this).attr('id');
projection.origin(projection.invert(#path.centroid(#CAN))); //this line is wrong
refresh(1500);
showPerson(countryabbrev)
});
I've gotten it to find the country and rotate. Now the rotate is sketchy, but at least there's progress:
$('.represented').click(function(){
var countryabbrev = $(this).attr('id');
getCentroid(d3.select("#" + countryabbrev));
//projection.origin(projection.invert(#path.centroid(#CAN)));
projection.origin(getCentroid(d3.select("#" + countryabbrev)));
refresh(1500);
//showPerson(countryabbrev)
});
function getCentroid(selection) {
// get the DOM element from a D3 selection
// you could also use "this" inside .each()
var element = selection.node(),
// use the native SVG interface to get the bounding box
bbox = element.getBBox();
// return the center of the bounding box
return [bbox.x + bbox.width/2, bbox.y + bbox.height/2];
}
In the following code, I just have 5 bars corresponding to their values in dataset. With .each and setInterval, I am trying to have the bar fill color transition from black to red repeatedly if the corresponding value in the dataset is less than 3.
<script type="text/javascript">
var w=500;
var h = 100;
dataset = [1,2,3,4,5];
var svg = d3.select("body")
.append("svg")
.attr("width",w)
.attr("height",h);
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("y", 0)
.attr("x", function(d,i){ return i*21;})
.attr("width",20)
.attr("height",function(d){return d*10;})
.each(function(d){
if (d<3) {
setInterval(function(){d3.select(this)
.attr("fill","black")
.transition()
.duration(1000)
.attr("fill","red")},1000);
}
});
</script>
When I run this code I get the error:
Uncaught TypeError: Object [object global] has no method 'setAttribute'
any ideas on how to make this work? I am open to any solution, I just want to have it so that a bar will pulse if it is under a certain value. Thank you.
The this variable won't have the same value in the new context. The following code should work.
.each(function(d){
var that = this;
if (d<3) {
setInterval(function(){d3.select(that)
.attr("fill","black")
.transition()
.duration(1000)
.attr("fill","red")},1000);
}
});