Drawing multiple sets of multiple lines on a d3.js chart - javascript

I have a d3 chart that displays two lines showing a country's imports and exports over time. It works fine, and uses the modular style described in 'Developing a D3.js Edge' so that I could quite easily draw multiple charts on the same page.
However, I now want to pass in data for two countries and draw imports and exports lines for both of them. After a day of experimentation, and getting closer to making it work, I can't figure out how to do this with what I have. I've successfully drawn multi-line charts with d3 before, but can't see how to get there from here.
You can view what I have here: http://bl.ocks.org/philgyford/af4933f298301df47854 (or the gist)
I realise there's a lot of code. I've marked with "Hello" the point in script.js where the lines are drawn. I can't work out how to draw those lines once for each country, as opposed to just for the first one, which is what it's doing now.
I'm guessing that where I'm applying data() isn't correct for this usage, but I'm stumped.
UPDATE: I've put a simpler version on jsfiddle: http://jsfiddle.net/philgyford/RCgaL/

The key to achieving what you want are nested selections. You first bind the entire data to the SVG element, then add a group for each group in the data (each country), and finally get the values for each line from the data bound to the group. In code, it looks like this (I've simplified the real code here):
var svg = d3.select(this)
.selectAll('svg')
.data([data]);
var g = svg.enter().append('svg').append('g');
var inner = g.selectAll("g.lines").data(function(d) { return d; });
inner.enter().append("g").attr("class", "lines");
inner.selectAll("path.line.imports").data(function(d) { return [d.values]; })
.enter().append("path").attr('class', 'line imports')
.attr("d", function(d) { return imports_line(d); });
The structure generated by this looks like svg > g > g.lines > path.line.imports. I've omitted the code for the export line here -- that would be below g.lines as well. Your data consists of a list of key-value pairs with a list as value. This is mirrored by the SVG structure -- each g.lines corresponds to a key-value pair and each path to the value list.
Complete demo here.

The point is that you're thinking to imperative. That's why you have so much code. I really can't put it better than Mike Bostock, you have to start Thinking with Joins:
svg.append("circle")
.attr("cx", d.x)
.attr("cy", d.y)
.attr("r", 2.5);
But that’s just a single circle, and you want many circles: one for each data point. Before you bust out a for loop and brute-force it, consider this mystifying sequence from one of D3’s examples.
Here data is an array of JSON objects with x and y properties, such as: [{"x": 1.0, "y": 1.1}, {"x": 2.0, "y": 2.5}, …].
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 2.5);
I'll leave translating this example to the "from one line to many lines" as an excerxise.

Related

Add points (x,y) to a d3 multi-line graph with time scale x-axis

I'm currently attempting to build a a multi-line graph with a d3.time.scale() for the x-axis.
I'm trying to add circles to each point on lines of the graph, but have been unsuccessful thus far.
When I do something like:
.attr('cx', function(d){ return x(d.price) })
I get a negative number.
I was thinking of setting up another scale (pointsScale) to handle this but have been largely unsuccessful.
What am I doing wrong?
Please refer to my JSBin for the code.
You're running into a few issues here:
Since you made the x-axis a time-scale, I'm guessing that you actually want price to be the y variable, while date is the x variable. That's why x(d.price) is negative - d3 is trying to interpret the prices as dates, which doesn't end up making much sense. So replace your line of code above with this: .attr('cy', function(d){ return y(d.price) })
In order to actually have circles be visible, they need to have three parameters set: cx, cy, and r. Since d3 already knows that your x axis is a time scale, you can set cx with .attr('cx', function(d){ return x(d.date) }). You can make r be whatever radius you want for the circles. Just choose one, or it will default to 0 and you won't be able to see the circles. .attr('r', 4), for instance, would set the radius to a perfectly visible value of 4.
You're drawing the circles before you draw the lines. As a result, the lines get drawn over the circles and it looks kind of weird. So move the circle code to after the line code if you want to avoid that.
Putting it all together, this is roughly what the code to create your circles should look like, and it should go after you declare var paths:
var circles = company.selectAll('circle')
.data(function(d){ return d.values; })
.enter().append('circle')
.attr('cy', function(d){
return y(d.price);}) //Price is the y variable, not the x
.attr('cx', function(d){
return x(d.date);}) //You also need an x variable
.attr('r',4); //And a radius - otherwise your circles have
//radius 0 and you can't see them!
Updated jsbin:
http://jsbin.com/gorukojoxu/edit?html,console,output

D3: gauge chart with growing arc

I want to achieve something like a growing arc which indicates 5 levels (see picture). My data has only an integer value which is between 1-5. You can ignore the icon in the middle for now. Is there any possibility to achieve something like that in d3? I couldn't find any example for this. Moreover I tried it with a cut off pie (donut) chart approach, but I couldn't make the growing arc... I would appreciate any help! Thanks for that.
You can do this with d3 without dependency on external images, SVG sprites or anything in the DOM — just d3.js.
Here's a working fiddle. The implementation is explained below.
But also, here's a more advanced fiddle that animates a clip-path over the growing arc. Check out its predecessor to see how the mask looks without clipping.
First, you need to represent the graphics as an array of data that you bind to with d3. Specifically, you need a color and a "line command" (the string you assign to d as in <path d="...">. Something like this:
var segmentData = [
{ color:"#ED6000", cmd:"M42.6,115.3c5.2,1.5,11,2.4,16.8,2.4c1.1,0,2.7,0,3.7-0.1v-2.2c-7,0-13.1-1.3-18.8-3.6L42.6,115.3z" },
{ color:"#EF7D00", cmd:"M25.7,99.3c4.3,4.7,9.5,8.6,15.3,11.3l-1.4,3.8c-6.9-2.4-13.2-6.1-18.6-10.8L25.7,99.3z" },
{ color:"#F4A300", cmd:"M23.7,97c-5.2-6.4-8.8-14-10.3-22.4L2.9,75.7c2.9,10,8.5,18.9,15.8,25.9L23.7,97z" },
{ color:"#F7BC00", cmd:"M13,71.5c-0.2-2-0.4-4-0.4-6c0-10.7,3.4-20.6,9.2-28.8L9.4,28.3c-5.6,9-8.9,19.6-8.9,30.9 c0,4.6,0.6,9.1,1.6,13.5L13,71.5z" },
{ color:"#FFCF36", cmd:"M63,15.7V0.8c-1-0.1-2.5-0.1-3.7-0.1c-19.9,0-37.5,9.9-48.1,25l12.7,8.6C33.1,23,46,15.7,63,15.7z" }
];
Then you need an empty <svg> and probably a <g> within it, into which to draw the graphics:
var svg = d3.select("body").append("svg")
.attr("width", 125)
.attr("height", 125);
var gauge = svg.append("g");
Then you use d3 binding to create the segments:
var segments = gauge.selectAll(".segment")
.data(segmentData);
segments.enter()
.append("path")
.attr("fill", function(d) { return d.color; })
.attr("d", function(d) { return d.cmd; });
This just creates the graphic, but doesn't color it based on an integer value. For that, you can define an update function:
function update(value) {
segments
.transition()
.attr("fill", function(d, i) {
return i < value ? d.color : "#ccc";
})
}
Calling update(4) will color all but the last segment. Calling update(0) color none (leaving all of them gray).
In the fiddle, there's also a tick() function that calls update with a new value on a setTimeout basis, but that's just for demo.
Finally, if you wish, you can wrap all that code up and create a reusable component by following the advice in [this article].(http://bost.ocks.org/mike/chart/)
since it is relatively simple picture, I'd use a sprite, with 5 variations.
That would be much easier than using d3 and gives the same result.
(you could use some online tool like http://spritepad.wearekiss.com/ )
If you want to mimic duolingo progress images you can just simply copy their solution with own images. They are using sprites as this one: http://d7mj4aqfscim2.cloudfront.net/images/skill-strength-sprite2.svg not the d3.js approach. This will save you a lot of time and effort.

Updating a pie chart with a new data set with more values

I'm relatively new to D3 and have been following a few pie chart tutorials.
Namely the Mike Bostock Tutorials. http://bl.ocks.org/mbostock/1346410
But I have question about a donut chart updating from one data set to another with the 2nd data set having much more values than the first.
I have attempted this numerous times through an update function but no luck, I'll keep it simple and give a hypothetical example , lets say my first data set had 5 values
[1,2,3,4,5]
and my second data set had 10 values
[1,2,3,4,5,6,7,8,9,10]
only 5 values of the new data set would be depicted on the arcs after the dynamic update. It's like the pie is fixed with only 5 arc sections being able to display 5 values of the new dataset.
Any help would be appreciated as its been stumbling around with the idea for awhile!
The key to making it work with data of different size is to handle the .enter() and .exit() selections. This tutorial goes into more detail, but briefly the enter selection represents data for which no DOM elements exist (e.g. in the case where you pass in more data), the update selection (which you're already handling) represents data for which DOM elements exist and the exit selection DOM elements for which no data exists anymore (e.g. when you have more elements to start with).
So in your change function, you would need to do something like this.
function change() {
clearTimeout(timeout);
var path = svg.datum(data).selectAll("path")
.data(pie);
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { this._current = d; }); // add the new arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
path.exit().remove(); // remove old arcs
}
This assumes that you're updating your data variable as you suggest above instead of getting a different value from the existing data structure as in the example.
Here I made a simple update that triggers when you click the text above the pie chart: JsFiddle
The main thing happening is all the data is updated when the .on("click") event triggers, so the chart gets updated like so:
d3.select("#update")
.on("click", function (d) {
data = [1,2,3,4,5,6,7,8,9,10];
vis.data([data]);
arc = d3.svg.arc().outerRadius(r);
pie = d3.layout.pie().value(function(d){return d; });
arcs.data(pie)
.enter()
.append("svg:g")
.attr("class", "slice")
.append("svg:path")
.attr("fill", function(d, i){return color(i);}).attr("d", arc);
});

d3: confusion about selectAll() when creating new elements

I am new to d3 and am using 'Interactive Data Visualization for the Web' by Scott Murray (which is great btw) to get me started. Now everything I saw so far works as described but something got me confused when looking at the procedure to create a new element. Simple example (from Scott Murray):
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
The name "circle" is used for the selectAll which returns an empty selection (which is ok as I learned). Then circles are appended by putting the same name into the .append. Great!
Now what got me confused was what happens when you want to do the same thing again. So you have a second dataset and want to generate new circles in the same way. Using the same code just replacing the dataset will obviously not work as the selectAll("circle") will not return an empty selection anymore. So I played around and found out that I can use any name in the selectAll and even leave it empty like this: selectAll()
Scott Murrays examples always just use one type (circle, text, etc.) per dataset. Finally I found in the official examples something like
svg.selectAll("line.left")
.data(dataset)
.enter()
.append("line")
.attr ...
svg.selectAll("line.right")
.data(dataset)
.enter()
.append("line");
.attr ...
Now my question: How is this entry in selectAll("ENTRY") really used? Can it be utilized later to again reference those elements in any way or is it really just a dummy name which can be chosen in any way and just needs to return an empty selection? I could not find this entry anywhere in the resulting DOM or object structure anymore.
Thank you for de-confusing me.
What you put in the selectAll() call before the call to .data() really only matters if you're changing/updating what's displayed. Imagine that you have a number of circles already and you want to change their positions. The coordinates are determined by the data, so initially you would do something like
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
Now your new data has the same number of elements, but different coordinates. To update the circle positions, all you need to do is
svg.selectAll("circle")
.data(newData)
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; });
What happens is that D3 matches the elements in newData to the existing circles (what you selected in selectAll). This way you don't need to append the circles again (they are there already after all), but only update their coordinates.
Note that in the first call, you didn't technically need to select circles. It is good practice to do so however just to make clear what you're trying to do and to avoid issues with accidentally selecting other elements.
You can find more on this update pattern here for example.

Multiple paths with d3.js and google maps API

I'm trying to add an overlay of some paths defined by GeoJSON data using the Google Maps API. I started by using the examples in this thread and it worked fine with my data -- until I tried using two different paths. The paths need to be rendered with different colors, so I can't combine the datasets.
I've encountered an issue where not all of the paths will render fully. Here's the gist of what I'm attempting to do:
path2 = d3.geo.path().projection(googleMapProjection);
path4 = d3.geo.path().projection(googleMapProjection);
svg.selectAll("path")
.data(line2_geoJson.features)
.attr("d", path2) // update existing paths
.attr("stroke", "red")
.enter().append("svg:path");
svg.selectAll("path")
.data(line4_geoJson.features)
.attr("d", path4) // update existing paths
.attr("stroke", "green")
.enter().append("svg:path");
To show some examples:
http://jsfiddle.net/HWxKu/ -- Notice that the red path renders, but the green path never shows up. (Zoom in a bit.)
http://jsfiddle.net/X644x/ -- The only different is that I switched the order of the two svg.selectAll statements. The green path mostly renders, and bits of the red path render after you zoom in a bit.
Can anyone explain what might be happening? My thought is that either the Google Maps API is imposing some kind of limit on the overlay (perhaps a timeout?), or is something asynchronous happening here? I'm a novice to d3, so any explanation is greatly appreciated.
You're using the enter() selection wrong. The first set of statements to set "d" and "stroke" doesn't do anything at all because there are no paths yet. You need to put these after appending the new elements.
The second problem is that with the second statement, you're overwriting the first paths. By default, D3 matches new to existing data by the array index. That is, the first new feature is matched to the first existing path and so on. You need to supply a function to tell D3 how to match.
The code I think you want looks like this.
svg.selectAll("path")
.data(line2_geoJson.features, function(d) { return d.properties.route_id; })
.enter().append("path")
.attr("d", path2)
.attr("stroke", "red");
svg.selectAll("path")
.data(line4_geoJson.features, function(d) { return d.properties.route_id; })
.enter().append("path")
.attr("d", path4)
.attr("stroke", "green");

Categories

Resources