D3 transition end events not firing correctly? - d3.js

This should be straightforward: I just want to start a D3 transition running when the user clicks on a button.
However, the D3 end event does not seem to be firing correctly on my transitions: I've asked for a transition to rotate(120), but the end event seems to be firing when the rotate attribute is only 45.
This is my code:
var angle = 0;
// handle click event
d3.selectAll('.mycolour').on('click', function() {
d3.select("#loadingicon").remove();
// redraw the icon (excluded here for brevity)...
// setting the transform attribute to 0 explicitly..
group2.attr("transform", "rotate(0,0,0)");
// and start it turning:
angle = 0;
function rotateLoadingIcon() {
angle = angle%360;
angle += 120;
console.log('rotateLoadingIcon', angle, group2.attr("transform"));
d3.select(".icon").transition().ease("linear")
.duration(4000)
.attr("transform", "rotate(" + angle + ",0,0)")
.each("end", function() {
angle += 120;
console.log('innerRotateLoadingIcon', angle, group2.attr("transform"));
group2.transition().ease("linear")
.duration(4000)
.attr("transform", "rotate(" + angle + ",0,0)")
.each("end", function() {
rotateLoadingIcon();
});
});
}
console.log('about to start icon loading');
rotateLoadingIcon();
});
It works the first time the icon is drawn:
rotateLoadingIcon 120 rotate(0,0,0)
innerRotateLoadingIcon 240 rotate(119.99999999999999)
But after second or subsequent clicks, I've seen it produce console output like this:
about to start icon loading
rotateLoadingIcon 120 rotate(0,0,0)
innerRotateLoadingIcon 240 rotate(45.14999999999999)
In practice what this means is that the direction of rotation changes :(
Why has innerRotateLoadingIcon started, when the rotate attribute is only set to 45.1...? Surely given the code, it shouldn't start until rotate reaches 120 - as in the first time the code is run.
I'm wondering if the way I have set up the JavaScript means that two different versions of rotateLoadingIcon could be running at the same time. Is this possible, and can I fix it if so?
Update: Here's a JSFiddle that demonstrates the problem, though I'm now trying setInterval rather than end - click twice and you'll see the rotation change direction, I can't figure out why: http://jsfiddle.net/QtBvJ/

In the version in your jsfiddle, you only need to clear the timeout properly to make it work. The function setTimeout returns a reference that you need to pass to window.clearTimeout, i.e.
var t = null;
...
window.clearTimeout(t);
...
t = setTimeout(...);
Working fiddle here.

Related

How to reset/remove zoom transform stored in an element in d3 v4?

I am trying to implement both zooming and brushing in my d3 (v4) chart.
I have got them both working separately, but my problem comes when I try to implement both features on the same chart.
My scenario is the following:
1. The user uses the brush to show a specific region of the chart.
2. They then zoom/pan, but this causes the view to jump back to the old location, because the stored zoom transform is not aware of the changes made by the brushing.
My understanding is that the current zoom transform (scale+translation) is stored inside the DOM element in an internal __zoom attribute. The zoom plugin automatically adjusts this whenever you interact with the element (e.g. by scrolling the mouse wheel).
I see that you can use d3.zoomTransform to get the current zoom transform for an element.
How can I reset/remove the stored zoom transform (e.g. after panning, so that any subsequent zooming carries on from where the brushing left off)?
Note: I don't want to have to change the zoom, but rather just update the stored zoom transform to treat that new scale as the "identity". This is important because I want to be able to smoothly transition from one scale to another when brushing etc.
The way I got around this in the end is:
in the zoom handler, use transform.rescaleX() to get a new transformed scale
Update the main scale's domain based on the transformed scale
Update the x-axis based on the scale
Reset the transform on the element to d3.zoomIdentity.
The key thing here is that after the scale has been updated, the stored transform on the DOM element is always put back to identity (i.e. scale=1, translate=0,0).
That means that we don't need to worry about brushing/zooming or any programatic changes to the scale on different elements won't conflict or have different values from each other. We effectively just keep applying very small scale factors to the element.
In terms of a code example, here are the relevant parts from my working chart:
// class contains:
// this.xScale - stored scale for x-axis
// this.xAxis - a d3 Axis
// this.xAxisElement - a d3 selection for the element on which the x-axis is drawn
// this.zoomX - a d3 ZoomBehavior
// this.chartElement - a d3 selection for the element on which the zooming is added
protected setupZooming(): void {
this.zoomX = d3.zoom().on('zoom', () => { this.onZoomX(); });
this.zoomXElement = this.xAxisElement
.append('rect')
.attr('fill', 'none')
.style('pointer-events', 'all')
.attr('width', this.width)
.attr('height', this.margin.bottom)
.call(this.zoomX);
}
onZoomX(): void {
const transform: d3.ZoomTransform = d3.event.transform;
if (transform.k === 1 && transform.x === 0 && transform.y === 0) {
return;
}
const transformedXScale = transform.rescaleX<any>(this.xScale);
const from = transformedXScale.domain()[0];
const to = transformedXScale.domain()[1];
this.zoomXTo(from, to, false);
this.chartElement.call(this.zoomX.transform, d3.zoomIdentity);
}
zoomXTo(x0: Date, x1: Date, animate: boolean): void {
const transitionSpeed = animate ? 750 : 0;
this.xScale.domain([x0, x1]);
this.xAxisElement.transition().duration(transitionSpeed).call(this.xAxis);
this.updateData(transitionSpeed);
}
updateData(transitionSpeed: number): void {
// ...
}
Apologies if this extract isn't easy to follow outside of the context of the rest of my code, but hopefully it is still helpful.

set zoom after each redraw

I am using 'dagre d3' for displaying dependency graph. I also use slider(which I use to display step by step evolution of graph when user slides the slider) so each time Slider is moved, graph is redrawn. For the first time, 'zoom' feature will be set like below:
var zoom = d3.behavior.zoom().on("zoom", function () {
inner.attr("transform", "translate(" + d3.event.translate + ")" +
"scale(" + d3.event.scale + ")");
});
svg.call(zoom);
I want to retain the zoom level if the user has zoomed in or zoomed out the graph and then moved the slider. So make it work, I did the following.
this method will be called whenever user slides the slider.
function redrawGraphToStage(stageToRedraw) {
// Not sure how to get previous values. below one works but feel its bit hacky.
if(inner[0][0].transform.animVal.length){
tempInnerTranslate = [inner[0][0].transform.animVal[0].matrix.e, inner[0][0].transform.animVal[0].matrix.f];
tempInnerScale = inner[0][0].transform.animVal[1].matrix.a;
}
// do modifications to graph. and re-render.
if(tempInnerScale || tempInnerTranslate){
// this will set up the zoom level to before.
zoom.translate(tempInnerTranslate).scaleExtent(tempInnerScale, tempInnerScale).event(svg);
}
Even though it setup the zoom level to previous values, I wont be able to zoom after this. Any suggestions would be great.
Try with,
var zoom = d3.behavior.zoom().on("zoom", zoomed);
svg.call(zoom);
function zoomed() {
inner.attr("transform",
"translate(" + zoom.translate() + ")" +
"scale(" + zoom.scale() + ")"
);
}
function redrawGraphToStage(stageToRedraw) {
// Not sure how to get previous values. below one works but feel its bit hacky.
if(inner[0][0].transform.animVal.length){
tempInnerTranslate = [inner[0][0].transform.animVal[0].matrix.e, inner[0][0].transform.animVal[0].matrix.f];
tempInnerScale = inner[0][0].transform.animVal[1].matrix.a;
}
// do modifications to graph. and re-render.
if(tempInnerScale || tempInnerTranslate){
// this will set up the zoom level to before.
zoom.translate(tempInnerTranslate).scale(tempInnerScale, tempInnerScale);
zoomed();
}
I'm not sure what you're using the slider for, but I made you a demo where you can control the zoom scale with a slider bar. Significant change: scaleIntent replaced with scale.
JSFiddle working demo

How to know if user is zooming and in which direction using d3 behavior zoom?

I'm developing an application which use d3 for charting. x axis is time scale. User can zoom in/out to expand or shrink x axis. Now I want to add some logic in the event handler based on the direction user is zooming. But I don't find an easy way to know if user is doing zoom and in which direction.
Can someone share some experience here?
You want to add an event handler to the zoom.
To run a function whenever the zoom changes, you can add an event handler for zoom as follows:
zoom.on("zoom", function () {
var scale = d3.event.scale;
var translate = d3.event.translate;
// Do your processing
});
If you want to tell the direction of the zoom, you can save the translate each time, and compare the current one to the previous.
Direction of zoom : Zooming in or zooming out
In version 4 of d3, ZoomEvent contains WheelEvent as sourceEvent property. So to have the direction i'am doing :
zoom.on("zoom", () => {
-1 * Math.sign(d3.event.sourceEvent.deltaY)
});
d3.event.sourceEvent.deltaY gives nigative values when you zooming in, for me i use negative values when i zoom out that's why i'am multipling by -1.
The value of d3.event.deltaY works only in wheelDelta method of the zoom (see code below) but it gives mes values in v3 when i called it in the handler of zoom.
d3
.zoom()
.wheelDelta((ev, i, g) => {
return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1) / 500;
})
.on('zoom', () => {
// ...
});

d3 - Axis Label Transition

In Mike Bostock's cubism demo (http://bost.ocks.org/mike/cubism/intro/demo-stocks.html), there is a cursor which displays the values of all horizon charts on display. Furthermore, the cursor text shows the time axis point in time. As the cursor text obscures an axis label, the label fades.
I am working on a similar display with d3.js (but not cubism). I have all working except that fade portion. I have searched through the CSS in the developer's window, searched the source code (as best I could), but I don't understand what manner of magic is being used to accomplish this feat. I've even looked through SO "axis label transition" questions, but I have failed to connect the dots on xaxis label transitions.
How does that fade in/out when obscured by text happen?
UPDATE:
I think I located the event script area where this happens - its just a little over my head at the moment - can anyone help me decipher what this event listener is doing? Specifically, in the second g.selectAll in the else clause below - what data (d) is being used here? What is causing this event to fire?
This is the coolest part of the display (outside of the horizon charts), I would love to figure this out ...
context.on("focus.axis-" + id, function(i) {
if (tick) {
if (i == null) {
tick.style("display", "none");
g.selectAll("text").style("fill-opacity", null);
} else {
tick.style("display", null).attr("x", i).text(format(scale.invert(i)));
var dx = tick.node().getComputedTextLength() + 6;
g.selectAll("text").style("fill-opacity", function(d) { return Math.abs(scale(d) - i) < dx ? 0 : 1; });
}
}
});
I used this as reference to accomplish the same effect.
I'm not sure what the context variable is or how the id's are set or what the tick flag references but what I did was simply update the opacity of the ticks according to their proximity to the mouse. With this, the vertical tick fades as well as the label text.
svg.selectAll('.x.axis .tick').style('opacity', function (d) {
return Math.min(1, (Math.round(Math.abs(d3.mouse(svg.node())[0] - x(d))) - 10) / 15.0);
});
This way, the opacity is set to 0 if it's within 10 pixels, and fades from 1-0 between 10 and 25. Above 25, the opacity would be set to an increasingly large number, so I clamp it to 1.0 using the Math.min function.
My labels are slightly rotated, so I also added an offset not shown inside the formula above (a +3 after [0]) just to make it look a bit nicer. A year late to answer your only question, but hey it's a nice effect.
same answer as Kevin Branigan's post, but using the d3 scale to calculate the opacity value.
var tickFadeScale = d3.scale.linear().domain([10,15]).range([0,1]).clamp(true);
svg.selectAll('.x.axis .tick').style('opacity', function (d) {
return tickFadeScale(Math.abs(d3.mouse(svg.node())[0] - x(d)));
}

jQuery Mousemove: trigger on change of 5px

For a number of technical reasons, I'm implementing my own 'draggable' feature on jQuery, rather than using jQuery UI, and I'm using mousedown and mousemove events to listen for the user trying to drag an element.
It works good so far, I would just like to fire up the mousemove event every 5 px of movement, rather than pixel by pixel. I've tried coding a simple:
$('#element').bind('mousemove', function(e) {
if(e.pageX % 5 == 0) {
// Do something
}
});
However, the movement is not stable every 5 pixels, and sometimes if you move the mouse too fast, it will skip several steps. I think this is because when moving the mouse very fast, jQuery will not trigger the event every pixel.
Do you guys know how to trigger the event every 5 pixels?
Thanks a lot,
Antonio
Your code does not take into account where your drag started. e.pageX will just give you page coordinates, not differences. You need to check for the change in distance moved.
This post is pretty relevant.
Here is the basic code:
$(document).mousemove(function(event) {
var startingTop = 10,
startingLeft = 22,
math = Math.round(Math.sqrt(Math.pow(startingTop - event.clientY, 2) +
Math.pow(startingLeft - event.clientX, 2))) + 'px';
$('span').text('From your starting point(22x10) you moved: ' + math);
});
EDIT: Now I think I understand what the OP is talking about. I used the above code to come up with this fiddle. It tracks your current position in relation to the Top Left of the screen and it checks to see if your difference is > 5 pixles.
New script:
var oldMath = 0;
$(document).mousemove(function(event) {
var startingTop = 10,
startingLeft = 22,
math = Math.round(Math.sqrt(Math.pow(startingTop - event.clientY, 2) +Math.pow(startingLeft - event.clientX, 2))) + 'px';
$('#currentPos').text('you are at :' + math);
if(Math.abs(parseInt(math) - oldMath) > 5){
//you have moved 5 pixles, put your stuff in here
$('#logPos').append('5');
//keep track of your position to compare against next time
oldMath = parseInt(math);
}
});​

Resources