Only one SVG component rendered when calling a method twice - javascript

I use a function to set up event handlers for a click and as it gets fired, the rendition goes as supposed to (fold in on the outer control and fold out on the inner one). Then, clicking it again, the process is retracted. However, the next time I perform the operation, only the outer component changes its size while the inner one does not get affected.
function pieClickOuter(target) {
var pie = d3.layout.pie()
.startAngle(0).endAngle(2 * Math.PI)
.value(function (d) { return d.val; });
var out = d3.svg.arc().innerRadius(90).outerRadius(99);
var org = d3.svg.arc().innerRadius(1).outerRadius(1);
var sub = d3.svg.arc().innerRadius(10).outerRadius(80));
d3.selectAll("#chart .sector path")
.transition().duration(1500).attr("d", out);
var grx = _.chart.selectAll(".subSector")
.data(pie(getData())).enter().append("g")
.attr("class", "subSector")
.on("click", pieClickInner);
grx.append("path")
.attr("d", org).style("fill", function (d) { return colors(d.value); });
grx.selectAll("#chart .subSector path")
.transition().duration(1000).attr("d", sub);
}
function pieClickInner() {
d3.selectAll("#chart .sector path")
.transition().duration(1500)
.attr("d", d3.svg.arc().innerRadius(80).outerRadius(99));
outerPieEvents(d3.selectAll("#chart .sector"));
d3.selectAll("#chart .subSector path")
.transition().duration(1000)
.attr("d", d3.svg.arc().innerRadius(1).outerRadius(1));
}
I cannot for my life see why. According to the console output, all the steps are executed, so it seems that the events are re-set up correctly. Still, the inner component seems to disobey.
See this fiddle

I believe that you want something like this.
I changed only 1 name:
var grx = _.chart.selectAll(".subSector")
to
var grx = _.chart.selectAll(".foo")//or any other name
so, we don't select what already exists.
The problem with this approach is that your SVG will have more and more groups each click. But you can avoid this removing them in your pieClickInner():
d3.selectAll("#chart .subSector path")
.transition().duration(1000)
.attr("d", d3.svg.arc().innerRadius(1).outerRadius(1)).remove();
d3.selectAll("#chart .subSector text").attr("opacity", 0).remove();
d3.selectAll("g.subSector").transition().duration(1000).remove();
I, personally, don't like remove(), i'd simply rebind the data.

Related

Underline node text on mouse over

I have a graph made with d3.js and I have the following attributes and properties for the nodes:
// Enter any new nodes at the parent's previous position
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
.on("click", click)
.on("dblclick", dblclick)
I would like to add the ability to underline the node title when hovering over it. Something like this which unfortunately doesn't work:
var nodeEnter = node.enter().append("g")
.on("mouseover").style("text-decoration","underline")
.on("mouseout").style("text-decoration","none")
EDIT: I would prefer to put a condition to make this happen only for some nodes of the graph.
You aren't using the selection.on() method correctly. In order to do something on an event you need to provide the method with a second parameter: a function that describes the action taken on the event:
D3v6+
.on("mouseover", function(event, datum) { ... })
D3v5 and before
.on("mouseover", function(datum, index, nodes) { ... })
In all versions of D3 this will be the target element (unless using arrow notation). The datum is the data bound to the target element, one item in the array passed to selection.data().
If you only provide one parameter it returns the current event handling function assigned to that event. In your case you likely haven't done this already (because you are attempting to do so), so .on("mouseover").style(...) will return an error such as "Cannot find property style of null" because .on("mouseover") will return null: there is no event handling function assigned to this event to return.
So, to highlight nodes on mouseover with some logic so we can have different outcomes for different nodes, we can use something like:
selection.on("mouseover", function(event, datum) {
if(datum.property == "someValue") {
d3.select(this).style("text-decoration","underline");
}
})
.on("mouseout", function(event,datum) {
d3.select(this).style("text-decoration","none");
})
Where the if statement can be replaced with whatever logic you prefer.
I see you are probably using a hierarchical layout generator, D3's hierarchical layout generators nest the original datum's properties into a data property so that layout properties and non layout properties do not collide, so datum.property may reside at datum.data.property (log the datum if you are having trouble).
var svg = d3.select("body")
.append("svg");
var data = [
"Underline on mouse",
"Never underline"
];
svg.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", 20)
.attr("y", (d,i)=>i*50+40)
.text(d=>d)
.on("mouseover", function(event, datum) {
if(datum == "Underline on mouse") {
d3.select(this).style("text-decoration","underline");
}
})
.on("mouseout", function(event,datum) {
d3.select(this).style("text-decoration","none");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
You can add an underline on hover using CSS
.node:hover{
text-decoration: underline;
}

Assign mouse click handler to line in d3 with snapping behaviour

I have a d3 line that is only one pixel wide. I want to have a on-click
handler assigned to this line. However it is very hard to be exactly on top of the line when trying to select it. Is there some "selection corridor" feature that lets you specify a "distance to line" instead. I.e. the click event is fired if the mouse pointer is, say, in a distance of max 8pt away from a line and the mouse is clicked?
The solution in the other answer is a good solution and an interesting one.
However, I prefer the "traditional" solution, which is painting another line, transparent and thicker than the visible thin line, just to catch the click event. And the reason I prefer this "traditional" solution (the most frequent one among D3 coders) is UX: it's a good idea letting the user know that she/he can click the line.
Thus, the advantage of having a thicker, transparent line over the thin, visible line is that you can set the cursor to a hand...
.attr("cursor", "pointer")
... when the user hover over the transparent line.
Here is a simple demo:
var svg = d3.select("svg");
var data = d3.range(30).map(function(d) {
return {
x: d * 10,
y: Math.random() * 150
}
});
var lineGenerator = d3.line()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
});
var realLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("d", lineGenerator(data));
var transparentLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 12)
.attr("opacity", 0)
.attr("cursor", "pointer")
.attr("d", lineGenerator(data));
transparentLine.on("click", function() {
console.log("clicked")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Of course, this "traditional" solution only makes sense if you have one or few lines on that chart. If you have dozens or hundreds of lines, it will be a complete mess.
You can attack click event on svg/group element and check if the distance from the mouse position and the closest point on the path is less than some fixed value, e.g. 4. The distance can be calculated using the function from Closest Point on Path example.
Click event:
var path = svg.append('path').datum(data).attr("d", line);
svg.on('click', function() {
var m = d3.mouse(this),
p = closestPoint(path.node(), m);
if (p.distance < 4) {
/* make action */
console.log('click', p.distance)
}
})
closestPoint() is taken from the example above.
example: https://jsfiddle.net/xny9bx4v/

D3.js: Trigger an event in a different chart

I have two identical charts. The graphics for them are built like so:
circles.enter().append("circle")
.attr("r", 0)
.attr("fill", function(d) { return fill_color; })
.attr("class", function(d) { return "circle_" + d.id; })
.on("mouseover", function(d, i) { build_tooltip(d, i, this); })
.on("mouseout", function(d, i) { hide_tooltip(d, i, this); });
On mouseover, it triggers the following function:
build_tooltip = function(data, i, element) {
var content = "Title: " + data.title;
show_tooltip(content, d3.event);
}
My question is: How can I make it so mousing over a circle in Chart #1 triggers the same mouseover event in Chart #2, but with unique data for each chart? Chart #2 must generate its own set of data (in this example, just a title). So, how can I make Chart #2's mouseover event fire whenever Chart #1's does?
In jQuery, this would be quite simple -- there is a literal .trigger() event. But how can I go about accomplishing the same with D3?
Have you tried using D3's dispatch? If not, see through this example for more details on how to use it.

d3js attach object to svg

I am creating a very long div containing hundreds of svg lines created by the following method:
function visualizeit(ORFdata,max) {
var browser = d3.select("#viewer")
.append("svg")
.attr("width", max/10)
.attr("height",'50%');
//Add svg to the svg container
for (orf in ORFdata) {
var line = browser.append("svg:line");
var object = ORFdata[orf]
line.datum(object)
line.attr("id", 'mygroup'+orf)
line.attr("x1", function(d){ return ORFdata[orf]["start"]/10})
line.attr("x2", function(d){ return ORFdata[orf]["stop"]/10})
line.attr("y1", function(d){ if (ORFdata[orf]["strand"] == "+1") {return 50} else {return 10}})
line.attr("y2", function(d){ if (ORFdata[orf]["strand"] == "+1") {return 50} else {return 10}})
line.style("stroke", "rgb(6,120,155)")
line.style("stroke-width", orf)
line.on('mouseover', function(d){console.log(d3.select("#mygroup"+orf).datum())})
}
}
However, when I do a mouseover on no matter what line I only get the data back from the last element. At first I thought it was due to 'mygroup' so I added a counter to it +orf but it somehow still erases my older stored data.
When I look in the created html code a svg seems correct by ID at least.
<line id="mygroup50" x1="103356.7" x2="103231.1" y1="10" y2="10" style="stroke: #06789b; stroke-width: 50px;"></line>
But somewhere the link goes awfully wrong...
How I fixed it so far...
var svgContainer = d3.select("body").append("svg")
.attr("width", max/10)
.attr("height", '50%');
//Add svg to the svg container
var lines = svgContainer.selectAll("line")
.data(ORFdata)
.enter()
.append("line")
.attr("x1", function(d){ return d.start/10})
.attr("y1", function(d){ if (d.strand == "+1") {return 65} else {return 10}})
.attr("x2", function(d){ return d.stop/10})
.attr("y2", function(d){ if (d.strand == "+1") {return 65} else {return 10}})
.attr("stroke-width","25")
.attr("stroke",function(d) {if (d.strand == "+1") {return 'green'} else {return 'red'}})
.on('mouseover', function(d) {console.log(d.start)})
}
You're creating a bunch of closures in a loop. Each of the functions you create have the variable orf in their closure scope but your loop is changing the value of orf. By the time the function runs when the mouse over event fires, orf has its final value so therefore your #mygroup + orf selection will always pick up the last element.
Here's a good page on closures that has a section detailing the pitfalls of closures in a loop: http://conceptf1.blogspot.ca/2013/11/javascript-closures.html.
In D3 you can get around this problem by using data joins instead of an external loop. Here's a good tutorial that should help to understand how this works:
http://bost.ocks.org/mike/join/
You need to create different event handlers for each line object, what I mean is store those line ojects them in an associated array or something. This way you are probably overwriting each time.
If you could provide a jsfiddle or something I would be happy to test this theory out for you...

d3 javascript click function call

I'm trying to get d3 to change the color of plot points upon clicking them, but can't seem to get this working at the moment. The commented line below does change the color from white to magenta, but the toggleColor function does not seem to do anything. Actually, the alert only happens when first run, and not when a point is clicked. What am I doing wrong here?
var circle = graph.selectAll("circle.value")
.data(data)
.enter().append("circle")
.attr("class", "value")
.attr("cx", function(d) { return x(d.hour); })
.attr("cy", function(d) { return y(d.value); })
.attr("r", 5)
//.on("click", function(){d3.select(this).attr("class", "flagged");});
.on("click", toggleColor);
var toggleColor = (function(){
// throw in an alert for good measure. . .
alert("Clicked?")
var currentColor = "white";
return function(){
currentColor = currentColor == "white" ? "magenta" : "white";
d3.select(this).atrr("class", "flagged");
}
})();
To begin with, var toggleColor is still undefined at the point where you're wiring up the click event (because it's defined further down the page). So you need to move it up.
Then, the reason the alert appears only once, at runtime, is because that's when that code is run. If you notice, the outer function is executed right after it's declared, as evident by the () at the very last line of the code. That's when alert() gets called. You'll want to move it into the body of the inner function -- the one that's returned -- because that inner function is the code that will actually run on click.

Categories