Selecting all elements in one group of a categorical variable - d3.js

I have a scatterplot based on this example. I would like highlight all the dots of one color when mousing over any single dot. Aka, if I hover over a green dot, all the green dots switch to full opacity.
I made a fiddle here: https://jsfiddle.net/nancynancy/mc3tz3dj/55/
As I understand it, my code currently only selects one dot, so I presume I need to create a group for each category of the species variable-- but I'm not sure what that looks like. In R the species variable would be a factor with different levels; what's the analog in D3?
// Part of the plot function
chart.selectAll(".dot")
.data(params.data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", function(d){ return responseScale(d.petalWidth);})
.attr("cx", function(d) { return x(d.sepalWidth); })
.attr("cy", function(d) { return y(d.sepalLength); })
.style("fill", function(d) { return color(d.species);});
// Create a group here, say ".species" that will replace ".dot"?
this.selectAll(".dot")
.on("mouseover", function(d){
d3.select(this)
.transition()
.style("opacity", 1)
})
.on("mouseout", function(d){
d3.select(this)
.transition()
.style("opacity", 0.1)
});

I would probably 'selectAll' the dots and then filter the selection to include only those which have a species property matching that of the mouseover-ed dot like this
chart.selectAll(".dot")
.on("mouseover", function(d){
d3.selectAll('.dot')
.filter(function(dot){
return (dot.species == d.species)
})
.transition()
.style("opacity", 1)
})
.on("mouseout", function(d){
d3.selectAll('.dot')
.filter(function(dot){
return (dot.species == d.species)
})
.transition()
.style("opacity", 0.1)
});
Note, also, I tend to avoid using this where possible as it's value can change depending upon the call site of the containing function -- this can make refactoring awkward
JS Fiddle

Related

Represent the same item twice in a D3 visualisation

The effect I'm going for in a map based visualisation is:
Red circles on a map represent data (things at locations)
When something happens at a location, we briefly (2 seconds) show another, blue circle fading out over the top.
I'm trying to work out how to do this in the D3 paradigm. Specifically: how do you represent the same thing twice?
The problem I run into is that when I try to add the same dataset twice to a given SVG canvas group, nothing gets added. That is, using code like this:
g = svg.append("g");
var feature = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes; });
var emphasis = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes; });
This workaround is ok, but kludgy and potentially limiting:
g2 = svg.append("g");
var emphasis = g2.selectAll("circle")
That is, adding the second group of elements to a different SVG group.
The proper way to do this is to use classes to select the circles (and applying that class when you create them). So you create the features like so:
var feature = g.selectAll("circle.feature")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "feature") // <- assign the class
....
Similarly, for the emphasis:
var feature = g.selectAll("circle.emphasis")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "emphasis") // <- assign the class
....
I've finally (sort of) figured it out. The two sets of data are treated as one because they share the same key, according to the rules of D3 constancy. So an easy way around is to give each set a key that can't overlap:
var feature = g.selectAll("circle")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes * 1; });
var emphasis = g.selectAll("notathing")
.data(stations, function (d) { return d.id + " cannot possibly overlap"; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes * 1; });
The only slight quirk is I have to modify the second selector (g.selectAll("notathing")) so it doesn't match any of the circles created by the first one.

Conflict in transitions

I'm coming to you today with another D3.js problem.
I have a simple bar chart.
When the mouse hovers a bar, a string is displayed on it.
This morning, I added a new transition that changes the background colour of the bar.
Now, the problem is the following:
If the .transition changing the background-color is placed under the one that displays the string, only the background colour changes, the string does not appear.
And if the .transition displaying the string is placed under the one that changes the background colour, only the string appears, without a change in the colour.
Here's a JSFiddle: http://jsfiddle.net/QbGRE/1/
d3.selectAll("div.bar")
.on("mouseover", function(d) {
d3.select(this)
.transition().duration(300)
.style("background-color", "#EE3B3B");
})
.on("mouseout", function(d) {
d3.select(this)
.transition().duration(300)
.style("background-color", "DarkRed");
});
d3.selectAll("div.line")
.append("div")
.attr("class","bar")
.style("width", function(d){return d.occurrence /10 + "px";})
.on("mouseover", function(d) {
d3.select(this)
.append("text").style("pointer-events", "none")
.text(function(d){return d.occurrence + " occurences";});
})
.on("mouseout", function(d) {
d3.select(this)
.select("text").remove();
});
Thank you all for your help, d3-savvy persons
The reason for this is that you're attaching mouse event handlers twice and the later ones overwrite the earlier ones. So first you're attaching the one that adds the text and then later you're attaching the one that changes the color which replaces the first.
The easiest way to fix this is to do everything you want to do on mouse events in one place:
.on("mouseover", function(d) {
d3.select(this)
.append("text").style("pointer-events", "none")
.text(function(d){return d.occurrence + " occurences";});
d3.select(this)
.transition().duration(300)
.style("background-color", "#EE3B3B");
})
.on("mouseout", function(d) {
d3.select(this)
.select("text").remove();
d3.select(this)
.transition().duration(300)
.style("background-color", "DarkRed");
});
Complete example here. Alternatively, you can use different namespaces for the event handlers:
.on("mouseover.text", function(d) {
// etc
});
.on("mouseover.color", function(d) {
// etc
});
Complete example here.

d3.js Add labels in Chord diagram

I am new to this kind of chord visualization. I am working on a sample http://bl.ocks.org/mbostock/4062006:
I would like to add "black", "blonde","brown","red" as labels in the respective chord. How is this possible.
I tried something like this but is not working:
var textLabel = d3.scale.ordinal().range(['Black','Blonde','Brown','Red']);
svg.append("g").selectAll("path")
.data(chord.groups)
.enter()
.append("path")
.style("fill", function(d) { return fill(d.index); })
.style("stroke", function(d) { return fill(d.index); })
.attr("d", d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius))
svg.append("g").append("svg:text")
.attr("x", 6)
.attr("dy", 15)
.append("svg:textPath")
.text(function(d,i) { return textLabel(i+1); })
.on("mouseover", fade(.1))
.on("mouseout", fade(1));
You have forgotten to use the enter selection and also do not link the svg:textPath to an actual path in the SGV. To do this the path you want the text to follow needs to have been given an id attribute and you need to reference this in the textPath with an href attribute.
So you want to add the id attribute to the path you want the text to follow
.attr("id", function(d, i){return "group-" + i;})
You then want to do something like this to add textPath elements
svg.append("g").selectAll("text")
.data(chord.groups)
.enter()
.append("sgv:text")
.attr("x", 6)
.attr("dy", 15)
.append("svg:textPath")
.attr("xlink:href", function(d, i){return "#group-" + i;})
.text(function(d,i) {return textLabel(i+1);});
Finally with the colour black you will probably want to invert the text so you don't end up with black on black text.
.filter(function(d, i){return i === 0 ? true : false;})
.attr("style", "fill:white;");

.data() binding only first element

I am building an epidemic simulation using D3's force-directed diagram.
When a transmission event occurs, I want to move a circle from the transmitter to the newly infected individual.
PROBLEM: Only the first element is created and moved according to the bound data.
First, I gather the coordinates:
xyCoords = getPathogen_xyCoords(newInfections);
Where xyCoords looks like the following:
{receiverX: newInfections[i].x, receiverY: newInfections[i].y, transmitterX: newInfections[i].infectedBy.x, transmitterY: newInfections[i].infectedBy.y}
Then I create the circles and bind them to xyCoords:
d3.select(".svg").append("circle")
.attr("class", "pathogen")
d3.selectAll(".pathogen")
.data(xyCoords)
.attr("cx", function(d) { return d.transmitterX})
.attr("cy", function(d) { return d.transmitterY})
.attr("r", 4)
.style("fill", "green")
Finally, the circle is moved with a transition:
d3.selectAll(".pathogen")
.transition()
.duration(500)
.attr("cx", function(d) { return d.receiverX} )
.attr("cy", function(d) { return d.receiverY} );
EDIT: The game has been up for a few months now and doing quite well! Check it out at http://vax.herokuapp.com!
I've solved my problem...
At creation of the circles, I was not "entering" new circles to be associated with the newly bound data.
It was displaying only the first element on bind because there was only one circle to begin with.
Creation of the circles now looks like this:
xyCoords = getPathogen_xyCoords(newInfections);
var pathogen = svg.selectAll(".pathogen")
.data(xyCoords)
.enter()
.append("circle")
.attr("class", "pathogen")
.attr("cx", function(d) { return d.transmitterX })
.attr("cy", function(d) { return d.transmitterY })
.attr("r", 4)
.style("fill", "green")

circle radius not updating on transition in d3

I am having trouble with a transtion in d3. This jsfiddle illustrates the problem: http://jsfiddle.net/3bzsE/
When the page loads, dataset01 is used to create a circle for each person in the array. d.name is used as the key.
The blue rectangles below the chart are buttons that update the data on click.
Here is the update funciton:
function updateVis(data) {
var points = svg.selectAll('.nameinfo')
.data(data, function(d) { return d.name;});
var pointsEnter = points
.enter()
.append('g')
.attr('class', 'nameinfo');
pointsEnter
.append('circle')
.attr('cx', function(d) { return 10 + d.position * 100; })
.attr('cy', width/2)
.attr('r', 0)
.style('fill', function(d) { return z(d.position); })
.style('fill-opacity', 0.5)
.style('stroke', '#232323')
.append("title")
.text(function(d) { return d.name; });
pointsEnter
.append('text')
.attr('x', function(d) { return 10 + d.position * 100; })
.attr('y', width/2)
.attr("dy", "0.35em")
.style("text-anchor", "middle")
.style("font-size", "11px")
.text(function(d, i) { return d.name; });
pointsUpdate = points
.selectAll('circle')
.transition().duration(500)
.attr('r', function(d){ return d.value/2;});
var pointsExit = points.exit().transition().duration(500).remove();
pointsExit
.selectAll('circle')
.attr('r', 0);
}
The enter and exits are behaving as expected, but circle radius is not changing for names that are present in the entering and exiting datasets.
An example using the values for Jim:
clicking on button three with button one active:
Joe, Janet and Julie exit
Jane and John enter
But, the radius of Jim does not change (it should shrink because d.value changes from 130 to 50)
Clicking on two with three active causes Jim to exit. Clicking on three from two causes Jim to enter with the proper radius from dataset03.
The same behavior can be see with the other names. In all cases enters and exits work, but if two datasets have an element with the same name, radius is not updated on transition
You might have to specifically select the circles for your transition rather than trying to do it on the outer group element. So instead of this:
pointsUpdate = points
.selectAll('circle')
.transition().duration(500)
.attr('r', function(d){ return d.value/2;});
Do this:
svg.selectAll('.nameinfo circle')
.data(data, function(d) { return d.name;})
.transition().duration(500)
.attr('r', function(d){ return d.value/2;});
UPDATE: Below is another way that fits better with the overall D3 philosophy of reusing the existing data/selection:
points
.select('circle').transition().duration(500)
.attr('r', function(d){ return d.value/2;});

Resources