This has to be simple, but it's been a while since I was using d3.js and I can't figure out a good solution.
I have a single set of data and I'm using it to create two sets of elements
circles = svg.selectAll('.highcircles')
.data(data)
.enter()
.append('circle');
and
list.selectAll('.states-list')
.data(data)
.enter()
.append('p');
I'd like to be able to have on mouseover of the <p> tags, to have the related circle animate. I can't though think of the way to link the two. Is it through a data-state attribute? Is there a better solution?
selection.filter can be used to filter down a selection based on data. You can use the datum from the <p> event target to filter down a <circle> selection like this:
var circleMatch = svg.selectAll(".highcircles")
.filter(function(d) {
return d.key === targetDatum.key; // 'key' is some datum-unique property
});
You can add "id" attributes to your circles, and then reference those ids in your mouseover function. Something like this:
circles.attr("id", function(d) { return "id" + d; })
list.on('mouseover', function(d) {
d3.select("#id" + d)
.style("fill", "yellow")
})
http://jsfiddle.net/woodedlawn/7ZqZx/
Related
I have a multi-line graph and I'm trying to create an interactive legend that when you click on each name, it will disable the corresponding line. It works only for the first one. Here is my code from this example:
svg.append("text")
.attr("x", (legendSpace / 2) + i * legendSpace) // space legend
.attr("y", height + (margin.bottom / 2) + 5)
.attr("class", "legend") // style the legend
.style("fill", function() { // Add the colours dynamically
return d.color = color(d.key);
})
.on("click", function() {
// Determine if current line is visible
var active = d.active ? false : true,
newOpacity = active ? 0 : 1;
// Hide or show the elements based on the ID
d3.select("#tag" + d.key.replace(/\s+/g, ''))
.transition().duration(100)
.style("opacity", newOpacity);
// Update whether or not the elements are active
d.active = active;
})
.text(d.key);
Also, I have this plunker with my visualisation (the data is just a small part of the dataset).
Thank you in advance.
Generally appending things with a foreach loop isn't the preferable option in d3. This is why you are having difficulty.
The first country in the legend is Albania, and only Albania works in the legend. Why? Because every path in the chart has the same id (the id for Albania):
So, when clicking on Albania in the legend, the on click function searches for the line with id tagAlbania (and looks only for the first entry as IDs should be unique) and toggles it.
The key issue you need to solve is why do all your paths share the same id. Let's look at your code:
dataNest.forEach(function(d, i) {
var country = plot.selectAll(".country")
.data(countries)
.enter().append("g")
.attr("class", "country");
country.append("path")
.attr("class", "line")
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
.attr("d", function(d) {
return line(d.values);
})
.style("stroke", function(d) {
return color(d.name);
});
On the first pass through this for each loop we add a plot for each country using selectAll().data().enter().append(). As we start with an empty selection, .enter().append() will create an element for each item in the data array: all countries' paths are drawn on the first pass.
On the second pass of the loop, the enter selection is empty - there is an element in the initial selection, selectAll('.country'), for each item in the data array. Therefore no elements are entered and appended on each iteration of the loop beyond the first.
Looking back at the first loop, we can see that d is an item in the array dataNest, and on the first loop, d is the zeroth item - Albania. So, as all paths are appended on the first loop, all paths share the same id:
.attr("id", 'tag' + d.key.replace(/\s+/g, '')) // assign ID
To fix that, we could simply change that line to access the datum of the line being appended:
.attr("id", function(datum) { return 'tag' + datum.name.replace(/\s+/g, '') }) // assign ID
As the datum is the item in the array countries, not dataNest here, I've replaced the property key with the property name
Here's an updated plunkr.
Better yet, we can make this more idiomatic with d3 by dropping the loop altogether, see this plunkr.
I am trying to upgrade this stackable bar chart to v4.
Everything works except for one thing.
When I filter one category the bars don't drop to the start of the x-axis. I get an error which says:
state.selectAll(...).forEach is not a function
I've tried multiple things but I can't figure this one out.
This is the broken code:
function plotSingle(d) {
class_keep = d.id.split("id").pop();
idx = legendClassArray.indexOf(class_keep);
//erase all but selected bars by setting opacity to 0
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.attr("width", 0) // use because svg has no zindex to hide bars so can't select visible bar underneath
.style("opacity", 0);
//lower the bars to start on x-axis
state.selectAll("rect").forEach(function(d, i) {
//get height and y posn of base bar and selected bar
h_keep = d3.select(d[idx]).attr("height");
y_keep = d3.select(d[idx]).attr("y");
h_base = d3.select(d[0]).attr("height");
y_base = d3.select(d[0]).attr("y");
h_shift = h_keep - h_base;
y_new = y_base - h_shift;
//reposition selected bars
d3.select(d[idx])
.transition()
.ease("bounce")
.duration(1000)
.delay(750)
.attr("y", y_new);
})
}
I find it strange that this works flawlessly in D3 v3, why wouldn't this work in v4?
In d3 v3 selectAll returned an array, in d3 v4 it returns an object.
From the v3 notes:
Selections are arrays of elements—literally (maybe not literally...).
D3 binds additional methods to the array so that you can apply
operators to the selected elements, such as setting an attribute on
all the selected elements.
Where as changes in v4 include:
Selections no longer subclass Array using prototype chain injection;
they are now plain objects, improving performance. The internal fields
(selection._groups, selection._parents) are private; please use the
documented public API to manipulate selections. The new
selection.nodes method generates an array of all nodes in a selection.
If you want to access each node in v4 try:
selection.nodes().forEach( function(d,i) { ... })
But, this is just the node, to get the data you would need to select each node:
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.nodes().forEach(function(d,i) {
console.log(d3.select(d).data());
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, if you need the data or to modify the selection properties, it could be easier to use selection.each(). d3.each iterates through each element of a d3 selection itself, and allows you to invoke a function for each element in a selection (see API docs here):
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.each( function() {
console.log(d3.select(this).data());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
In v3 of this bar chart, in the forEach loop
`states.selectAll("rect").forEach(function(d,i) {`
d is an array of nodes (the rectangles in each .g).
But, in v4 d3 selections aren't arrays, you can't use a forEach loop in the same way. But you can still get the nodes in it without much modification using selection.nodes() and than get the childNodes to replicate the array in the v3 version:
state.nodes().forEach(function(d, i) {
var nodes = d.childNodes;
Here we go through each element/node in state and get the child rects, returned as an array. Here's an updated fiddle.
I have this table and chart with scattergraph:
https://jsfiddle.net/horacebury/bygscx8b/6/
And I'm trying to update the positions of the scatter dots when the values in the second table column change.
Based on this SO I thought I could just use a single line (as I'm not changing the number of points, just their positions):
https://stackoverflow.com/a/16071155/71376
However, this code:
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
Is giving me this error:
Uncaught TypeError: svg.selectAll(...).data is not a function
The primary issue is that:
svg.selectAll("circle") is not a typical selection as you have redefined svg to be a transition rather than a generic selection:
var svg = d3.select("#chart").transition();
Any selection using this svg variable will return a transition (from the API documentation), for example with transition.selectAll():
For each selected element, selects all descendant elements that match
the specified selector string, if any, and returns a transition on the
resulting selection.
For transitions, the .data method is not available.
If you use d3.selectAll('circle') you will have more success. Alternatively, you could drop the .transition() when you define svg and apply it only to individual elements:
var svg = d3.select('#chart');
svg.select(".line").transition()
.duration(1000).attr("d", valueline(data));
...
Here is an updated fiddle taking the latter approach.
Also, for your update transition you might want to change scale and values you are using to get your new x,y values (to match your variable names):
//Update all circles
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return x(d.date);
})
.attr("cy", function(d) {
return y(d.close);
});
}
I have a scatter graph built with d3.js. It plots circles in the graph for the spending habits of specific people.
I have a select menu that changes the specific user and updates the circles on the scatter graph.
The problem is the old circles are not removed on update.
Where are how should I use .remove() .update(), please see this plnkr for a working example
http://plnkr.co/edit/qtj1ulsVVCW2vGBvDLXO?p=info
First, Alan, I suggest you to adhere to some coding style convention to make your code readable. I know that D3 examples, and the library code per se, almost never promote code readability, but it's in your interest first, because it's much easier to maintain readable code.
Second, you need to understand how D3 works with enter, update and exit sets, when you change data. Mike Bostock's Thinking with Joins may be a good start. Unless you understand how the joins work, you won't be able to program dynamic D3 charts.
Third, here's a bug in updateScatter. name.length makes no sense because your first name variable is value. So it's not the case of deleting old data in the first place.
// Update circles for the selected user chosen in the select menu.
svg.selectAll(".markers")
.data(data.filter(function(d){ return d.FirstName.substring(0, name.length) === value;}))
.transition().duration(1000)
.attr("cx", function(d) { return xScale(d.timestamp); })
.attr("cy", function(d) { return yScale(d.price); });
Also what that weird equality comparison is d.FirstName.substring(0, name.length) === name. Your first name data is not even spaced in CSV file. Plain d.FirstName == name is fair enough. If you expect trailing spaces anyway, just trim your strings in the place where you coerce prices and dates.
This is how correct updateScatter may look look like:
function updateScatter()
{
var selectedFirstName = this.value;
var selectedData = data.filter(function(d)
{
return d.FirstName == selectedFirstName;
});
yScale.domain([
0,
d3.max(selectedData.map(function(d)
{
return d.price;
}))
]);
svg.select(".y.axis")
.transition().duration(750)
.call(yAxis);
// create *update* set
var markers = svg.selectAll(".markers").data(selectedData);
// create new circles, *enter* set
markers.enter()
.append('circle')
.attr("class", 'markers')
.attr("cx", function(d)
{
return xScale(d.timestamp);
})
.attr("cy", function(d)
{
return yScale(d.price);
})
.attr('r', 5)
.style('fill', function(d)
{
return colour(cValue(d));
});
// transition *update* set
markers.transition().duration(1000)
.attr("cx", function(d)
{
return xScale(d.timestamp);
})
.attr("cy", function(d)
{
return yScale(d.price);
});
// remove *exit* set
markers.exit().remove();
}
The example looks like this:
a=[1,2,3,4,5];
svg.selectAll("rect").data(a)
a[1]=10;
a[4]=50;
svg.selectAll("rect").data(a)
The second and the fifth elements of a are changed. And I want to ONLY select these two elements and set their color as red. However, I don't know how to do that, or in other words, select these changed element in d3.js. (As I understand, enter() can't do this job). Does anyone have ideas about this?
There might be a better way of doing this:
//update data array
a[4]=50;
//color update elements
svg.selectAll('rect')
.filter(function(d, i){ return d != a[i]; })
.style('color', 'red')
//bind updated data
svg.selectAll('rect').data(a)
You need a way to store the old data value so that you can compare it against the new one. Maybe add a custom data attribute like this:
a=[1,2,3,4,5];
svg.selectAll("rect").data(a)
.attr("data-currentVal", function(d){return d});
a[1]=10;
a[4]=50;
svg.selectAll("rect").data(a)
.style("fill", function(d) {
if (d3.select(this).attr("data-currentVal") != d) {return red;}
else {return black;}
});
Live example (slightly fancied up so you can see the changes happening):
http://fiddle.jshell.net/5Jm5w/1/
Of course, for the more common example where d is a complex object, you would need to have a way of accessing it's value(s) as a unique string, since the attribute value would always be coerced to string. For example, if you have an (x,y) point, you would need to create a named helper function like dToString = function(d) {return "(" + d.x + "," + d.y + ")";}, and then pass in the name of that function when you set the attribute, and use it again when you compare the old and new.