Overlay circle, text, etc. over d3.js multi-series lines - javascript

I have the multi series line chart code with slight modifications to support my data set. This is what I wish to do, and no solution I have looked at seems to function properly for me. I wish to overlay some element (circle, rectange, hidden, whichever) over each point on the line such that I could then attach a mouseover element on that point to display a box with data containing the d.time, d.jobID and how much that differs from an average. If possible, I would like the solution to only do this to the main line (the varying line) rather than the two lines drawn to represent the average. Here, I have a picture of the graph as-is for visual inspection. If that doesn't work, I have also attached it.
I have posted a bit the code below:
d3.tsv("values.tsv", function(error, data) {
color.domain(d3.keys(data[0]).filter(function(key) { return key !== "time" && key !== "jobID"; }));
data.forEach(function(d) {
d.time = parseDate(d.time);
d.jobID = parseInt(d.jobID);
});
var points = color.domain().map(function(name) {
return {
name: name,
values: data.map(function(d) {
return {time: d.time, jobID: d.jobID, value: parseFloat(d[name],10)};
})
};
});
....
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 7)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("mbps");
var point = svg.selectAll(".point")
.data(points)
.enter().append("g")
.attr("class", "point");
point.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
point.append("text")
.datum(function(d) { return {name: d.name, jobID: d.jobID, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.time) + "," + y(d.value.value) + ")"; })
.attr("x", 6)
.attr("dy", ".7em")
.text(function(d) { return d.name; });
});
I have already tried the following code just to see if it worked with my implementation:
point.append("svg:circle")
.attr("stroke", "black")
.attr("fill", function(d, i) { return "black" })
.attr("cx", function(d, i) { return x(d.time) })
.attr("cy", function(d, i) { return y(d.value) })
.attr("r", function(d, i) { return 3 });
D3.JS seems like a pretty awesome piece of work, and I'm fortunate to have it.
EDIT: jsfiddle

The trick is to pass the data again to a selection and then operate on the result of that. Have a look at Mike's tutorial for some background and examples.
I've changed your jsfiddle to add circles here. Attaching svg:title elements or doing something else to show more information should be straightforward. Note that I modified your code to create the data points slightly to include the name with each element. This way, only one additional level of selections is necessary (treat all the points the same and add them in a single pass). The cleaner way to solve this from a code design point of view would be to have 2 additional levels -- first have a selection for the points for an individual line (and add an svg:g element to group them) and then add the points within this group. This would make the code quite a bit more complex and difficult to understand though.

Related

D3 exit mutli-series line chart labels on transition

I'm trying to create a multi-series line chart (based off the Mike Bostock example) but transitioning between multiple data sets. I have gotten the lines to transition in and out, but the labels for each line stick around after they should have disappeared. Screenshot at this link.
Furthermore, the lines are transitioning in an odd way; almost like they are just extending the same line to create a new one (Second screenshot at this link).
Here is the relevant part of my code (where I draw the lines and add labels):
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.Date); })
.y(function(d) { return y(d.candidate); });
var person = svg_multi.selectAll(".candidate")
.data(people);
var personGroups = person.enter()
.append("g")
.attr("class", "candidate");
person
.enter().append("g")
.attr("class", "candidate");
personGroups.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
var personUpdate = d3.transition(person);
personUpdate.select("path")
.transition()
.duration(1000)
.attr("d", function(d) {
return line(d.values);
});
person
.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.Date) + "," + y(d.value.candidate) + ")"; })
.attr("x", 3)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
person.selectAll("text").transition()
.attr("transform", function(d) { return "translate(" + x(d.value.Date) + "," + y(d.value.candidate) + ")"; });
person.exit().remove();
You are appending a new text element to each person every time you render and not removing the old ones. Presumably the code you posted gets run every time you want to draw the chart with new data, so you end up with more text elements every time, rather than updating the existing ones. You need to only append on the "enter" selection. You did this right on the path elements, so you just need to make the text work more like the path. I've updated your example with comments to highlight what I changed.
var line = d3.svg.line()
.interpolate("basis")
.x(function(d) { return x(d.Date); })
.y(function(d) { return y(d.candidate); });
var person = svg_multi.selectAll(".candidate")
// I added a key function here to make sure you always update the same
// line for every person. This ensures that when you re draw with different
// data, the line for Trump doesn't become the line for Sanders, for example.
.data(people, function(d) { return d.name});
var personGroups = person.enter()
.append("g")
.attr("class", "candidate");
// This isn't needed. The path and text get appended to the group above,
// so this one just sits empty and clutters the DOM
// person
// .enter().append("g")
// .attr("class", "candidate");
personGroups.append("path")
.attr("class", "line")
// You do this down below, so no need to duplicate it here
// .attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
// Append the text element to only new groups in the enter selection
personGroups.append("text")
// Set any static attributes here that don't update on data
.attr("x", 3)
.attr("dy", ".35em");
var personUpdate = d3.transition(person);
personUpdate.select("path")
.transition()
.duration(1000)
.attr("d", function(d) {
return line(d.values);
});
person.select("text")
// You don't have to do this datum call because the text element will have
// the same data as its parent, but it does make it easier to get to the last
// value in the list, so you can do it if you want
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 1]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.Date) + "," + y(d.value.candidate) + ")"; })
.text(function(d) { return d.name; });
// Remove this. You don't need it anymore since you are updating the text above
// person.selectAll("text").transition()
// .attr("transform", function(d) { return "translate(" + x(d.value.Date) + "," + y(d.value.candidate) + ")"; });
person.exit().remove();
The key to your question was really just doing personGroups.append('text') rather than person.append('text'). The rest was just me going overboard and pointing out some other ways to improve your code that should make it easier to understand and maintain.

Random loading of labels for cities

I have a world map with the states of Germany and Syria and their cities. Right now they load totally randomly as you can see.
German cities are loaded partially because the labels are missing
The Syrian cities are not loaded at all. When I reload it radomly becomes one of the the pictures i posted.
This is my function for calling germany for example.
d3.json("germany.topo.json", function(error, ger){
if (error) throw error;
var states = topojson.feature(ger, ger.objects.states_germany),
cities = topojson.feature(ger, ger.objects.cities_germany);
g.selectAll(".states")
.data(states.features)
.enter()
.append("path")
.attr("class", "state")
.attr("class", function(d) { return "state " + d.id; })
.attr("d", path);
g.append("path")
.datum(cities)
.attr("d", path.pointRadius('0.35'))
.attr("class", "city");
g.selectAll(".place-label")
.data(cities.features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
});
The date ist here
I can partially reproduce this error. Maybe you can take a look and tell me why it is not working properly.
Thanks in advance
The problem you're having is a result of this call, and the fact that you are repeating it for both the German and Syrian cities:
g.selectAll(".place-label")
.data(cities.features)
.enter().append("text")
.attr("class", "place-label")
.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; })
.attr("dy", ".35em")
.text(function(d) { return d.properties.name; });
You are messing up your selections by selecting all objects with class "place-label" in different calls to d3.json. Instead, try something like the following:
// For German cities
g.selectAll(".german-place-label")
// For Syrian cities
g.selectAll(".syrian-place-label")
This seems to fix your problem, though you might consider rewriting your code so you only need to add all the cities with one call, instead of two separate, nearly identical calls.

D3JS refreshing graph for new Data

I'm trying to update the graph with a new csv file (data2.csv) by calling update but the graph isnt changing. The code as below is the function that would be called when I click a button.
Plnkr is the sample code..
Do advice!
http://plnkr.co/edit/pOYqmaOxy1lmY82jlhfA
<script>
function update(){
d3.csv("data2.csv", function(error, data) {
if (error) throw error;
var ageNames = d3.keys(data[0]).filter(function(key) { return key !== "State"; });
data.forEach(function(d) {
d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});
x0.domain(data.map(function(d) { return d.State; }));
x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) { return d3.max(d.ages, function(d) { return d.value; }); })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Population");
var state = svg.selectAll(".state")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
}
</script>
You say you want to update an existing graph with your update function and new data coming from an csv after some event occurs, correct?
D3 stands for Data Driven Documents, so your data is very important when drawing graphs. D3 works with selections (or collections if that works better for you) based on the data you want to display.
Say you want a barchart displaying the following array: [10,20,30]. The height of the bars is in function with the data in the array.
creating new elements
If you dont have bar elements on the page already, that means you will need to 'append' them to the graph. This is usually done with a code pattern resembling like:
svg.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
With this code, you take the svg variable (which is basically a d3 selection containing one element, the svg) and you select all "rect" elements on the page but inside the svg element. There are none at this very moment, remember, you are going to create them. After the selectAll, you see the data function which specifies the data that will be bound to the elements. Your array contains 3 pieces of data, that means that you expect ot see 3 bars. How will D3 know? It will because of the .enter() (meaning: which elements are not on the graph yet?) and the .append(element) functions. The enter function basically means: In my current d3 selection (being selecAll('rect') ), how many of the specified elements do i need to append? Since you current selection is empty (you dont have 'rect' elements yet), and d3 spots 3 pieces of data in your data function, using .append() it will create and append 3 elements for you. With the attr fuctions you can specify how the elements will look like.
updating elements
Suppose my array of [10,20,30] suddenly changes to [40,50,60]. notice something very important here, my array still contains 3 pieces of data! It is just their value that changed!
I would really want to see my bars reflecting this update! (and i think your case matches this one).
But if I use this pattern again, what will happen?
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
...
The state.selectAll("rect") selection contains 3 elements, d3 checks how many pieces of data you have (still 3) and it sees that it doesnt need to append anything!
Does that mean you cannot update with D3? Absolutely not! It is just much simpler then you would think :-).
If i would want to update my bars so that their height reflects the new values of my data, I should do it like this:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.attr("height", function(d) { return height - y(d.value); });
Basically, I select all my rects, I tell d3 what data i am working on and then I simply tell d3 to alter the height of of my rect. You can even do it with a transition! (more info on transitions here ). I think this is what you need to do, instead of appending the "rect" elements again.
Updating elements, part 2
continuing with my example, what do to if my array all of a sudden wouuld be like this: [100,200,300, 400]? Note that my values changed again BUT there is an extra element there!!
Well, when handling the event (for example a click on a button, or a submit of data) that changes the data, you need to tell D3 that it will need to append something and update the existing bars. This can simply be done by doing coding both patterns:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
state.selectAll("rect")
.data(function(d) { return d.ages; })
.attr("height", function(d) { return height - y(d.value); });
Removing elements
What if my data array would suddenly only consist of only 2 pieces of data: [10,20] ?
Just like there is a function for telling d3 that it needs to append something, you can tell it that it what to do with elements that dont seem to have data to be bound on anymore:
svg.selectAll("rect")
.data(function(d) { return d.ages; })
.exit().remove();
The exit function matches the amount of pieces of data you have vs the amount of selected elements. The exit function then tells d3 to drop those elements.
I hope this was helpfull. It is a bit of a basic explanation (its a little more complicated then that) but I had to hurry, so if there should be questions or errors, please tell me.
d3.csv("data2.csv", function(error, data) {
if your server is caching this reference - try a Math.random() :D
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
This will force(ish) a refresh of data - could be costly so triage according to your needs via a serverside process
edit:
d3.csv("data2.csv?=" + (Math.random() * (100 - 1) + 1), function(error, data) {
would be the alteration. Its sloppy but illustrates how to suggestively force a cache refresh
You can do it like this:
Make a buildMe() function which makes the graph.
function buildMe(file) {//the file name to display
d3.csv(file, function(error, data) {
if (error) throw error;
var ageNames = d3.keys(data[0]).filter(function(key) {
return key !== "State";
});
data.forEach(function(d) {
d.ages = ageNames.map(function(name) {
return {
name: name,
value: +d[name]
};
});
});
x0.domain(data.map(function(d) {
return d.State;
}));
x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.ages, function(d) {
return d.value;
});
})]);
svg.selectAll("g").remove();//remove all the gs within svg
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Population");
var state = svg.selectAll(".state")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) {
return "translate(" + x0(d.State) + ",0)";
});
state.selectAll("rect")
.data(function(d) {
return d.ages;
})
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) {
return x1(d.name);
})
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
})
.style("fill", function(d) {
return color(d.name);
});
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
});
}
Then on Load do this buildMe("data.csv");
On button click do this
function updateMe() {
console.log("Hi");
buildMe("data2.csv");//load second set of data
}
Working code here
Hope this helps!

Update line order (z-orientation) on mouseover in d3

I am new to using d3 and JavaScript and would appreciate some constructive feedback. I'm mocking up a practice example to learn d3 that involves plotting climate data (temperature anomaly) from 1880-2010 from two sources (GISS and HAD). So far I have generated a multiple line chart in d3 using this data. Code and data are here https://gist.github.com/natemiller/0c3659e0e6a0b77dabb0
In this example the data are originally plotted grey, but each line colored a different color on mouseover.
I would like to add two additional features.
I would like, on mouseover, to reorder the lines so that the moused-over line is on top, essentially reorder the lines. I've read that this requires essentially replotting the SVG and I have tried code along the lines of this
source.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", "lightgrey")
.on("mouseover", function() {
if (active) active.classed("highlight", false);
active = d3.select(this.parentNode.appendChild(this))
.classed("highlight", true);
})
.style("stroke",function(d) {return color(d.name);})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.attr("id", function(d, i) { return "path-" + d.name; });
where the .on("mouseover"... code is meant to highlight the current "moused-over" line. It doesn't seem to work in my example. All the lines are highlighted initially and then turn grey with the mouseover/mouseout. If someone could help me identify how to update my code so that I can reorder the lines on mouseover that would be great!
I have been playing around with labeling the lines such that when either the line or its label is moused-over the line and label colors update. I've played around a bit using id's but so far I can't get both the text and the line to change color. I've managed to 1. mouseover the line and change the color of the text, 2. mouseover the text and change the color of the line, 2. mouseover the line and change the line, but not have both the line and the text change color when either of them are moused-over. Here is a section of code that serves as a start (using ids), but doesn't quite work as it only specifies the path, but not the text and the path. I've tried adding them both to d3.select('#path-','#text-'..., and variations on this, but it doesn't seem to work.
source.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", "lightgrey")
.on("mouseover", function(d){
d3.select(this)
.style("stroke",function(d) {return color(d.name);});
})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.attr("id", function(d, i) { return "path-" + d.name; });
source.append("text")
.datum(function(d) { return {name: d.name, value: d.values[d.values.length - 15]}; })
.attr("transform", function(d) { return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")"; })
.attr("x", 5)
.attr("dy", ".35em")
.style("stroke", "lightgrey")
.on("mouseover", function(d){
d3.select('#path-' + d.name)
.style("stroke",function(d) {return color(d.name);});
})
.on("mouseout", function(d) {
d3.select('#path-' + d.name)
.transition()
.duration(750)
.style("stroke", "lightgrey")
})
.text(function(d) { return d.name; })
.attr("font-family","sans-serif")
.attr("font-size","11px")
.attr("id", function(d, i) { return "text-" + d.name; });
I greatly appreciate your help. I am new to d3 and this help-serve. Its a steep learning curve at the moment, but I hope this example and the code is reasonably clear. If its not let me know how I can make it better and I can repost the question.
Thanks so much,
Nate
Chris Viau provided a good answer to this question over on the d3 Google group.
https://groups.google.com/forum/?fromgroups=#!topic/d3-js/-Ra66rqHGk4
The trick is to select the path's g parent to reorder it with the others:
this.parentNode.parentNode.appendChild(this.parentNode);
This appends the current selection's container "g" on top of all the other "g".
I've found this useful in lots of other instances as well.
Thanks Chris!

How to set the label for each vertical axis in a parallel coordinates visualization?

I'm new to d3.js (and stackoverflow) and I'm currently working through the parallel coordinates example. I'm currently using a 2d array named 'row' for the data. Above each vertical axis is the label '0' or '1' or '2', etc. However, I'd like each vertical axis to be labeled with the text in row[0][i]. I believe the numbers 0,1,2 are coming from the datum. Any suggestions on how I may use the labels in row[0][i] instead? I suspect I'm doing something wrong that's pretty basic. Here's the relevant code. Thanks !
// Extract the list of expressions and create a scale for each.
x.domain(dimensions = d3.keys(row[0]).filter(function (d, i) {
return row[0][i] != "name" &&
(y[d] = d3.scale.linear()
.domain(d3.extent(row, function (p) { return +p[d]; }))
.range([height, 0]));
}));
// Add a group element for each dimension.
var g = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function (d) { return "translate(" + x(d) + ")"; });
// Add an axis and title.
g.append("g")
.attr("class", "axis")
.each(function (d) { d3.select(this).call(axis.scale(y[d])); })
.append("text")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(String);//.text(String)
If you only have a controlled set of Axis (like three axis), you may just want to set them up, individually, as follows...
svg.append("text").attr("class","First_Axis")
.text("0")
.attr("x", first_x_coordinate)
.attr("y", constant_y_coordinate)
.attr("text-anchor","middle");
svg.append("text").attr("class","Second_Axis")
.text("1")
.attr("x", first_x_coordinate + controlled_offset)
.attr("y", constant_y_coordinate)
.attr("text-anchor","middle");
svg.append("text").attr("class","Third_Axis")
.text("2")
.attr("x", first_x_coordinate + controlled_offset*2)
.attr("y", constant_y_coordinate)
.attr("text-anchor","middle");
However, if you have dynamically placed axis that rely on the data, you may want to place the axis info using a function that holds the y coordinate constant while determining the x coordinate based on a fixed "offset" (i.e. data driven axis placement). For example...
svg.append("text")
.attr("class",function(d, i) {return "Axis_" + i; })
.text(function(d,i) {return i; })
.attr("x", function(d, i) { return (x_root_coordinate + x_offset_value*i); })
.attr("y", constant_y_coordinate)
.attr("text-anchor","middle");
I hope this helps.
Frank

Categories