Is it expensive to call D3.js transition multiple times during scroll? - d3.js

I have a scroll event listener that triggers a D3.js transition when the page hits a specific scroll target. The animation works nicely and doesn't lag, but the transition is called on each scroll position. Is this expensive? After the first draw() the #mySvgShape is set to fill-opacity:0.8. Is it an issue to keep calling the transition during the scroll?
scrollPosition is the percentage (0 to 1) of the page that has been scrolled.
function draw(scrollPosition) {
if (scrollPosition > 0.2 && scrollPosition < 0.4) {
el.select('#mySvgShape')
.transition()
.style("fill-opacity", "0.8");
}else{
el.select('#mySvgShape')
.transition()
.style("fill-opacity", "1e-6");
}

You can check if there is any transition happening and, if yes, do not call the transition again.
To do that, use d3.active:
d3.active(node[, name]):
returns the active transition on the specified node with the specified name, if any.
So, your if can be changed to this:
if (percentage > 0.2 && percentage < 0.4) {
if (!d3.active(d3.select("#dam-fill").node())) {
d3.select("#dam-fill")
.transition()
.duration(5000)
.style("fill-opacity", "1")
.attr("transform", "translate(-50, -200) scale(1.2,1.2)");
}
}
Here is your updated fiddle: https://jsfiddle.net/gerardofurtado/ze2dc976/
I changed the transition to 5000 ms just to show that, if you scroll up or down while the transition is happening, it doesn't stop (as in your fiddle). Thus, not only calling transition multiple times in the scroll is expansive, it will mess the transition.

Related

d3 (v4) zoom using transitions doesn't seem to work

I am using the d3 zoom behaviour and attempting to use a transition for both a translateTo and a scaleTo at the same time. If I make the calls to zoom without a transition everything works fine. If I use a transition for just one of the transforms it also works, but if I attempt to use a transition for both it fails (it appears to only apply the first transform). I have a JSFiddle with several combinations here: JSFiddle
Here's the code that isn't working as I expect
svg.transition()
.duration(750)
.call(zoom.scaleTo, 2)
.call(zoom.translateTo, 50, 50)
You can do like this:
svg.transition()
.duration(750)
.call(zoom.scaleTo, 2)
.transition() <--- this is missing.
.call(zoom.translateTo, 50, 50)
First zoom then translate.
working code here
EDIT
Performing zoom and translate both # same time you need to tween.
function twizzle(selection, duration) {
d3.select(selection).transition()
.duration(duration)
.tween("attr:transform", function() {
//interpolate from start to end state
var i = d3.interpolateString("scale(1)translate(0,0)", "scale(2)translate(50,50)");
return function(t) {
selection.attr("transform", i(t));
};
});
}
Now call the function like this:
d3.select('body')
.append('button')
.text('transition both - scale first')
.on('click', function() {
//on click call the function created above
group.call(twizzle, 750) <-- perform the scale and translate on the group not on the SVG.
})
working code here

D3 Selection Highlight (efficiency?)

I have a simple visual of many rects, over 100 I'd say. For aesthetic purposes I want to create a high light effect on mouse click. I also wanted to make this effect somewhat intuitive by removing that effect once the user clicks on a new rect. However I couldn't get this to work without resorting to a d3.selectAll() call, so I'm thinking this approach might not be ideal if this project gets any bigger. Here is the code:
.on('click.highlight', function() {
//set any previously highlighted rects back to normal color/brightness
d3.selectAll('.highlight').transition().duration(250)
.style('fill', function(d) { return d3.rgb(d.color)})
d3.select(this).classed('highlight',true);
//now it's safe to assign the current highlighted rect a brighter hue... i think
d3.select(this).transition().duration(250)
.style('fill', function(d) { return d3.rgb(d.color).brighter(.5)})
})
Though this code does what I wanted it to do, but presumably there could only ever be 1 other highlight rect to worry about at any give time. So again, I'm not sure that using d3.selectAll() is warranted here.
So anyway, is there a more efficient way? I'd like to keep it all within one .on('click') function if possible.
If you are looking to avoid use of .selectAll, you could create a selection of one rect that contains the last clicked rectangle. Each time you click on a rectangle:
unhighlight the previously selected highlighted rect
update that selection to reflect the most recently clicked rectangle
highlight the newly selected rect
I use the variable highlightedRect to hold the selection that will allow the above workflow:
var svg = d3.select("body").append("svg")
.attr("width",600)
.attr("height",400);
var highlightedRect = d3.select(null);
var rects = svg.selectAll("rect")
.data(d3.range(1600))
.enter()
.append("rect")
.attr("y",function(d) { return Math.floor(d/50)*12; })
.attr("x",function(d) { return d%50 * 12 })
.attr("width",11)
.attr("height",11)
.attr("stroke","white")
.on("click",function(d) {
// Recolor the last clicked rect.
highlightedRect.attr("fill","black");
// Color the new one:
highlightedRect = d3.select(this);
highlightedRect.attr("fill","steelblue");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Add graph in dc.js tooltip

I'm building a number of graphs using crossfilter and dc.js. Among others, there is a row chart and an histogram (a bar chart).
What I am trying to do is to create a tooltip on the row chart which will show the histogram.
Looking at this SO-question I saw an example using d3-tip. I have made an attempt in this jsfiddle. However, I cannot see how to embed a div in the tooltip.
Any suggestion? (If using plain d3 is better, I'm ok with that.)
Snippet of code is:
function draw_row(div_id){ ...; return row_chart; }
function draw_hist(div_id){ ...; return bar_chart; }
var rate_chart = draw_row('#rate').title(function(){return'';});
dc.renderAll();
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function () {
// What to put in here???
draw_hist('#distr').render();
return "<div id='distr'>Distribution<br></div>"
});
d3.selectAll("#rate g.row")
.call(tip)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
Fun project!
Yes, as you noticed, you're not going to be able to render the chart while you're in the .html() callback - that only returns static HTML, and I don't think you can give it an element instead.
So we'll have to find a place to render after the HTML has already been generated. Luckily, d3-tip doesn't try to handle mouse events or anything like that - the code which displays the tip is right there in the code you've posted:
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
So we can wrap tip.show in a function of our own, and then render the chart into the tip once it's on the screen.
We have to watch out because mouseover will fire every time the mouse moves, and we probably don't want to replace the tip-chart until we hover over another bar. So we'll remember the id of the last bar we hovered:
var last_tip = null;
d3.selectAll("#rate g.row")
.call(tip)
.on('mouseover', function(d) {
if(d.key !== last_tip) {
tip.show(d);
draw_hist('#distr').render();
last_tip = d.key;
}
})
.on('mouseout', function(d) {
last_tip = null;
tip.hide(d);
});
Finally, d3-tip needs to know the size of the tip content in order to render in the right place. (If it accidentally renders on top of the element, this can cause horrible flickering when the mouse goes over the tip, registering mouseout on the element.)
So we'll just hard-code that, since we're hard-coding the chart size anyway. 20 extra pixels to fit the title:
.html(function (d) {
return "<div id='distr' style='min-width:300px; min-height: 320px'>Distribution<br></div>"
});
Looks pretty cool with the default translucent black style from d3-tip:
Here's the fork of your fiddle: https://jsfiddle.net/gordonwoodhull/hkx7j3r5/10/

d3.v3 transitions happening instantly?

I've recently passed from d3.v2 to d3.v3, and am trying to understand the differences in the transition mechanisms.
In the code underneath, I'm trying to make a bar graph that, when drawn, has bars that increase in height via a transition. This code works without issue in d3.v2, but in v3, the transition seems to happen "instantly" (the height is immediately set to the end value).
graph.enter()//for each bucket
.append('g')
.attr('transform',function(d,i){ return 'translate('+(xBand(i))+')';})
.attr('width',xBand.rangeBand())
.each(function(data,index){//here we are working on the selection for a single bucket
var $this=d3.select(this); //this refers to the group selection
var currentY=0;
var rects=$this.selectAll('rect')
.data(data.values);
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}));
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
});
Try setting a starting height in your enter selection:
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}))
.attr('height', 0);
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));

StopPropagation() with SVG element and G

I created an SVG element with an .on("click") behavior and appended g elements with .on("click") and thought that I could use d3.event.stopPropagation() to keep the SVG click event from firing with the g click event. Instead, both events continue to fire. So I must be placing stopPropagation in the wrong place.
svg = d3.select("#viz").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("mousedown", mousedown);
sites = svg.selectAll("g.sites")
.data(json.features)
.enter()
.append("svg:g")
.on("click", siteClick)
;
sites.append("svg:circle")
.attr('r', 5)
.attr("class", "sites")
;
function mousedown() {
console.log("mouseDown");
}
function siteClick(d, i) {
d3.event.stopPropagation();
console.log("siteClick");
}
You seem to be mixing up the click and mousedown events. Calling stopPropagation will only prevent propagation of a single event at a time, and these are separate events.
Typically, a click gesture will cause mousedown, mouseup and click events, in that order.
You can keep the click event handler on the child elements and add a mousedown event handler with a stopPropagation call, and that should achieve what you're after.
Here is an example demonstrating its use in a similar situation to yours.

Resources