cant change the opacity of a specific path in D3js sunburst chart - d3.js

My objective is to change the opacity of a specific path.
This is where I am adding a path for each slice in the chart:
h = f.selectAll("path").data(o);
h.enter().append("path")
.attr("id", function (t, n) { return "path-" + n})
.attr("d", x).attr("fill-rule", "evenodd").style("fill", n).on("click", l)
.on("mouseover", function(t,n) {mouseover("path-" + n)});
inside of the mouseover function here is what I have tried:
function mouseover(d){
// d is the id of the path that was hovered over
// d looks like 'path-20'
d3.selectAll("path").style("opacity", 0.3); // changes opacity for entire sunburst chart
d3.selectAll(d).style("opacity", 0.3); // does nothing
d3.selectAll("path-20").style("opacity", 0.3); // does nothing
d3.select(d).style("opacity", 0.3); // does nothing
d3.select("path-20").style("opacity", 0.3); // does nothing
}

Firstly, if you want to select by a class, use .<class-name> instead. If you want to select by an ID, use #<id>. These are universal selectors from CSS, jQuery, vanilla JavaScript, and also D3.
Much easier, though, is that for the function on("mouseover", function() { ... }), on the place of the dots, this now points to the HTMLElement you want to select.
That makes the following function perfect for what you want:
const data = d3.range(5);
d3.select('svg')
.selectAll('rect')
.data(data)
.enter()
.append('rect')
.attr('width', 20)
.attr('height', 20)
.attr('x', function(d, i) {
return 30 * i;
})
.attr('y', 40)
.on('mouseover', function() {
d3.select(this).attr('opacity', 0.2);
})
.on('mouseleave', function() {
d3.select(this).attr('opacity', null);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

Related

D3.js .each() with function that passes data

I would like to put my tooltip into a function such that I can re-use it for multiple elements. When I call the tooltip function for the label element the tooltip displays only the first x value to all labels instead of looping over the X value array. How do I properly access the data in the function?
const tooltip = d3.select('body').append('div')
.attr('id', 'rect-tooltip');
function mouseover(data-x){
d3.select('g')
area.selectAll("text")
.on('mouseover', (d) => {
rect-tooltip.transition()
.duration(100)
.style('opacity', .9)
rect-tooltip.html(`${data-x}`) //Pass in X-values
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
})
.on('mouseout', (d) => {
rect-tooltip.transition()
.duration(400)
.style('opacity', 0);
})
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.each(function(d) {
mouseover(d.x);}); // Only first data point is added to each label?
Without an example of the this, I may be mis-reading your issue.
Problem
The key problem stems from iterating through the text elements twice:
area.each(function (d) {
// do something with each element/datum in the selection
})
d3.selectAll("text")
.on("mouseover", function(d) {
// apply an event listner and corrsesponding function to each text element.
})
The problem is you nest the second in the first. For every element in area you select all the text elements: if you have 2 elements you're selecting all the text twice. You only need to select each text element once.
In the pattern you have, for each element in area we pass that element's datum to the nested function which takes a property of that datum and with d3.selectAll("text").on("mouseover" ... applies that single datum to all text mouseover events. Since you do this for every element in area, we end up overwriting the event listeners multiple times.
No where do you reference the current datum in the chain following d3.selectAll("text"), so we only have a value from the current datum in the current iteration of .each().
Solution
You shouldn't need to use .each() here to apply an event listener, .on() should be sufficient.
We have our mouseover and mouseout functions:
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
Then we can call it with:
selection.on("mouseover",mouseover)
.on("mouseout",mouseout);
And we can resuse this on multiple selections or elements. The datum specific to each element will be used to define the tooltip text.
var tooltip = d3.select(".tooltip");
var svg = d3.select("svg");
var data = [{x: 10},{x:50},{x:90},{x:130},{x:170},{x:210},{x:250},{x:290},{x:330}]
var g = svg.selectAll(null)
.data(data)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+[d.x,0]+")"; })
var rect = g.append("rect")
.attr("width", 35)
.attr("height", 100)
.attr("fill","steelblue")
.on("mousemove",mouseover)
.on("mouseout",mouseout)
var text = g.append("text")
.attr("y", 120)
.attr("x", 18)
.style("text-anchor","middle")
.text(function(d) { return d.x; })
.on("mouseover",mouseover)
.on("mouseout",mouseout)
function mouseover(d) {
tooltip
.style("opacity", 0.9)
.text(d.x)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout() {
tooltip
.style("opacity",0);
}
.tooltip {
position: absolute;
padding: 5px;
background: yellow;
}
rect, text {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div class="tooltip"></div>
<svg width="400" height="300"></svg>
Based on Andrew his comment I changed my code to:
const tooltip = d3.select('body').append('div')
.attr('id', 'tooltip');
function mouseover(d){
tooltip.transition()
.duration(100)
.style('opacity', .9)
tooltip.html(d)
.style('left', `${d3.event.pageX + 10}px`)
.style('top', `${d3.event.pageY - 18}px`);
}
function mouseout(){
tooltip.transition()
.duration(400)
.style('opacity', 0);
}
const label = d3.select('g')
area.selectAll("text")
.data(data)
.join('text')
.attr("class", "label")
.text( (d)=> {return d.name;})
.attr("x", (d)=> {return d.x;})
.attr("y", (d)=> {return d.y;})
.on("mouseover", function(d) { mouseover(d.x); })
.on("mouseout", mouseout);

reversing order of d3.zoom scale and translate

If you click the red button in this example:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
you can see that the chart translates so that the circle is in the center and then zooms in to a specified level (reclicking on the button zooms back out). Translating and then zooming in this way leaves a gap on the left that I would rather not have. How might I change the code so that the chart zooms first and then translates to center so that I don't have this gap in the chart?
I have tried reversing the order of the scale and translate in both the zoom definition and the zoomToExtent function but there is no different in effect.
The ultimate source of the problem is d3.interpolateZoom. This interpolator has scale interpolate faster than translate - even though they mostly both are transitioning at the same time. The pattern implemented with d3.interpolateZoom is based on this paper.
Because scale and translate both interpolate differently in d3.interpolateZoom, you get a gap in the side of your chart as the scale decreases/increases more rapidly than the translate values.
d3.interpolateZoom is used when you call the zoom on a transition.
However, if you apply a transform directly on a transition using .attr(), the d3 transition will use d3.interpolateString, which will search the start and end strings for corresponding numbers and use d3.interpolateNumber on those. This will apply the same interpolation to both scale and translate.
Using both methods we can compare the discrepancy between d3.interpolateZoom and d3.interpolateString. Below the black rectangle uses d3.interpolateString while the orange rectangle uses d3.interpolateZoom. Click on a rectangle to start the transition:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
Where the first rectangle transitions the transform with .attr(), we need to call the zoom afterwards to ensure the zoom has the current transform, we don't need to in this example, but if you wanted to use the zoom after the transform you need to do this
Comparing these two we get:
(Y axis indicates percentage remaining in transition from start attribute to end attribute)
You want scale and translate to move simultaneously at the same rate when transitioning. We can do this if we use a tweening function. Unlike above we can't just use transition().attr("transform",newTransfrom) because you are also drawing canvas and updating the axis. So we'll need to create our own tweening function that can use the current transform and scale, apply it to the axis, canvas, and markers.
For example, rather than calling the zoom (which will use d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
Instead, we can use a tweening function which controls the element's transform and applies the same interpolator to scale and translate:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
You may notice I'm passing a transform value to the zoomed function, since there is no d3.event.transform for this, we need to modify the zoomed function to use the passed parameter if available, otherwise to fall back on the event transform:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
Altogether, that might look something like this.
For another comparison between the two transitioning methods, I've created a gridded comparison that can be toggled between the two zoom identities:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>

D3.js get data index of shape on mouseover event

I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})

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

Update D3 chart dynamically

The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle

Resources