Interactive Legend onclick or mouseover - D3js - javascript

I've been looking for a way to have my legend control my chart animation (similar to NVD3 examples). I've run into a problem though - nested selections.
var legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(70,10)")
;
var legendRect = legend.selectAll('rect').data(colors);
legendRect.enter()
.append("rect")
.attr("x", w - 65)
.attr("width", 10)
.attr("height", 10)
.attr("y", function(d, i) {
return i * 20;
})
.style("stroke", function(d) {
return d[1];
})
.style("fill", function(d) {
return d[1];
});
I'm using a bit of a hack to do my animation. Basically setting style to display: none.
I want to be able to click on the rectangles and call the function. But putting a mouseover or onclick within legendRect doesn't work. The bars to animate are not children of the legend. How can I call the function, or chain my function to my legend?
function updateBars(opts) {
var gbars = svg.selectAll("rect.global");
var lbars = svg.selectAll("rect.local");
if (opts === "global") {
gbars.style("display", "block") ;
lbars.style("display", "none");
gbars.transition()
.duration(500)
.attr("width", xScale.rangeBand());
};
if (opts === "local") {
lbars.style("display", "block")
;
gbars.style("display", "none");
lbars.transition()
.duration(500)
.attr("x", 1 / -xScale.rangeBand())
.attr("width", xScale.rangeBand());
};
}
My other obstacle is changing the fill color on click. I want it to almost imitate a checkbox, so clicking (to deselect) would turn the fill white. I tried something similar as .on("click",(".style" ("fill", "white"))). But that is incorrect.
Here is my fiddle. For some reason, the function isn't updating things on Fiddle. It works on my localhost though. Not sure the problem with that.

I'm not completely sure I understand you correctly, but if your first question is how to change element X when clicking on element Y, you need something along the lines of:
legendRect.on("click", function() {
gbars.transition()
.duration(500)
.style("display", "block")
// etc...
}
As for changing the fill on click, try:
gbars.on("click", function() {
d3.select(this)
.attr("fill", "white");
}

Related

Change style after a d3 path has been clicked

How can I stop the path style being changed if the path has been clicked? I want path style to not be changed after pathHover has been clicked.
let pathHover = this.svg.append('path')
.data([data])
.attr('class', 'line-padded')
.attr('d', line);
pathHover.on('mouseover', function() {
console.log('path mouse over');
path.style('stroke-width', 6);
});
pathHover.on('mouseleave', function() {
path.style('stroke-width', 4);
});
pathHover.on('click', function() {
console.log('clicked');
path.style('stroke', 'blue');
path.style('stroke-width', 6);
});
There are different ways to achieve this. Since the first D in DDD (also known as D3) means data, the approach I like most is binding a datum to the clicked element, indicating that it was clicked:
d.clicked = true;
Or, if you want to reverse the boolean after a second click:
d.clicked = !d.clicked;
Then, in the mouseover, just check that datum:
if (d.clicked) return;
Here is a demo using green circles: if you mouse over them, they turn red. If you click them, they turn blue, and never turn red (or green) again.
var svg = d3.select("svg");
var circles = svg.selectAll(null)
.data(d3.range(5).map(function(d) {
return {
x: d
}
}))
.enter()
.append("circle")
.attr("cursor", "pointer")
.attr("cy", 75)
.attr("cx", d => 30 + 50 * d.x)
.attr("r", 20)
.style("fill", "lime");
circles.on("mouseover", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "firebrick")
}).on("mouseout", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "lime")
}).on("click", function(d) {
d.clicked = !d.clicked;
d3.select(this).style("fill", "blue")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably a few tactics, either
set path.style("pointer-events", "none") for clicked paths, which will stop all future clicks/mouseover events.
Or if that is too drastic, add a class to clicked paths path.classed("clicked", true), which you could use in a test during your mouseover event before applying any styling changes

D3 onclick, onmouseover and mouseout behavior overriding

I have a D3 multi-line graph that uses the legend for onclick mouseover and mouseout events. Clicking on the legend will hide the line. Mousing over the legend will make the line bold and mousing out will put the line back to normal.
The problem is that if I click the legend and then remove the mouse before the D3 transition completes the transition will not finish. If I keep the mouse over the legend long enough for the transition everything works fine.
To test click a legend rectangle and quickly move the mouse out - the line will not disappear.
Fiddle here: https://jsfiddle.net/goodspeedj/5ewLxpre/
The code for the mouse events is below:
.on("click", function(d) {
var selectedPath = svg.select("path." + d.key);
//var totalLength = selectedPath.node().getTotalLength();
if (d.visible === 1) {
d.visible = 0;
} else {
d.visible = 1;
}
rescaleY();
updateLines();
updateCircles();
svg.select("rect." + d.key).transition().duration(500)
.attr("fill", function(d) {
if (d.visible === 1) {
return color(d.key);
} else {
return "white";
}
})
svg.select("path." + d.key).transition().duration(500)
.delay(150)
.style("display", function(d) {
if(d.visible === 1) {
return "inline";
}
else return "none";
})
.attr("d", function(d) {
return line(d.values);
});
svg.selectAll("circle." + d.key).transition().duration(500)
//.delay(function(d, i) { return i * 10; })
.style("display", function(a) {
if(d.visible === 1) {
return "inline";
}
else return "none";
});
})
.on("mouseover", function(d) {
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "4px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 4 })
// Fade out the other lines
var otherlines = $(".line").not("path." + d.key);
d3.selectAll(otherlines).transition().duration(200)
.style("opacity", 0.3)
.style("stroke-width", 1.5)
.style("stroke", "gray");
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 0.3)
.style("stroke", "gray");
})
.on("mouseout", function(d) {
d3.select(this)
.attr("height", 10)
.attr("width", 25)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "1.5px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 2 })
// Make the other lines normal again
var otherlines = $('.line').not("path." + d.key);
d3.selectAll(otherlines).transition().duration(100)
.style("opacity", 1)
.style("stroke-width", 1.5)
.style("stroke", function(d) { return color(d.key); });
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 1)
.style("stroke", function(d) { return color(dimKey(d)); });
});
Thanks in advance.
You could assign a class to your legend when it's clicked (.clicked), then call setTimeout with an appropriate delay to remove that class once the transition is complete.
When you mouseover or mouseout, first check to see if the legend has the .clicked class. If so, set some delay value as suggested in the other answer, otherwise, proceed without a delay. The advantage of this compared to the other answer is that there would only be a delay if it's needed.
EDIT
If your legend has the class ".legend", modify your code as follows:
.on("click", function(d) {
// Add .clicked class to the legend
$('.legend').addClass('clicked');
// remove clicked class after 750ms. Your duration is 500ms,
// so I'm padding it a bit although you can adjust this as needed
setTimeout(function () { $('.legend').removeClass('clicked') }, 750);
... rest of your function
})
.on("mouseover", function(d) {
// check if legend has been clicked recently and change delay if so
var transitionDelay = 0;
if($('.legend').hasClass('clicked')) transitionDelay = 750;
// your function
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().delay(transitionDelay).duration(200)
.style("stroke-width", "1.5px");
... rest of your function
});
When you have multiple transitions, one can interrupt another. What is happening with your code is that the onclick transition get interrupted by the mouseout transition. This results in the lines not showing showing up. To fix this, just add delay to your mouseout event, so that it occurs after the onclick event has completed. For example, I made the following changes:
added a delay to line 295:
d3.select("path." + d.key).transition().delay(300).duration(200)
.style("stroke-width", "1.5px");
and on line 244 reduced your onclick delay to 200 from 500, just for this test,
svg.select("path." + d.key).transition().duration(200)
.delay(150)

D3.js / Javascript Node-Lines pop-up window and color change

I am working on D3.js and I have a map with lines and nodes. When I come on them with mouse I want them to change color and get thicker and I want a small pop-up window which shows their IDs.
On HTML for the pop-up there is a function onmouseover.
First question: Is there any function in javascript like onmouseover?
Second question: Is there any way me to change the color and make the lines or nodes thicker when the mouse are on that specific node or line. (I can use JQuery if there is a way in JQuery)
For the tooltip I have this fiddle : http://jsfiddle.net/reko91/7NReF/36/
Firstly, create the container for the toolip :
var tooltip = d3.select("body")
.append("div")
.attr('class','tooltipdiv')
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
Then on mouseover set the text to what you want, mousemove, move the tooltip, and mouseout, hide the tooltip :
circles.on("mouseover", function(d){return tooltip.style("visibility", "visible").text(d);})
.on("mousemove", function(){return tooltip.style("top",
(d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px");})
.on("mouseout", function(){return tooltip.style("visibility", "hidden");});
As for the nodes, you can either style it directly or add a class and do it all in one. So you only need to change the CSS. So with the above being said, I have implemented both in this fiddle : http://jsfiddle.net/reko91/7NReF/37/
Source code :
var w = 500;
var h = 50;
var dataset = [ 5, 10, 15, 20, 25 ];
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
circles.attr("cx", function(d, i) {
return (i * 50) + 25;
})
.attr("cy", h/2)
.attr("r", function(d) {
return d;
});
var tooltip = d3.select("body")
.append("div")
.attr('class','tooltipdiv')
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("a simple tooltip");
circles.on("mouseover", function(d){
d3.select(this).classed('hovernode', true)
return tooltip.style("visibility", "visible").text(d);})
.on("mousemove", function(){return tooltip.style("top",
(d3.event.pageY-10)+"px").style("left",(d3.event.pageX+10)+"px");})
.on("mouseout", function(){
d3.select(this).classed('hovernode', false)
return tooltip.style("visibility", "hidden");});
.tooltipdiv{
background:white;
}
.hovernode{
fill:red;
stroke:blue;
stroke-width:5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<body>
</body>
I have added the ability to change the border of the nodes
var node = svg.selectAll(".node")
.data(Data.nodes)
.on("mouseover", function(){
d3.select(this).style("stroke","thick"); // sample CSS operation.
})
this is the element a node in your case which you want to work on.
I would suggest against using jquery. I somehow feel d3 is faster than jquery and can implement similar functionality with greater control.
Okay I wrote a piece of code as an answer to change the color and it works:
.on("mouseover", function(){
d3.select(this)
.attr("fill", "orange");
})
.on("mouseout", function(d){
d3.select(this)
.attr("fill", "rgb(0, 0, " +(d*10) + ")");
});
first .on function makes it orange(you can change the color however you want, second .on makes the original color when you're not on the element anymore.
For pop-up window:
.append("title")
.text(function(d){return d;});

D3 not updating label

I have a map and a matching legend on my website. As the user selects different values from a select list, the map is updated and in the same function, the legend should be updated with new values. As the map actualization works properly, the values of the legend stay the same even in the console are logged the right values if I log the variables.
This is the function that draws the legend:
color_domain = [wert1, wert2, wert3, wert4, wert5];
ext_color_domain = [0, wert1, wert2, wert3, wert4, wert5];
console.log(ext_color_domain);
legend_labels = ["< "+wert1, ""+wert1, ""+wert2, ""+wert3, ""+wert4, "> "+wert5];
color = d3.scale.threshold()
.domain(color_domain)
.range(["#85db46", "#ffe800", "#ffba00", "#ff7d73", "#ff4e40", "#ff1300"]);
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
var ls_w = 20, ls_h = 20;
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.append("text")
.attr("x", 50)
.attr("y", function(d, i){ return height - (i*ls_h) - ls_h - 4;})
.text(function(d, i){ return legend_labels[i]; });
console.log(legend_labels); //gives the right legend_labels but doesn't display them correctly
};
Sadly even the map is updated with new colors they're colored with the old thresholds. This is the way the map is colored:
svg.append("g")
.attr("class", "id")
.selectAll("path")
.data(topojson.feature(map, map.objects.immoscout).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) {
return color(rateById[d.id]);
})
This is tough to answer without a complete, working code sample but...
You are not handling the enter, update, exit pattern correctly. You never really update existing elements, you are only re-binding data and entering new ones.
Say you've called your legend function once already, now you have new data and you do:
var legend = svg.selectAll("g.legend")
.data(ext_color_domain)
.enter().append("g")
.attr("class", "legend");
This re-binds the data and computes an enter selection. It says, hey d3, what data elements are new? For those new ones, you then append a g. Further:
legend.append("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
Again, this is operating on those newly entered elements only. The ones that already existed on the page aren't touched at all.
Untested code, but hopefully it points you in the right direction:
// selection of all enter, update, exit
var legend = svg.selectAll("g.legend")
.data(ext_color_domain); //<-- a key function would be awesome here
legend.exit().remove(); //<-- did the data go away? remove the g bound to it
// ok, what data is coming in? create new elements;
var legendEnter = legend.enter().append("g")
.attr("class", "legend");
legendEnter.append("rect");
legendEnter.append("text");
// ok, now handle our updates...
legend.selectAll("rect")
.attr("x", 20)
.attr("y", function(d, i){ return height - (i*ls_h) - 2*ls_h;})
.attr("width", ls_w)
.attr("height", ls_h)
.style("fill", function(d, i) { return color(d); })
.style("opacity", 0.7);
legend.selectall("text")
...
There's some really great tutorials on this; and it's confusing as hell, but it's the foundation of d3.
An example that helps you get started with updating d3 (d3, v4):
const line = svg.selectAll('line').data(d3Data); // binds things
line.exit().remove(); // removes old data
line.enter()
.append('line') // add new lines for new items on enter
.merge(line) // <--- this will make the updates to the lines
.attr('fill', 'none')
.attr('stroke', 'red');

Getting the value of SVG attributes in D3.js

I am new to D3.js and am trying to build rectangles that represent all nodes from an XML file. So far so good but I want interactivity with each of the rectangles I draw and to be able to capture the nodes that have been touched for further processing. So let's say I click on a rectangle, I can make it react by doing an onclick event (like increasing the font size) but I can't seem to retrieve some of the info. I'd like to create an array with the text of each item that was clicked on.
Here's the code for one instance of the rectangle.
d3.select("#chart")
.append("svg")
.attr("width", 600)
.attr("height", 2000)
.style("background", "#93A1A1")
d3.select("svg")
.append("rect").attr("x", 50)
.attr("y", 25)
.attr("height", 20)
.attr("width", 200)
.attr("title", "resourceDef")
.style("fill", "#CB4B19")
d3.select("svg")
.append("text")
.attr("x", 55)
.attr("y", 37)
.attr("font-size", 11)
.attr("font-family", "sans-serif")
.text("resourceDef")
.on("mouseover", function(d) {
tempText = this.text;
alert(tempText);
d3.select(this)
.attr("font-size", 15)})
.on("mouseout", function(d) {
d3.select(this)
.attr("font-size", 11)})
I can grab style info by using but not the title and I can't find that info anywhere. Thanks for your help, I know it's a long question with probably a simple answer.
You can attach a mouse over event on the rectangle DOM by doing something like this:
d3.select("svg")
.append("rect").attr("x", 50)
.attr("y", 25)
.attr("height", 20)
.attr("width", 200)
.attr("title", "resourceDef")
.style("fill", "#CB4B19")
.on("click", function (d) {
var t = d3.select(this).attr("title");
//pushing the title into the array.
clickedTitles.push(t);
console.log(t);
});
You can get the attribute of a DOM(in your case tite) by doing something like this:
.on("click", function (d) {
var t = d3.select(this).attr("title");
clickedTitles.push(t);
console.log(t)
})
You can store the clicked rectangles title in an array like this:
//create an array
var clickedTitles = [];
//in your click function push the title into the array
clickedTitles.push(t);
//use the clickedTitles where ever you need in the code
Full code is here.

Categories

Resources