d3 bring elements to front by class from selector - sorting

I have a d3 map with a number of circles and squares appended. The shapes represent data from different years, and I have a selector to allow users to choose the year, which makes that year's shapes more prominent via a function that (among other things) gives them the class '.currentYear'. There's a live version here.
I need to re-append the circles and rects with class '.currentYear' after the others so that they appear on top. My (rather verbose) function for updating when the selector changes is:
d3.select("#yearSelector").on('change', yearTrans);
function yearTrans() {
selected = this.value;
rec.selectAll('.grant')
.filter('.currentYear')
.classed('currentYear', false)
.on('mouseover', tip.hide)
.transition().duration(1000)
.attr("r", rad/2)
.style("opacity", 0.8);
rec.selectAll('.grant')
.filter('.' + selected)
.classed('currentYear', true)
.transition().duration(1000)
.attr("r", rad)
.style("opacity", 1);
rec.selectAll('.award')
.filter('.currentYear')
.classed('currentYear', false)
.on('mouseover', tip.hide)
.transition().duration(1000)
.attr("x", function(d) { return proj([d.custom_fields.longitude - 40, d.custom_fields.latitude])[0] - (rad/2); })
.attr("y", function(d) { return proj([d.custom_fields.longitude, d.custom_fields.latitude])[1] - (rad/2); })
.attr("width", rad)
.attr("height", rad)
.style("opacity", 0.8);
rec.selectAll('.award')
.filter('.' + selected)
.classed('currentYear', true)
.transition().duration(1000)
.attr("x", function(d) { return proj([d.custom_fields.longitude - 40, d.custom_fields.latitude])[0] - (rad); })
.attr("y", function(d) { return proj([d.custom_fields.longitude, d.custom_fields.latitude])[1] - (rad); })
.attr("width", rad * 2)
.attr("height", rad * 2)
.style("opacity", 1);
rec.selectAll(".grant, .award")
.sort(function(a, b) {
if(a.memberOfClass(".currentYear") && !b.memberOfClass(".currentYear")) {
return -1;
} else if(b.memberOfClass(".currentYear")) {
return 1;
} else {
return 0;
}
});
rec.selectAll(".currentYear")
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
};
One way is to select their parentNode and re-append them. I've come across a number of solutions with this approach such as this one, but they are always on mouseover and I can't seem to make it work in the function.
Another approach is to sort them. I've come across what seems to be the solution here, but I don't understand where to include my class in the sort – I've tried every combination I can think of. The relevant code for the sort is
rec.selectAll(".grant, .award")
.sort(function(a, b) {
if(a.memberOfClass() && !b.memberOfClass()) {
return -1;
} else if(b.memberOfClass()) {
return 1;
} else {
return 0;
}
});
How do I apply my class of .currentYear to have it sort correctly?
I feel like I should be able to figure it out from what I've found, but I obviously lack the knowledge right now. Thanks in advance for any help.

In order to bring the selected year's element on the top do it this way:
Instead of your sort code
rec.selectAll(".grant, .award")
.sort(function(a, b) {
if(a.memberOfClass() && !b.memberOfClass()) {
return -1;
} else if(b.memberOfClass()) {
return 1;
} else {
return 0;
}
});
Add this code which will append the elements on the top.
d3.selectAll("." + selected).each(function()
{
this.parentNode.parentNode.appendChild(this.parentNode)
});

Related

Circles keep accumulating and are not merging correctly

I am trying to merge these circles but I keep getting a graph of accumulating circles as opposed to circles moving across the graph?
What am I missing?
I have attached the code below. This function is called updatechart. It corresponds to a slider. So whenever I move the slider across the screen. I corresponding year it lands on is where the updated circle data should move.
var filteredyears = d3.nest()
.key(function(d) {
if(year === d.year){
return d;
}
}).entries(globaldataset);
var circled = svg.selectAll('.countries')
.data(filteredyears[1].values);
var circledEnter = circled.enter()
circled.merge(circledEnter);
circledEnter.append("circle").attr("cx", function(d) {
return xScale(d.gdpPercap);
})
.attr("cy", function(d) {
return yScale(d.lifeExp);
})
.attr('transform', "translate("+[40,30]+")")
.attr( 'r', function(d) {
return rScale(d.population) / 100})
.style("fill", function(d) {
if(d.continent == 'Asia'){
return '#fc5a74';
} else if (d.continent == 'Europe') {
return '#fee633';
} else if (d.continent == 'Africa') {
return '#24d5e8';
} else if (d.continent == 'Americas') {
return '#82e92d';
} else if (d.continent == 'Oceania') {
return '#fc5a74';
}
})
.style("stroke", "black");
circled.exit().remove();
You have a couple of issues using the merge() method, which is indeed quite hard to understand initially.
First, you have to reassign your selection:
circled = circled.merge(circledEnter);
Now, from this point on, apply the changes to circled, not to circledEnter:
circled.attr("//etc...
Besides that, your exit selection won't work, since you're calling it on the merged selection. Put it before the merge.
Finally, append goes to the circledEnter selection, before merging, as well as all attributes that don't change.
Here is a very basic demo showing it:
var svg = d3.select("svg"),
color = d3.scaleOrdinal(d3.schemeCategory10);
render();
function render() {
var data = d3.range(~~(1 + Math.random() * 9));
var circles = svg.selectAll("circle")
.data(data);
circles.exit().remove();
var circlesEnter = circles.enter()
.append("circle")
.attr("r", 5)
.attr("fill", function(d) {
return color(d);
});
circles = circlesEnter.merge(circles);
circles.attr("cx", function() {
return 5 + Math.random() * 290
})
.attr("cy", function() {
return 5 + Math.random() * 140
});
}
d3.interval(render, 1000);
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>

Conditionally change background image of D3 circles

I am trying to conditionally change the background of some circles created with D3 but I am unable to get the if else logic to correctly pick out the correct picture. The code goes right to the final else statement for the default.gif. I don't see any errors in the console. All the images are in the same directory as the html file.
var diameter = 500, //max size of the bubbles
format = d3.format(",d"),
color = d3.scaleOrdinal(d3.schemeCategory20c); //color category
var bubble = d3.pack()
.size([diameter, diameter])
.padding(1.5);
var svg = d3.select("body")
.append("svg")
.attr("width", diameter)
.attr("height", diameter)
.attr("class", "bubble");
d3.csv("fruit.csv", function(error, data){
if (error) throw error;
//convert numerical values from strings to numbers
data = data.map(function(d){ d.value = +d["Amount"]; return d; });
//Sets up a hierarchy of data object
var root = d3.hierarchy({children:data})
.sum(function(d) { return d.value; })
.sort(function(a, b) { return b.value - a.value; });
//Once we have hierarchal data, run bubble generator
bubble(root);
//setup the chart
var bubbles = svg.selectAll(".bubble")
.data(root.children)
.enter();
//create the bubbles
bubbles.append("circle")
.attr("class", "circle")
.attr("r", function(d){ return d.r; })
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
.append("defs")
.append("pattern")
.append("image")
.attr("xlink:href", function(d) {
if ( d.Fruit == "Apple") {
return "apple.jpg";
}
else if (d.Fruit == "Pear") {
return "pear.jpg"
}
else if (d.Fruit == "Banana") {
return "banana.jpg";
}
else if (d.Fruit == "Strawberry") {
return "strawberry.jpg";
}
else if (d.Fruit == "Grapes") {
return "grapes.jpg";
}
else { return "default.gif"; }
});
//.style("fill", function(d) { return color(d.value); });
//format the text for each bubble
bubbles.append("text")
.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y + 5; })
.attr("text-anchor", "middle")
.text(function(d){ return d.data["Fruit"]; })
.style("fill","white")
.style("font-family", "Helvetica Neue, Helvetica, Arial, san-serif")
.style("font-size", "12px");
});
The csv file contains the following data:
Fruit,Amount
Apple,32
Pear,13
Banana,25
Grapes,29
Strawberry,36
As I said in my comment, I'm surprised that you're seeing anything as the circles' background, since your code for creating/referencing the patterns is not correct. For a quick explanation, check my answer here.
However, I'll address only your main question here, which is the if statement.
The problem is that you're using d3.hierarchy to create your data array. Because of that, all data are inside node.data. According the API:
The returned node and each descendant has the following properties:
node.data - the associated data, as specified to the constructor.
Therefore, for your conditionals, instead of:
d.Fruit == "Apple"
It should be:
d.data.Fruit == "Apple"
And the same for d.data.Fruit == "Pear", d.data.Fruit == "Banana" etc...

D3 update pie data with zero values

I am updating a pie chart with a dynamic JSON feed. My update function is below
function updateChart(data) {
arcs.data(pie(data)); // recompute angles, rebind data
arcs.transition()
.ease("elastic")
.duration(1250)
.attrTween("d", arcTween)
sliceLabel.data(pie(data));
sliceLabel.transition()
.ease("elastic").duration(1250)
.attr("transform", function (d) {
return "translate(" + arc2.centroid(d) + ")";
})
.style("fill-opacity", function (d) {
return d.value == 0 ? 1e-6 : 1;
});
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
When the JSON has 0 values for all objects, the arcs and labels disappear. Exactly what I want to happen.
The problem is when I pass a new JSON after one that was full of zeros, the labels come back and tween etc but the arcs never redraw.
Any suggestions on correcting my update function so that the arcs redraw correctly after they their d values have been pushed to zero?
-- edit --
Lars suggested below that I use the .enter() exactly in the same way as I did when I created the graph. I tried doing this but the results did not change. See new update function below.
this.updatePie = function updateChart(data) {
arcs.data(pie(data))
.enter()
.append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc)
.each(function (d) {
this._current = d
})
arcs.transition()
.ease("elastic")
.duration(1250)
.attrTween("d", arcTween)
sliceLabel.data(pie(data));
sliceLabel.transition()
.ease("elastic").duration(1250)
.attr("transform", function (d) {
return "translate(" + arc2.centroid(d) + ")";
})
.style("fill-opacity", function (d) {
return d.value == 0 ? 1e-6 : 1;
});
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function (t) {
return arc(i(t));
};
}
You've actually hit a bug in D3 there -- if everything is zero, the pie layout returns angles of NaN, which cause errors when drawing the paths. As a workaround, you can check whether everything is zero and handle this case separately. I've modified your change function as follows.
if(data.filter(function(d) { return d.totalCrimes > 0; }).length > 0) {
path = svg.selectAll("path").data(pie(data));
path.enter().append("path")
.attr("fill", function(d, i) { return color(d.data.crimeType); })
.attr("d", arc)
.each(function(d) { this._current = d; });
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
} else {
path.remove();
}
Complete jsbin here.

How do I remove all children elements from a node and then apply them again with different color and size?

So I have the next force layout graph code for setting nodes, links and other elements:
var setLinks = function ()
{
link = visualRoot.selectAll("line.link")
.data(graphData.links)
.enter().append("svg:line")
.attr("class", "link")
.style("stroke-width", function (d) { return nodeStrokeColorDefault; })
.style("stroke", function (d) { return fill(d); })
.attr("x1", function (d) { return d.source.x; })
.attr("y1", function (d) { return d.source.y; })
.attr("x2", function (d) { return d.target.x; })
.attr("y2", function (d) { return d.target.y; });
graphData.links.forEach(function (d)
{
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
};
var setNodes = function ()
{
node = visualRoot.selectAll(".node")
.data(graphData.nodes)
.enter().append("g")
.attr("id", function (d) { return d.id; })
.attr("title", function (d) { return d.name; })
.attr("class", "node")
.on("click", function (d, i) { loadAdditionalData(d.userID, this); })
.call(force.drag)
.on("mouseover", fadeNode(.1)).on("mouseout", fadeNode(1));
};
//append the visual element to the node
var appendVisualElementsToNodes = function ()
{
node.append("circle")
.attr("id", function (d) { return "circleid_" + d.id; })
.attr("class", "circle")
.attr("cx", function (d) { return 0; })
.attr("cy", function (d) { return 0; })
.attr("r", function (d) { return getNodeSize(d); })
.style("fill", function (d) { return getNodeColor(d); })
.style("stroke", function (d) { return nodeStrokeColorDefault; })
.style("stroke-width", function (d) { return nodeStrokeWidthDefault; });
//context menu:
d3.selectAll(".circle").on("contextmenu", function (data, index)
{
d3.select('#my_custom_menu')
.style('position', 'absolute')
.style('left', d3.event.dx + "px")
.style('top', d3.event.dy + "px")
.style('display', 'block');
d3.event.preventDefault();
});
//d3.select("svg").node().oncontextmenu = function(){return false;};
node.append("image")
.attr("class", "image")
.attr("xlink:href", function (d) { return d.profile_image_url; })//"Images/twitterimage_2.png"
.attr("x", -12)
.attr("y", -12)
.attr("width", 24)
.attr("height", 24);
node.append("svg:title")
.text(function (d) { return d.name + "\n" + d.description; });
};
Now, the colors and size dependencies changed and I need to redraw the graph circles (+all appended elements) with different color and radius. Having problem with it.
I can do this:
visualRoot.selectAll(".circle").remove();
but I have all the images that I attached to '.circles' still there.
In any way, any help will be appreciated, let me know if the explanation is not clear enough, I will try to fix it.
P.S. what is the difference between graphData.nodes and d3.selectAll('.nodes')?
Your answer will work, but for posterity, these methods are more generic.
Remove all children from HTML:
d3.select("div.parent").html("");
Remove all children from SVG/HTML:
d3.select("g.parent").selectAll("*").remove();
The .html("") call works with my SVG, but it might be a side effect of using innerSVG.
If you want to remove the element itself, just use element.remove(), as you did. In case you just want to remove the content of the element, but keep the element as-is, you can use f.ex.
visualRoot.selectAll(".circle").html(null);
visualRoot.selectAll(".image").html(null);
instead of .html("") (I wasn't sure which element's children you want deleted). This keeps the element itself, but cleans all included content. It the official way to do this, so should work cross-browser.
PS: you wanted to change the circle sizes. Have you tried
d3.selectAll(".circle").attr("r", newValue);
My first advice is that you should read the d3.js API about selections: https://github.com/mbostock/d3/wiki/Selections
You have to understand how the enter() command works (API). The fact that you have to use it to handle new nodes has a meaning which will help you.
Here is the basic process when you deal with selection.data():
first you want to "attach" some data to the selection. So you have:
var nodes = visualRoot.selectAll(".node")
.data(graphData.nodes)
Then you can modify all nodes each times data is changed (this will do exactly what you want). If for example you change the radius of old nodes which are in the new dataset you loaded
nodes.attr("r", function(d){return d.radius})
Then, you have to handle new nodes, for this you have to select the new nodes, this is what selection.enter() is made for:
var nodesEnter = nodes.enter()
.attr("fill", "red")
.attr("r", function(d){return d.radius})
Finally you certainly want to remove the nodes you don't want anymore, to do this, you have to select them, this is what selection.exit() is made for.
var nodesRemove = nodes.exit().remove()
A good example of the whole process can also be found on the API wiki: https://github.com/mbostock/d3/wiki/Selections#wiki-exit
in this way, I have resolved it very easily,
visualRoot.selectAll(".circle").remove();
visualRoot.selectAll(".image").remove();
and then I just re-added visual elements which were rendered differently because the code for calculating radius and color had changed properties. Thank you.
To remove all element from a node:
var siblings = element.parentNode.childNodes;
for (var i = 0; i < siblings.length; i++) {
for (var j = 0; j < siblings.length; j++) {
siblings[i].parentElement.removeChild(siblings[j]);
}
}`

d3.js color from Json on data change

Using this as an example: http://mbostock.github.com/d3/talk/20111018/treemap.html
I am trying to make color of the rectangles in the tree map dependent on which data is visualized. The example lets you draw the tree map by size or by count. I've added two variables to each entry json file, "cola" and "colb". Each is just a color.
When the graphic is first drawn it is sorted based on size and uses cola to color the rectangles. I have no problem making the map redraw using count rather than size. What I can't figure out is how to get it to use colb instead of cola.
Here's part of the code. This is where the color is first picked.
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy - 1; })
.style("fill", function(d) { return d.cola; });
Here's the change function.
d3.select("select").on("change", function() {
treemap.value(this.value == "size" ? size : count).nodes(root);
if (treemap.value(this.value == "size")) {
cell.append("svg:rect").style("fill", function(d) {return d.cola; });
}
else {
cell.append("svg:rect").style("fill", function(d) {return d.colb; });
};
zoom(node);
});
});
Still trying to figure out d3.
Thanks
You could use some kind of variable in your code that specifies which color you want to use (based on the state of your select control):
var cola = (d3.select("select").node().value == "size");
and
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy - 1; })
.style("fill", function(d) { return cola ? d.cola : d.colb; });
Then, the select button can toggle this variable:
d3.select("select").on("change", function() {
treemap.value(this.value == "size" ? size : count).nodes(root);
cola = (this.value == "size");
zoom(node);
});
Make sure to update the fill style in your zoom function as well:
t.select("rect")
.attr("width", function(d) { return kx * d.dx - 1; })
.attr("height", function(d) { return ky * d.dy - 1; })
.style("fill", function(d) { return cola ? d.cola : d.colb; });
I think the problem is in the line:
if (treemap.value(this.value == "size")) ...
It should read:
if (this.value == "size") ...
Also, you probably mean cell.selectAll('rect').style(...) instead of cell.append('rect').style(...) inside the if-then-else body. However, I cannot say for sure without seeing rest of the code.

Resources