d3js: Pairing CSV rows into "g" elements - d3.js

I'm working through re-creating this population pyramid by Mike Bostock. I have a basic question about how .data() works.
I have data in a CSV file like so:
So, I have the number of men and women in each region (dodoma, arusha, etc.). Following Mike's example, I want to create a bar chart where men and women occupy different rects, which overlap with 80% opacity. I want region to be my x-axis, and number of people to be my y-axis. Here are my x- and y-axis scales:
var x = d3.scale.ordinal()
.rangeRoundBands([0, w], 0.05);
var y = d3.scale.linear()
.range([0, h-padding]);
And here's my attempt at grouping men+women over each region:
var regions = svg.append("g")
.classed("regions", true);
var region = regions.selectAll(".region")
.data(dataset)
.enter()
.append("g")
.attr("class", "region")
.attr("transform", function(region) { return "translate(" + x(region) + ",0)"; });
region.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d) { return h - y(d.people_0); })
.attr("width", barWidth)
.attr("height", function(d) { return y(d.people_0); });
I end up with this:
That is, I end up with 50 g elements which have all the data, and are just creating the full bar chart for each gender-region pair (men from Dodoma, women from Dodoma; men from Tanga, women from Tanga, etc.). The full chart is thus being redrawn 50 times. I have 25 regions, so I want 25 g elements with only the men/women numbers for that region.
I think this could all be tied up in .data(). But I don't know how to tell d3 that I want to group my observations by region, and end up with 2 rects per g.
Here's my JSFiddle (though I don't know how to load my external data - so nothing shows up; any hints on that would be appreciated).
Edited to add:
I've tried using .nest, given Benjamin's comment. So I've got:
dataset = d3.nest()
.key(function(d) { return d.round; })
.key(function(d) { return d.region; })
.rollup(function(v) { return v.map(function(d) { return d.people_0; }); })
.map(dataset);
And am now calling my rects like so:
region.selectAll("rect")
.data(function(region) { return data[round][region]; })
.enter()
.append("rect")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d) { return h - y(d.people_0); })
.attr("width", barWidth)
.attr("height", function(d) { return y(d.people_0); });
No luck. FWIW, console.log(dataset) shows me that the data has, indeed, been rolled up. But I'm still having trouble calling it and associating it with my g.region elements.
Could someone explain what data[round][region] does? I don't understand how d3 is interpreting this.

There are several ways to do this. My approach would be to create a two-level nest for the data, by region and then by sex:
dataset = d3.nest()
.key(function(d) {
return d.region;
})
.sortKeys(d3.ascending)
.key(function(d) {
return d.sex;
})
.entries(dataset);
This will give you a structure that's easy to use later, which will look something like this:
key: "dodoma"
values:
key: "female"
values:
people_0: "43"
region: "dodoma"
sex: "female"
key: "male
values:
people_0: "77"
region: "dodoma"
sex: "male"
key: "killimanj"
values: ...
that's quite verbose, and could be simplified a little (eg using .map). This is a great nest tutorial page for more info.
From then, only a few changes are needed to draw your rects. See this plunk: http://embed.plnkr.co/PPLbQER4FERA6D7T4m9P/preview.
Plunker (http://plnkr.co/) is a good alternative to jsfiddle if you want to link external data by the way.
A few of the changes diverge from Mike's original example, including using class="male" \ "female" in the CSS to get blue \ pink colours, rather than using rect:first-child

http://bost.ocks.org/mike/selection/ explains how data works in d3. if you call it once, it will 'bind' each entry of data to the element you specify.
In your case you have to nest() by region and you get a array of objects that looks like:
['regionA' : [{region, sex, people}, {region, sex, people}], 'regionB' : [{region, sex, people}, {region, sex, people}]
you then do a double iteration of the data. so you first call .data() (its a array of regions this time, ie regionA) and bind it on a group called region
then bind data() again (this time its the array of objects for one region ie [{region, sex, people}, {region, sex, people}]) on rectangles.

Related

piechart over a map point using d3.js

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

create bar chart with D3

Hi everyone I am starting with D3.JS, and I want to show a bar with the temperature of a place.
This is my code
json_url= "https://data.cityofchicago.org/resource/7edu-s3u7.json";
var all_name_station = ["63rd Street Weather Station", "Foster Weather Station", "Oak Street Weather Station"];
var one_name_station = ["63rd Street Weather Station"];
var width = 900;
var height = 400;
$(document).ready(function(){
d3.json(json_url, function(data) {
var canvas = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
one_name_station.forEach(function(station_item){
data.forEach(function(item_data){
if (item_data["station_name"] == station_item){
//console.log(item_data);
canvas.selectAll("rect")
.data(item_data)
.enter()
.append("rect")
.attr("width", function(d) {return d.air_temperature;})
.attr("height", 50)
.attr("y", function(d,i){return d;})
.attr("fill", "blue");
}
});
});
});
});
I take my data from JSON, there are many station but I start only represente the data for "63rd Street Weather Station"
But my problems is that this code don't return nothing. You can try here
What I missing!
Thanks in advance
Your immediate problem is that right now you are not passing an iterable to data, just an object. If you wrap item_data in an array it will create a single blue rectangle:
.data([item_data])
Then you will have other problems, however, such as setting y. I would recommend reformatting your data first into a list of objects (instead of appending them one at a time), and building off of one of the canonical bar chart examples.
I would pass array of data, not by one item. So, compile the array of proper stations first, and then pass it to d3:
//array of objects
var stations = [{name: "Station 1", air_temperature: 19.5},
{name: "Station 2", air_temperature: 21.5},
{name: "Station 3", air_temperature: 20},];
//adding bars
var bars = canvas.selectAll("rect")
.data(stations)
.enter()
.append("rect")
.attr("width", function(d) {return d}) //guess: pass through function figuring out the right scale
.attr("height", 50) //guess: pass through function figuring out the right scale
.attr("y", function(d,i){return d.air_temperature;}) //guess: multiply by scaleFactor, obtained depending on number of entries in data and canvas size
.attr("fill", "blue"); //guess: pass through d3.scale.linear() function

Would like to use length (from d3.nest) as radius

I'm having a mental block about using the result of:
.rollup(function(leaves) { return leaves.length;})
as the radius of a circle in a scatter plot. My complete code (and sample data) is in a plunk here https://plnkr.co/edit/Cwuce6inLV5jouCWTFfN
The scatter works with a static value of 5 but I'd like to use value based on the .rollup from the d3.nest as explained in this other SO question I had: Capturing leaves.length value from d3.nest
I think I'm missing a key concept about in this section of code:
d3.tsv("etds_small.tsv", function(error, dataset) {
dataset.forEach(function(d) {
if(deptlist.indexOf(d.dept) == -1) deptlist.push(d.dept);
if(years.indexOf(d.year) == -1) years.push(d.year);
})
var deptYearCount = d3.nest()
//.key(function(d) { return d.college;} )
.key(function(d) { return d.dept})
.key(function(d) { return d.year })
.rollup(function(leaves) { return leaves.length;})
.map(dataset);
console.log(dataset); // retains the college, dept, and year labels
console.log(deptYearCount); // replaces labels with "key"
x.domain(years.sort(d3.ascending));
y.domain(deptlist.sort(d3.ascending));
//console.log(y.domain());
//console.log(x.domain());
svg.selectAll(".dot")
.data(dataset) //should this be deptYearCount from the d3.nest above?
.enter().append("circle")
.attr("class", "dot")
.attr("r", 5) // Would like to use length (from d3.nest) as radius
//.attr("r", function(d) {return d.values.length*1.5;}) works with .data(debtYearCount)
.style("opacity", 0.3)
.style("fill", "#e31a1c" )
.attr("cx", function(d) {
return x(d.year);
})
.attr("cy", function(d) {
return y(d.dept);
});
Give this a try for your radius:
.attr("r", function(d) {return Object.keys(deptYearCount[d.dept]).length*1.5;})
Because you are using .map(dataset) instead of .entries(dataset), d3.next() is returning one-big-object instead of an array of objects. That one-big-object does not contain a property called values.
Updated explanation:
First, look at the structure of the object deptYearCount. It has property names like Earth Sciences., Education., etc.
Our d3 data is iterating over an array of objects. Each object has property dept that looks like Earth Sciences., Education., etc.
So, deptYearCount[d.dept] is getting us to the correct property within deptYearCount.
For example, at one round of our iteration we are looking at deptYearCount["Education."]. That turns out to be another object with properties like 2007,2008, etc. Therefore, the number of properties in deptYearCount["Education."] is the value we want for the radius.
How do we find the number of properties of deptYearCount["Education."]? One way is the Object.keys(someObject) function. It returns an array of strings corresonding to the property names, and we just need its .length.

D3.js nested data - how can i display my circles

I am a beginner on D3.js and hit the wall on the following:
I wish to display the score given by an attendee of an event over time. Then as the attendee can give also comments, I would like to place a circle on the curve of the score in the same colour as the scoring.
I succeeded to do this for a single user.
The code is on JS Fiddle http://jsfiddle.net/roestigraben/8s1t8hb3/
Then, trying to extend this to multiple attendees, I run into problems.
The JSFiddle http://jsfiddle.net/roestigraben/Lk2kf1gh/
This code displays nicely the score data for the 3 attendees simulated. However the circles to display the possible comments (there is only one in the data set) from the attendees do not work
I try to filter the attendees array
svg.selectAll("circle")
.data(data.filter(function(d, i){ if(d.comment){return d}; })) // condition here
.enter().append("circle")
.attr("class", "dotLarge")
.attr({r: 5})
.attr("cx", function(d) { return x(d.time); })
.attr("cy", function(d) { return y(d.status); })
I think I need to go deeper into the nesting, but ....my ignorance.
Thanks a lot
Peter
The code where you're displaying your circles doesn't even come close to matching the format of your data. I don't know that you're having a problem with the nest, but you probably want a slightly different data structure when it comes to graphing your comments.
I have updated your fiddle here: http://jsfiddle.net/Lk2kf1gh/7/
The important bit is:
var comments = attendees.map(function(d) {
return {
name: d.name,
comments: d.values.filter(function(e) {
return e.comment != undefined;
})
};
});
//generation of the circles to indicate a comment
// needs to be done with a filter
svg.selectAll("g.comments")
.data(comments)
.enter()
.append("g")
.attr("class", function(d) { return "comments " + d.name })
.selectAll("circle.comment")
.data(function(d) {
return d.comments;
})
.enter()
.append("circle")
.attr("class", "dotLarge comment")
.attr("r", 5)
.attr("cx", function(e) { return x(e.time); })
.attr("cy", function(e) { return y(e.status); });
The first part of this code creates a new data structure that is more focused on the comment information. The second part creates the circles.
Note: I've grouped each person into their own g element and then created a circle for each of their comments. This makes use of d3 nested selections and data binding. Hopefully you can follow what is happening.
I've also added a few more comments to your data for testing. I didn't bother to fix any of the cosmetic issues, I'll leave that up to you.

Show info when hovering over voronoi polygons (in D3.js)

I want to show the city name and population related to the voronoi area hovered over. However, with how I made the voronoi areas, I had to either only send coordinate data and have all of the drawings work, or send more data and none of the voronoi areas are drawn (b/c it can't read the non-coordinate data, and I don't know how to specify within an array or object, at least when creating voronois). I can enter static or irrelevant data for the tooltip (as I did below), but not anything from the actual dataset.
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [w, h]]);
d3.csv("us-cities1.csv", function(d) {
return [projection([+d.lon, +d.lat])[0], projection([+d.lon, +d.lat])[1]];
}, function(error, rows) {
vertices = rows;
drawV(vertices);
}
);
function polygon(d) {
return "M" + d.join("L") + "Z";
}
function drawV(d) {
svg.append("g")
.selectAll("path")
.data(voronoi(d), polygon)
.enter().append("path")
.attr("class", "test")
.attr("d", polygon)
.attr("id", function(d, i){return i;})
.on("mouseover", function(){return tooltip.style("visibility", "visible");})
.on("mousemove", function(){return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+10)+"px").text((this).id);})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
svg.selectAll("circle")
.data(d)
.enter().append("circle")
.attr("class", "city")
.attr("transform", function(d) { return "translate(" + d + ")"; })
.attr("r", 2);
}
I've put together an example using your data to demonstrate what Lars mentions. I created a variable for Voronoi like this:
var voronoi = d3.geom.voronoi()
.x(function(d) { return (d.coords[0]); })
.y(function(d) { return (d.coords[1]); });
which was taken from this Bl.ock by Mike. This allows you to specify the array of coordinates and still have them connected to the descriptive data you want to display.
I then created the object to store all the data in a format that could be used in the Voronio polygons using:
cities.forEach(function (d,i) {
var element = {
coords: projection([+d.lon, +d.lat]),
place: d.place,
rank: d.rank,
population: d.population
};
locCities.push(element);
});
I could have specified the translation of the coordinates in the voronio variable and then just used the cities variable, but I didn't.
The title attribute was used for the tooltips, but this can be replaced with something more appropriate such as what you have in your code. The relevant code is :
.append("title") // using titles instead of tooltips
.text(function (d,i) { return d.point.place + " ranked " + d.point.rank; });
There were a few issues with the data. I had to use an older version of d3 (3.1.5) to get the geojson to render correctly. I know there have been a number of chnanges to the AlberUsa projection so beware there is an issue there.
The location of some of the cities seems wrong to me for instance San Fancisco appears somewhere in Florida (this caused me some confusion). So I checked the original csv file and the coordinates seem to be wrong in there and the data is rendering where it should (just not where I'd expect according to the labels).
Now putting it all together you can find it here

Resources