D3 change visibilty of nodes on click based on class - javascript

I have a bubble chart, where nodes are declared as below and I append for each circle a class which is decided by a array ("category") which decides its category, the variable color is d3.scale.category10() .domain(d3.range(number of elements in "category" array));.
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {return category[d.cluster];})
.text(function(d) { return d.text; })
.filter(function(d){ return d.count >= 1; })
.style("fill", function(d) { return color(d.cluster); })
.call(force.drag);
Next, I make a legend which depends on the categories of each of the circles with their color (as shown above). For this, I do the following
var legend = svg.selectAll(".legend")
.data(color.domain())
.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 category[d]; })
Now, what I want is that when the user clicks the legend text, then the bubbles corresponding to the category of the legend be hidden.
So I add the following to the legend, text object.
.on("click", function(d){
node.selectAll('.'+category[d]).style("visibility", "hidden");
});
But, this does not hide the nodes. Please help.

When you call node.selectAll() it will select all childs of this node that fit the selector. In your case you want to call it on document. So you have to do something like d3.selectAll('.'+category[d])

Related

How to select a specific d3 node group element

I have a D3 v4 force simulation with several nodes. Each node has a group. When I mouse over one of the elements of that group(an invisible circle) I want one of the other elements (the red circle on that specific node only which I gave an id of "backcircle") to do something. Currently this is what I have, but it does it to all nodes not just the one I'm hovering over's element.
this.node = this.d3Graph.selectAll(null)
.data(this.props.nodes)
.enter()
.append("g")
.attr("class", "nodes");
this.node.append("circle")
.attr("id", "backCircle")
.attr("r", 60)
.attr("fill", "red")
this.node.append("svg:image")
.attr("xlink:href", function(d) { return d.img })
.attr("height", 60)
.attr("width", 60)
.attr("x", -30)
.attr("y", -30)
this.node.append("circle")
.attr("r", 60)
.attr("fill", "transparent")
.on( 'mouseenter', function(d) {
d.r = 65;
this.node.select("#backCircle")
.transition()
.attr("r", 80);
}.bind(this))
Before anything else, two important tips:
Do not use "transparent" in an SVG.
IDs are unique. So, use classes instead (or select by the tag name)
Back to your question:
There are several ways of selecting the circle element based on a sibling circle element. The first one is going up the DOM and down again, using this.parentNode. The second one, if you know exactly the sequence of the siblings, is using previousSibling.
In the following demos, I have 3 elements per group: a circle, a text and a rectangle. Hovering over the rectangle will select the circle.
First, the option with this.parentNode. in your case:
d3.select(this.parentNode).select(".backCircle")
Hover over the squares:
var svg = d3.select("svg");
var data = [50, 150, 250];
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d + ",75)"
});
g.append("circle")
.attr("class", "backCircle")
.attr("r", 40)
.attr("fill", "teal")
g.append("text")
.attr("font-size", 20)
.attr("text-anchor", "middle")
.text("FOO");
g.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 20)
.attr("height", 20)
.style("fill", "firebrick")
.on("mouseenter", function() {
d3.select(this.parentNode).select(".backCircle")
.transition()
.attr("r", 50)
}).on("mouseleave", function() {
d3.select(this.parentNode).select(".backCircle")
.transition()
.attr("r", 40)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Then, the option with previousSibling (here, you don't even need to set a class). In your case:
d3.select(this.previousSibling.previousSibling)
Hover over the squares:
var svg = d3.select("svg");
var data = [50, 150, 250];
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d + ",75)"
});
g.append("circle")
.attr("r", 40)
.attr("fill", "teal")
g.append("text")
.attr("font-size", 20)
.attr("text-anchor", "middle")
.text("FOO");
g.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 20)
.attr("height", 20)
.style("fill", "firebrick")
.on("mouseenter", function() {
d3.select(this.previousSibling.previousSibling)
.transition()
.attr("r", 50)
}).on("mouseleave", function() {
d3.select(this.previousSibling.previousSibling)
.transition()
.attr("r", 40)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: Have in mind that, since I'm not using an object, there is no need for bind(this) in my snippets.
I think you need to select the node that is firing the mouseenter event from within its handler.
this.node.append("circle")
.attr("r", 60)
.attr("fill", "transparent")
.on( 'mouseenter', function(d) {
var mouseenterNode = d3.select(this)
mouseenterNode.attr("r", 65);
mouseenterNode.select("#backCircle")
.transition()
.attr("r", 80);
}.bind(this))

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!

How to append text in d3 force layout?

I am trying to add text in force layout. First i am creating a svg group and i am appending circle and text into it. The circle is working fine but the text is not working. Here is the code
var node = svg.selectAll("g")
.data(measures.nodes)
.enter().append("g")
.attr("class", "node")
.call(node_drag);
var circle = node.append("circle")
.attr("fill", "blue")
.attr("r",5)
.attr("dx", ".10em")
.attr("dy", ".10em");
var text = node.append("text")
.data(measures.nodes)
.attr("color", "blue")
.text(function(d){ return d.name; })
The text is of the screen because you've missed out the positioning methods. If you add this you'll see text attached to the nodes.
text.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });

Wrong positioning of D3 nodes

I have a simple force layout that calculates nodes and links when certain buttons are clicked. The first time the nodes are calculated and displayed everything is correctly positioned. However, when nodes are recalculated after another click, the position of the circles I have appended to the nodes are way off yet the text I added remains in the right place. Here's my JS:
//Compute Nodes and Links
var data = this.computePreviewNodes($(e.currentTarget).data("id"), $(e.currentTarget).data("type"));
var canvas = d3.select(".body").append("svg")
.attr("width", width)
.attr("height", screen.height/2)
.append("g");
canvas.append("text")
.text(compObj.name)
.attr("text-anchor", "middle")
.attr("font-size", "2em")
.attr("x", width/2)
.attr("y", 40);
var force = d3.layout.force()
.nodes(data.nodes)
.links(data.links)
.gravity(.05)
.distance(100)
.charge(-10)
.size([width, screen.height/2]);
var links = canvas.selectAll(".links")
.data(data.links)
.enter().append("line")
.attr("class", "links")
.attr("fill", "none")
.attr("stroke", "blue");
var nodes = canvas.selectAll(".nodes")
.data(data.nodes)
.enter()
.append("g")
.attr("class", "nodes")
.call(force.drag);
nodes.append("circle")
.attr("cx", function(d) {return d.x;})
.attr("cy", function(d) {return d.y;})
.attr("r", 10)
.attr("fill", "green");
nodes.append("text")
.text(function(d) {return d.name})
.attr("text-anchor", "right")
.attr("font-size", "1.8em")
.attr("y", 5);
force.on("tick", function(e) {
nodes
.attr("transform", function(d, i){
return "translate(" + d.x + ", " + d.y + ")";
});
links
.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;})
})
force.start();
My computePreviewNodes() function simply comes up with what nodes need to be displayed based on which button is clicked. My thoughts are that maybe I'm not updating my node positions correctly after the second rendering of my nodes. Any ideas?
Here's my canvas at the first click:
And here it is when I click/calculate my nodes once again:

d3js force layout dynamically adding image or circle for each node

I am new to D3JS, I need to create force layout with both image or circle for each node.
that means, A Image node or Circle node be added dynamically.
Is this possible?, if any examples please answer
Well if you just want to have an image in the middle of the circle try this:
/* Create nodes */
var node = vis.selectAll("g.node")
.data(json.nodes) // get the data how you want
.enter().append("svg:g")
.call(node_drag);
/* append circle to node */
node.append("svg:circle")
.attr("cursor","pointer")
.style("fill","#c6dbef")
.attr("r", "10px"})
/* append image to node */
node.append("image")
.attr("xlink:href", "https://github.com/favicon.ico")
.attr("x", -8)
.attr("y", -8)
.attr("width", 16)
.attr("height", 16);
You can also append a title, some text... See the documentation of selection.append(name) for more info.
For Dynamic Image, you can keep the image in local and using image name you can use the dynamic image from the local.
For making circle, you can use :
var groups = node.enter().append("g")
.attr("class", "node")
.attr("id", function (d) {
return d.entityType;
})
.on('click', click)
groups.append("circle")
.attr("cursor", "pointer")
.style("fill", function(d) { return color(d.entityType); })
.style("fill", "#fff")
.style("stroke-width", "0")
.style("stroke", "#ddd")
.attr("r", 20);
Here, d.entityType will be the name of the image example : favicon.png
groups.append("image")
.attr("xlink:href",function (d) {
return "../assets/images/"+d.entityType+".png";
})
.attr("x", -14)
.attr("y", -15)
.attr("width", 30)
.attr("height", 30)
If you want to add text inside the circle, you can use :
groups.append("text")
.attr("dy", 18)
.style("font-size", "2.5px")
.style("text-anchor", "middle")
.style('fill','#000')
.attr("refX", 15)
.attr("refY", -1.5)
.text(function (d) {
return d.entityName;
});

Categories