How to prevent a mouseout after an click in D3? - d3.js

I am working on developing a force directed graph in D3. Right now when I mouseover nodes it changes the opacity of the connected links from 0 to 1 and then when I mouseout it returns the opacity back to 0.
That works fine but what I am having trouble with is making a click on the node maintain the opacity of the links at 1 even after the mouseout event. Then I want to be able to click on other nodes to make their links opacity 1 as well. Then also be able to click on the some of the previously clicked nodes to be able to return the opacity of their associated links to 0.
In short, I want to be able to toggle the opacity of the associated links of a node with out it being affect by mouseout events. A sample of my current code is below. I am thinking I might have to set a new id to toggle on and off when I click on a node?
var nodeClick = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText1 = function(d) {
svg.selectAll(".link")
.filter(function(p) {
return _(d.facets).contains(p.target.name)
})
.transition()
.style('stroke-opacity', 0.9);
};
var overText0 = function(d) {
svg.selectAll(".link")
.transition()
.duration(500)
.style('stroke-opacity', 0);
};
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", function (d) {
return d.group === 1 ? "nodeBig" : "node";
})
.attr("r", function(d) {return d.radius })
.style("fill", function (d) {
return color(d.group);
})
.on("mouseover", overText1)
.on('click', nodeClick)
.on('mouseout', overText0)
.call(force.drag);

I actually finally figured this out on my own. I created a lock field that accepts either a "true" or "false". Then I put an if statement in the mouseoff function that only enables mouseoff functionality on elements that don't have "true" in their lock field.

Related

Unable to remove d3 transition despite calling interrupt on selection

I have a transition running on some circles in d3. The transition brings the circles into view, and updates a text display with their time stamp:
var component = this;
select(this.node).select("#circles").selectAll(".pin")
.data(this.props.data)
.enter()
.append("circle", ".pin")
.attr("r", 5/component.state.zoomScale)
.style("fill", "#ff0000")
.style("opacity", "0.0")
.transition()
.on("start", function(d, i) {
if (i % component.props.multiplier == 0) {
select("#timer").text(d.time);
}
})
.style("opacity", "1.0")
.delay(function(d, i) {return d.delay/component.props.multiplier;});
If my component receives new data, I want to stop any running transitions on the circles, clear the text and remove the circles:
var circles = select(this.node).select("#circles").selectAll(".pin");
if (!circles.empty()) {
circles.interrupt();
select("#timer").text(""); //This is my time display that I want to clear
circles.remove();
While the circles are removed fine, the text reappears after being removed suggesting the transition was never actually stopped. How do I correctly stop the transition running on my circles? I am using d3.js v4 within ReactJS.
you have to set the class separate and not with the append call.
Your selection to interrupt does select nothing, there is no circle with that class.
var component = this;
select(this.node).select("#circles").selectAll(".pin")
.data(this.props.data)
.enter()
.append("circle")
.attr("class", "pin")
.attr("r", 5/component.state.zoomScale)
.style("fill", "#ff0000")
.style("opacity", "0.0")
.transition()
.on("start", function(d, i) {
if (i % component.props.multiplier == 0) {
select("#timer").text(d.time);
}
})
.style("opacity", "1.0")
.delay(function(d, i) {return d.delay/component.props.multiplier;});

D3 Grouped Bar Chart - Selecting entire group?

I have a grouped bar chart similar to https://bl.ocks.org/mbostock/3887051
I used a mouseover function to fade the bars the mouse is currently not over
function mouseover(bar)
{
d3.selectAll(".bar")
.filter(function(d){ return (d != bar);})
.transition(t)
.style("opacity", 0.5);
}
While this works nicely to highlight a single bar, I now need to highlight the entire group / fade everything but this group.
So far I haven't been able to figure out though how to get from the datum element d passed via .on("mouseover", function(d) ... back to the entire group this element belongs to.
Is there a simple way to achieve this in D3v4?
In D3 4.0 the callback function for the .on() method is passed 3 arguments: the current datum (d), the current index (i), and the current group (nodes).
Within the mouseover callback, you can selectAll("rect"), and filter out items which are in the current group (node). With this selection, you then set opacity to 0.5. On mouseout, you just need to set all opacity back to 1.0. The pertinent code is:
...
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
with a small helper function to check if a value is present in an array (array of DOM elements in our case):
function isInArray(value, array) {
return array.indexOf(value) > -1;
}
The full code in context (given your linked example):
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height - y(d.value); })
.attr("fill", function(d) { return z(d.key); })
.on('mouseover', function(d, i, node) {
d3.selectAll("rect")
.filter(function (x) { return !isInArray(this, node)})
.attr('opacity', 0.5);
}
)
.on('mouseout', function() {
d3.selectAll("rect").attr('opacity', 1.0);
});
One solution could be:
Make a function which selects all group and gives it a transition of opacity 0.
The DOM on which mouse is over give opacity 1.
function hoverIn(){
d3.selectAll(".group-me").transition()
.style("opacity", 0.01);//all groups given opacity 0
d3.select(this).transition()
.style("opacity", 1);//give opacity 1 to group on which it hovers.
}
Make a function which selects all group and gives it a transition of opacity 1, when the mouse is out.
function hoverOut(){
d3.selectAll(".group-me").transition()
.style("opacity", 1);
}
On the group add a class and add the mouse out and in function like
g.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.classed("group-me", true)//add a class for selection.
.on("mouseover", hoverIn)
.on("mouseout", hoverOut)
working code here

d3 nav bar strange behaviour on mouseout

I'm trying to develop a d3 navigation menu without using the normal li/ul approach. So far I have 2 levels and I'm using mouse events to trigger the changes. However, the first cycle works okay and the parent items go black on mouseout thereafter things start behaving oddly and this line doesn't execute; svg.selectAll(".lvl1").attr("fill", "black"); but the remove() process works. Have I missed something or is it hanging on an event? Any ideas that will help extend this approach to level 3 would also be appreciated. https://jsfiddle.net/sjp700/djcc6kxq/
lvl2.selectAll(".band")
.attr("width", function (d) { return d.width; })
.attr("height", 18)
.style("opacity", .5) // set the element opacity
.style("stroke", "black")
.attr("class", "tbd")
.style("cursor", "move")
.on('mouseover', over2)
.on('mouseout', out)
.attr("link", function (d) { return d.link; });
}
function out() {
var t = d3.select(this);
t.attr("fill", "pink")
setTimeout(function () {
svg.selectAll(".lvl2").remove();
svg.selectAll(".lvl1").attr("fill", "black");
}, 2000);
}
As mentioned in the comments, you need to style the rect not the g element.
Updated fiddle : https://jsfiddle.net/thatOneGuy/djcc6kxq/1/
Also, I have rearranged the colouring of the rects, so previously you had :
function out() {
var t = d3.select(this);
t.attr("fill", "pink")
//setTimeout(function() {
svg.selectAll(".lvl2").remove();
svg.selectAll(".lvl1 rect").attr("fill", "black");
// }, 2000);
}
But change it to this to keep the last selected tab coloured pink :
function out() {
//setTimeout(function() {
svg.selectAll(".lvl2").remove();
svg.selectAll(".lvl1 rect").attr("fill", "black");
var t = d3.select(this);
t.attr("fill", "pink")
// }, 2000);
}
To be honest, I wouldn't use the remove as when you try mouseover the level 2 elements, because you aren't over the parent anymore, they get removed. I would just create the structure and hide all at first. Then on mouseover of parent, show children, i.e set visibility to visible and on mouseout, set visibility to hidden. Just saves you removing and then recreating elements.

how to update specific node in D3

I'm living updating nodes and links. I also want to change the radius of existing nodes(circles). how can i update eg a specific circle?
When i do this below it updates all circles.
node.select("circle").attr("r", circleradius);
when i update the node array nothing changes in the viz.
nodes[index].size = 2000;
this is how the outputted html looks like
this is the function update code:
function update() {
// Restart the force layout.
force
.nodes(nodes)
.links(links)
.start();
// Update links.
link = link.data(links, function(d) { return d.target.id; });
link.exit().remove();
link.enter().insert("line", ".node")
.attr("class", "link");
// Update nodes.
node = node.data(nodes, function(d) { return d.id; });
node.exit().remove();
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("click", click)
.call(force.drag);
nodeEnter.append("circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 2 || 24.5; });
nodeEnter.append("text")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
node.select("circle")
.style("fill", color);
}
Child elements are not automatically updated until you use d3.select. It's a bit of a hard thing to wrap your head around which I won't get into here.
But for practical purposes, what you need to do is, after you've created your force-directed layout and elements and such, if you want to change a data value used for size element 0 and see it update, the code needs to read like this:
nodes[0].size = 2000;
d3.selectAll("g").select("circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 2 || 24.5; });

d3: How to properly chain transitions on different selections

I am using V3 of the popular d3 library and basically want to have three transitions, followed by each other: The first transition should apply to the exit selection, the second to the update selection and the third to the enter selection. They should be chained in such a manner that when one of the selections is empty, its respective transition is skipped. I.e. when there is no exit selection, the update selection should start immediately. So far, I have come up with this code (using the delay function).
// DATA JOIN
var items = d3.select('#data').selectAll('.item');
items = items.data(data, function(d){
return d.twitter_screenname;
});
// EXIT
items.exit().transition().duration(TRANSITION_DURATION).style('opacity', 0).remove();
// UPDATE
// Divs bewegen
items.transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 1)
.style('left', function(d, i) {
return positions[i].left + "px";
}).style('top', function(d, i) {
return positions[i].top + "px";
});
// ENTER
// Divs hinzufügen
var div = items.enter().append('div')
.attr('class', 'item')
.style('left', function(d, i) {
return positions[i].left + "px";
}).style('top', function(d, i) {
return positions[i].top + "px";
});
div.style('opacity', 0)
.transition().duration(TRANSITION_DURATION).delay(TRANSITION_DURATION * 2)
.style('opacity', 1);
First of all it doesn't allow to "skip" transitions and secondly I think there is a better way than delay. I've looked at http://bl.ocks.org/mbostock/3903818 but I did not really understand what is happening.
Also, somehow just writing items.exit().transition().duration(TRANSITION_DURATION).remove() does not work with the items, probably because they are not SVG elements but divs.
Sure. Here are two ways.
First, you could use an explicit delay, which you then compute using selection.empty to skip empty transitions. (This is only a minor modification of what you have already.)
var div = d3.select("body").selectAll("div")
.data(["enter", "update"], function(d) { return d || this.textContent; });
// 2. update
div.transition()
.duration(duration)
.delay(!div.exit().empty() * duration)
.style("background", "orange");
// 3. enter
div.enter().append("div")
.text(function(d) { return d; })
.style("opacity", 0)
.transition()
.duration(duration)
.delay((!div.exit().empty() + !div.enter().empty()) * duration)
.style("background", "green")
.style("opacity", 1);
// 1. exit
div.exit()
.style("background", "red")
.transition()
.duration(duration)
.style("opacity", 0)
.remove();
http://bl.ocks.org/mbostock/5779682
One tricky thing here is that you have to create the transition on the updating elements before you create the transition on the entering elements; that’s because enter.append merges entering elements into the update selection, and you want to keep them separate; see the Update-only Transition example for details.
Alternatively, you could use transition.transition to chain transitions, and transition.each to apply these chained transitions to existing selections. Within the context of transition.each, selection.transition inherits the existing transition rather than creating a new one.
var div = d3.select("body").selectAll("div")
.data(["enter", "update"], function(d) { return d || this.textContent; });
// 1. exit
var exitTransition = d3.transition().duration(750).each(function() {
div.exit()
.style("background", "red")
.transition()
.style("opacity", 0)
.remove();
});
// 2. update
var updateTransition = exitTransition.transition().each(function() {
div.transition()
.style("background", "orange");
});
// 3. enter
var enterTransition = updateTransition.transition().each(function() {
div.enter().append("div")
.text(function(d) { return d; })
.style("opacity", 0)
.transition()
.style("background", "green")
.style("opacity", 1);
});
http://bl.ocks.org/mbostock/5779690
I suppose the latter is a bit more idiomatic, although using transition.each to apply transitions to selections (rather than derive transitions with default parameters) isn’t a widely-known feature.

Resources