d3.js selection conditional rendering - javascript

Using the d3js join model, is it possible to do conditional rendering based on the data content?
I want to do something like this:
var nodes = svg.selectAll('.node').data(nodes);
var node = nodes.enter().insert('svg:g').attr('class', 'node');
// if node.hasDuration {
node.insert('svg:rect');
//} else {
node.insert('svg:circle');
//}
nodes.exit().remove();
There doesn't seem to be a way using the join model (enter/exit) to have conditional rendering. I can brute force it with selection.each() but that seems to defeat the purpose of the selection model.

You could use a filter:
var nodes = svg.selectAll('.node').data(nodes);
nodes.enter()
.insert('svg:g')
.attr('class', 'node');
nodes.filter(function(d,i){
return d.hasDuration;
}).append('svg:rect');
nodes.filter(function(d,i){
return !d.hasDuration;
}).append('svg:circle');
Example here.

Related

D3.js v5: How can I use filter() to get the data in the selected region of a brushable bar chart

I was trying to zoom the chart according to the data in the selected region using d3 brush. However, when I tried to use dataselected to receive the filtered data, it just returned an empty array after the selection had been done. I wonder if there is something wrong with my usage of filter() or anything else.
brushed(selection){
if (selection) {
console.log(this.data); // Not empty
let kw0 = this.xScale.invert(selection[0]);
let kw1 = this.xScale.invert(selection[1]);
this.xScale2 = d3.scaleLinear()
.domain([kw0, kw1])
.range([0, vis.width - vis.margin.right]);
this.dataselected = this.data.filter(function(d){
return (kw0 <= this.xScale(d)) && (this.xScale(d) <= kw1);
});
console.log(dataselected); // Empty array
}
The selected bar chart:
Problem solved. No problem with the usage of filter. I used d.x0 and d.x1 instead of this.xScale(d) and it works.

d3 get all nodes of tree

I try to keep this as short as I can:
In the snippet I have a Json which represents a tree. With the help of d3 I try to get all child nodes of the root as an Array. For that I use the function "nodes".
The Problem is that my children key is called "_children" instead of "children". I try to find a good solution to tell the nodes function to check "children" instead of "children". If I remove the "" of all children keys it works.
var json = {"_name":"root","_children":[{"_name":"Application","_children":[{"_name":"Application Heap","_children":[],"_color":"#0000ff", "MEMORY":20},{"_name":"Other","_children":[],"_color":"#000055","MEMORY":30},{"_name":"Statement Heap","_children":[],"_color":"","MEMORY":40}]}]};
console.log(json);
// tell d3 that my children key is "_children"
var treemap = d3.layout.treemap()
.children(function(d) { return d._children; })
.value(function(d) { return d.MEMORY; });
// With this line I try to get all child nodes of the root element
var nodes = treemap.nodes(json)
.filter(function(d) {return !d._children; });
console.log(json); // d3 sets the value and everything else correct
console.log(nodes); // for some reason I get an empty array
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
One possible solution (but not the one I want) is to rename the field recursive like that at first:
var renameKeys=function(obj){
if(obj._children){
obj.children=obj._children;
delete obj._children;
obj.children.forEach(renameKeys);
}
return obj;
};

D3.js animating update of stacked bar graph

I am trying to update a stacked bar chart with transitions as the underlying data is changed. It calls the same "render" function each time and works well when no transitions are involved. However, I would like to animate the changes in values, transitioning from its current state to the next.
I have somewhat solved the problem, but feel like my solution is clunky - hoping there is a better way to do this for stacked bar charts.
My approach has been to do the following:
Load the data
Load the initial conditions (req. for transitions)
Load the final conditions (within a transition)
Copy the current data into another array: prevData
Reload data after interval
Using the above approach, if prevData has values, then use these to set the initial conditions. My problems is that finding and setting the initial conditions feels really clunky:
if (prevData.length > 0) {
//get the parent key so we know who's data we are now updating
var devKey = d3.select(this.parentNode).datum().key;
//find the data associated with its PREVIOUS value
var seriesData = seriesPrevData.find(function (s) { return (s.key == devKey); })
if (seriesData != null) {
//now find the date we are currently looking at
var day = seriesData.find(function (element) { return (element.data.Date.getTime() == d.data.Date.getTime()); });
if (day != null) {
//now set the value appropriately
//console.debug("prev height:" + devKey + ":" + day[1]);
return (y(day[0]) - y(day[1]));
}
}
}
All I'm doing, is finding the correct key array (created by d3.stack()), then trying to find the appropriate previous data entry (if it exists). However, searching parent nodes, and searching through arrays to find the required key and the appropriate data element feels very long-winded.
So, my question is, is there a better way to do this? or parts of this?
Find the previously bound data values associated with this element or the current values before it is changed within a function.
Better way to find the current key being updated rather than using: d3.select(this.parentNode)... ? I've tried passing key values but don't seem to be getting it right. The best I have achieved, is passing a key function to the parent, and looking for it the way described above.
Sorry for the long post, I just spent a whole day working out my solution, frustrated by the fact that all I really needed, was the previous values of an item. Having to do all these "gymnastics" to get what I needed seems very "un" D3.js like :-)
Thanks
Following is a simple example for an animated bar chart. It'll iterate over two different versions of the dataset to show how one can handle changes in the underlying data very easily with d3. There is no need (in this example) for any manual data preparation for the transition/animation.
var data = [
[1, 2, 3, 4, 5],
[1, 6, 5, 3]
];
var c = d3.select('#canvas');
var currentDataIndex = -1;
function updateData() {
// change the current data
currentDataIndex = ++currentDataIndex % data.length;
console.info('updating data, index:', currentDataIndex);
var currentData = data[currentDataIndex];
// get our elements and bind the current data to it
var rects = c.selectAll('div.rect').data(currentData);
// remove old items
rects.exit()
.transition()
.style('opacity', 0)
.remove();
// add new items and define their appearance
rects.enter()
.append('div')
.attr('class', 'rect')
.style('width', '0px');
// change new and existing items
rects
// will transition from the previous width to the current one
// for new items, they will transition from 0px to the current value
.transition()
.duration('1000')
.ease('circle')
.style('width', function (d) { return d * 50 + 'px'; });
}
// initially set the data
updateData();
// keep changing the data every 2 seconds
window.setInterval(updateData, 2000);
div.rect {
height: 40px;
background-color: red;
}
div#canvas {
padding: 20px;
border: 1px solid #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="canvas">
</div>

d3.js v4 - Nested Selections

In d3.js v4, nested selections don't appear to be working as they had in the past.
This works (in v3):
var data = [["1-a", "1-b"], ["2-a", "2-b"]];
var tbody = d3.select("tbody");
var row = tbody.selectAll("tr").data(data);
row.exit().remove();
row.enter().append("tr");
var cell = row.selectAll("td").data(function(d){ return d;});
cell.exit().remove();
cell.enter().append("td");
cell.text(function(d){ return d; });
https://jsfiddle.net/nwozjscs/
But not in v4: https://jsfiddle.net/nwozjscs/1/
My sense is that this has something to do with the merge(...) changes, but I haven't been able to find an example of the proper way to write a nested selection in v4.
I think I figured it out. It appears to work correctly if you merge the enter and update selections into a single selection before joining the next layer of data. This way any new data as well as any existing data at the top level will be correctly taken into account at the next level down.
This makes total sense if you think about it. I think I was just too used to the magic of v3 to see the obvious.
Please comment if there is a better way to do this!
https://jsfiddle.net/nwozjscs/2/
function render(data){
var tbody = d3.select("tbody");
var row = tbody.selectAll("tr").data(data);
var rowenter = row.enter().append("tr");
var cell = row.merge(rowenter)
.selectAll("td").data(function(d){ return d;});
cell.enter().append("td").text(function(d){ return d; });
}
render([["1-a", "1-b"], ["2-a", "2-b"]]);
setTimeout(function(){
render([["1-a", "1-b", "1-c"], ["2-a", "2-b", "2-c"], ["3-a", "3-b", "3-c"]]);
}, 2000);

D3: How to conditionally bind SVG objects to data?

I have here an array of objects that I'm visualising using D3. I bind each object to a group element and append to that an SVG graphic that depends on some object property, roughly like this:
var iconGroups = zoomArea.selectAll("g.icons")
.data(resources)
.enter()
.append("g")
var icons = iconGroups.append(function(d){
if(d.type == "Apple"){
return appleIcon;
}else if(d.type == "Orange"){
return orangeIcon;
})
etc. Now I'd like to extend some of those icons with an additional line. I could add a line element for each data point and set them visible only where applicable, but since I want to add them only for say one out of a hundred data points, that seems inefficient. Is there a way to bind SVG lines to only those objects where d.type == "Apple"?
I would create separate selections for icons and lines, this way:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources);
iconGroups
.enter()
.append('g')
.classed('icons', true);
iconGroups.exit().remove();
var icons = iconGroups.selectAll('.icon').data(function(d) {return [d];});
icons
.enter()
.append(function(d) {
if(d.type === 'Apple'){
return appleIcon;
}else if(d.type === 'Orange'){
return orangeIcon;
}
}).classed('icon', true);
icons.exit().remove();
var lines = iconGroups.selectAll('.line').data(function(d) {
return d.type === 'Apple' ? [d] : [];
});
lines
.enter()
.append('line')
.classed('line', true);
lines.exit().remove();
.exit().remove() is added just because I add it always to be sure that updates work better. :)
Maybe the code is longer than .filter() but I use the following structure all the time and it's easier to scale it.
edit: apropos comment - If you need to pass indexes, you should pass them in binded data:
var iconGroups = zoomArea.selectAll('g.icons')
.data(resources.map(function(resource, index) {
return Object.create(resource, {index: index})
}));
(Object.create() was used just to not mutate the data, you can use _.clone, Object.assign() or just mutate it if it does not bother you)
then you can access it like:
lines.attr("x1", function(d){ console.log(d.index);})
You could add a class to the icons to be selected (e.g. appleIcon), and use that class in a selector to add the lines.
Use d3 filter.
selection.filter(selector)
Filters the selection, returning a new selection that contains only the elements for which the specified selector is true.
Reference: https://github.com/mbostock/d3/wiki/Selections#filter
Demo: http://bl.ocks.org/d3noob/8dc93bce7e7200ab487d

Categories