D3 zoom pan stutter - d3.js

I'm experiencing 'stutter' with the D3 drag behavior.
Seems to be a similar problem to "Stuttering" drag when using d3.behavior.drag() and transform
However the solution does not seem to work for the zoom behavior.
Here is an example of the issue: (try dragging the rectangle)
http://jsfiddle.net/EMNGq/109/
blocks = [
{ x: 0, y: 0 }
];
var translate_var = [0,0];
zoom_var = d3.behavior.zoom()
.on("zoom", function(d) {
d.x = d3.event.x;
d.y = d3.event.y;
draw();
});
svg = d3.select("body")
.append("svg:svg")
.attr("width", 600)
.attr("height", 600);
function draw() {
g = svg.selectAll("g")
.data(blocks);
gEnter = g.enter().append("g")
.call(zoom_var);
g.attr("transform", function(d) { return "translate("+translate_var[0]+","+translate_var[1]+")"; });
gEnter.append("rect")
.attr("height", 100)
.attr("width", 100);
}
draw()

You zoom in or drag the element, but then translate the same element. Because the translation is relative, it results in this stuttering.
As stated in the documentation for Zoom Behavior:
This behavior automatically creates event listeners to handle zooming and panning gestures on a container element. Both mouse and touch events are supported.
Contrast it to the documentation for Drag Behavior:
This behavior automatically creates event listeners to handle drag gestures on an element. Both mouse events and touch events are supported.
Your solution is inverse to the similar question. Call your zoom function on the container.
svg = d3.select("body")
.append("svg:svg")
.attr("width", 600)
.attr("height", 600)
.call(zoom_var);
Here's the demo.
You might also be interested in the actual zoom. To do that simply add the scale to your transform rule. Here's the demo with zoom enabled.

Related

d3.js - Allow brushing and zooming on same chart

UPDATE
I finally managed to get a working solution to this challenge. Zoom incorporates event.stopPropagation(). This means that any events on elements contained within a parent element will not "bubble up" to the parent element, as per:
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture
This is how I understand it working and what I have rationalised to get my solution functional:
For brushing, the event will propagate up to ancestor elements. In my case, I have appended a 'rect' element to the svg, as per the examples in the docs, and attached the zoom behaviour to the rect. Therefore, the element with the zoom action is not a parent of the element with the brush action.
I changed the code by instead attaching the zoom behaviour to the svg (rather than the rect) so that it is an ancestor of the group element with the brush action, this will allow the action on the g element to bubble up to the svg. Now zoom can also fire as the event will bubble up from the g element.
I then put a check to see if control was clicked or not, and depending on the outcome I ran the appropriate zoom or brush function. I guess another method may be to check the source event fired, and then run only if the event was the one that we want to trigger either the zoom or brush action.
Good day,
I am having an issue where the zoom action is consuming the events on a line chart. I have seen a number of examples using context and focus, and zoom and drag with discrete elements, but none with allowing zooming and brushing on the same line/area chart.
Objective: To allow a user to either hold down ctrlKey or shiftKey to enable panning, and without the key being held down the chart will pan (using d3-zoom).
Problem: Zooming is consuming the events, as per the d3.js documentation. This prevents the brushing from ever being triggered. This means that even if I use a filter and allow the ctrlKey to be used, and then use an if-block to determine if ctrlKey has been pressed or not, the event will still never get passed the zoom handler to the brush.
I noted in early versions of d3 (seems prior to v4) this co-existing of events was not an issue for my use case, but seemed to have caused problems with multiple events being fired simultaneously for other use-cases, which caused problems. To resolve, it seems that preventDefault and stopPropagation have been implemented on the d3-zoom behaviour. This prevents allowing the event to propagate to the brush handler and then using custom logic to either zoom (in my case just pan) or brush.
Question: Does anyone know any means to allow the events to propagate without changing the d3.js code itself? I would really like be able to pan or zoom depending on whether ctrlKey is pressed, without using a toggle button to disable pointer events.
Code:
const MARGIN = { TOP: 10, BOTTOM: 80, LEFT: 120, RIGHT: 10 }
const WIDTH = 1000 - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = 500 - MARGIN.TOP - MARGIN.BOTTOM;
svg = d3.select(element.current)
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
graph = svg.append("g")
.attr("transform", `translate(${MARGIN.LEFT-50}, ${MARGIN.TOP})`)
.attr("class", "graph")
svg.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", WIDTH )
.attr("height", HEIGHT );
x = d3.scaleTime()
.range([0, WIDTH]);
y = d3.scaleLinear()
.range([HEIGHT , 0]);
panBehaviour = d3.zoom()
.extent([[0, 0], [WIDTH, HEIGHT]])
.translateExtent([[0, 0], [WIDTH, HEIGHT]])
.scaleExtent([1, 1])
.filter( ()=>!d3.event.button)
.on('zoom', panPlot)
pan = svg.append('rect')
.attr('class', 'pan')
.attr("transform", `translate(${MARGIN.LEFT-50}, ${MARGIN.TOP})`)
.attr("width", WIDTH )
.attr("height", HEIGHT )
.call(panBehaviour);
brush = d3.brushX()
.extent([ [0, 0], [WIDTH, HEIGHT] ])
.on("end", zoom)
graph.append('g')
.attr('class', 'brush')
.call(brush)
path = graph.append('path');
path.attr("clip-path", "url(#clip)");
// The panning function replaced with a log statement
panPlot() {
if (d3.event.sourceEvent.ctrlKey){
console.log("PANNING");
}
};
// The zoom function replaced with a log statement
zoom(){
if (!d3.event.sourceEvent.ctrlKey){
console.log("ZOOMING");
}
};
A very similar question was asked here, but with no solution. D3 v4 Brush and Zoom on the same element (without mouse events conflicting)
You can disable/enable pan and brush behaviors each time ctrl key is pressed/released.
These two behaviors can't coexist, so you have to disable one.
The answer is based on d3 v5.
const noZoom = d3.zoom();
const doc = d3.select(document);
doc.on('keydown', () => {
if (d3.event.key === "Control") {
pan.call(noZoom);
}
})
doc.on('keyup', () => {
if (d3.event.key === "Control") {
pan.call(panBehaviour);
}
})

Capturing mouseover events on two overlapping elements

So I have a d3 chart with a rect overlay to hold crosshair elements on mouseover events. Under the overlay I have other rects displaying data that have mouseover event handlers also, but The overlay is blocking mouseover events form triggeron the children rects below.
let chartWindow = svg
.append("g");
/* this holds axis groups, and cadlestick group*/
let candleStickWindow = chartWindow.append("g")
//this event never fires
.on('mousemove', ()=>console.log('mouse move'));
let candlesCrosshairWindow = chartWindow
.append("rect")
.attr("class", "overlay")
.attr("height", innerHeight)
.attr("width", innerWidth)
.on("mouseover", function() {
crosshair.style("display", null);
})
.on("mouseout", function() {
crosshair.style("display", "none");
removeAllAxisAnnotations();
})
.on("mousemove", mousemove);
The CrosshairWindow has CSS property pointer-events: all. If I remove that, I get my events to fire on the candleStickWindow but not the CrosshairWindow. How can I get mouse events onto both of the elements??
Thanks for any help!
Update
I changed the crosshair rect element to be on the bottom and it kinda works, the candlestick bars mouseover event works but it blocks the crosshair from working.
One solution that comes to mind might use event bubbling which, however, only works if the events can bubble up along the same DOM sub-tree. If, in your DOM structure, the crosshairs rectangle and the other elements do not share a common ancestor to which you could reasonably attach such listener, you need to either rethink your DOM or resort to some other solution. For this answer I will lay out an alternative approach which is more generally applicable.
You can position your full-size rect at the very bottom of your SVG and have its pointer-events set to all. That way you can easily attach a mousemove handler to it to control your crosshairs' movements spanning the entire viewport. As you have noticed yourself, however, this does not work if there are elements above which have listeners for that particular event type attached to them. Because in that case, once the event has reached its target, there is no way propagating it further to the underlying rectangle for handling the crosshairs component. The work-around is easy, though, since you can clone the event and dispatch that new one directly to your rectangle.
Cloning the event is done by using the MouseEvent() constructor passing in the event's details from the d3.event reference:
new MouseEvent(d3.event.type, d3.event)
You can then dispatch the newly created event object to your crosshairs rect element by using the .dispatchEvent() method of the EventTarget interface which is implemented by SVGRectElement:
.dispatchEvent(new MouseEvent(d3.event.type, d3.event));
For lack of a full example in your question I set up a working demo myself illustrating the approach. You can drag around the blue circle which is a boiled down version of your crosshairs component. Notice, how the circle can be seamlessly moved around even when under the orange rectangles. To demonstrate the event handlers attached to those small rectangles they will transition to green and back to orange when entering or leaving them with the mouse pointer.
const width = 500;
const height = 500;
const radius = 10;
const orange = d3.hsl("orange");
const steelblue = d3.hsl("steelblue");
const limegreen = d3.hsl("limegreen");
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
const target = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("mousemove", () => {
circle.attr("cx", d3.event.clientX - radius);
circle.attr("cy", d3.event.clientY - radius);
});
const circle = svg.append("circle")
.attr("r", radius)
.attr("fill", steelblue)
.attr("pointer-events", "none");
const rect = svg.selectAll(null)
.data(d3.range(3).map(d => [Math.random() * width, Math.random() * height]))
.enter().append("rect")
.attr("x", d => d[0])
.attr("y", d => d[1])
.attr("width", 50)
.attr("height", 50)
.attr("fill", orange)
.attr("opacity", 0.5)
.on("mouseover", function() {
d3.select(this).transition().attr("fill", limegreen);
})
.on("mousemove", function() {
target.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event));
})
.on("mouseout", function() {
d3.select(this).transition().attr("fill", orange);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

D3.zoom jumps when using mouse wheel after programmatically zoom

When I zoom to a specific location on a mouse click and then try to pan or when I'm using the mouse wheel, the zoom behavior jumps. It seems that my zoom level is being restored like it was before the mouse click.
Here is my event handler:
function click(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
svgContainer.transition()
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")");
}
And this is how I "activate" my zoom and pan functionalities.
var svgContainer = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function () {
svgContainer.attr("transform", d3.event.transform)
}))
.append("g")
...
svgContainer.selectAll(null)
.data(feat.features)
.enter()
.append("path")
.on("click", click)
...
Background
You are manipulating the transform applied to your elements in the click function but not updating the zoom state to reflect this. d3.zoom does not track an element's transform at all. So when you modify the transform attribute of an element independently of d3.zoom, d3.zoom no longer "knows" what the transform is of that element - it remains unchanged. D3.zoom does track zoom state - internally and independently of any element's transform attribute.
It might seem odd that d3.zoom doesn't track an element's transform, but there is good reason. d3.zoom isn't always used to manipulate the transform of an element, it may alter something like element width or a scale while that element's transform remains unchanged. Here's a bl.ock of mine where d3.zoom here manipulates only the radius of circles on canvas.
Problem
As you don't update the zoom state in your click event, d3.zoom picks up where it was last left when applying a new zoom event, which explains your symptom: "It seems that my zoom level being restored like it was before the mouse click."
Solution
So, what do you need to do? You need to pass your zoom transform to d3.zoom and trigger a zoom event. This way d3.zoom is apprised of the current zoom state. Luckily there is a method for this. We take a d3.zoomIdentity (k=1,x=0,y=0) and translate and scale as appropriate, we can translate, scale, and then translate again as you have done too:
// Create a zoom transform from d3.zoomIdentity
var transform = d3.zoomIdentity
.translate(250,150)
.scale(k)
.translate(-x,-y);
And then we apply the zoom by calling zoom.transform which according to the docs:
sets the current zoom transform of the selected elements to the
specified transform, instantaneously emitting start, zoom and end
events. If selection is a transition, defines a “zoom” tween to the
specified transform using d3.interpolateZoom, emitting a start event
when the transition starts, zoom events for each tick of the
transition, and then an end event when the transition ends (or is
interrupted). (link)
We can call zoom.transform with:
// Apply the zoom and trigger a zoom event with a provided zoom transform:
svg.call(zoom.transform, transform);
So if this is analagous to what you have:
var zoom = d3.zoom()
.scaleExtent([1,8])
.translateExtent([[0,0],[500,300]])
.on("zoom",zoomed);
var svg = d3.select("div")
.append("svg")
.attr("width",500)
.attr("height",300)
.call(zoom);
var g = svg.append("g");
var rects = g.selectAll(null)
.data(d3.range(750))
.enter()
.append("rect")
.attr("width",17)
.attr("height",17)
.attr("fill","#eee")
.attr("y", function(d) { return Math.floor(d/50) * 20; })
.attr("x", function(d) { return d%50 * 20; })
.on("click", click);
function zoomed() {
g.attr("transform", d3.event.transform);
}
function click(d) {
rects.attr("fill","#eee");
var clicked = d3.select(this);
clicked.attr("fill","orange");
var x = +clicked.attr("x")+10;
var y = +clicked.attr("y")+10;
var k = 5;
var transform = "translate(" + 250 + "," + 150 + ")scale(" + k + ")translate(" + -x + "," + -y + ")";
g.transition()
.attr("transform",transform)
.duration(1000);
}
rect {
stroke-width: 1px;
stroke: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>
This can be analagous to your solution:
var zoom = d3.zoom()
.scaleExtent([1,8])
.translateExtent([[0,0],[500,300]])
.on("zoom",zoomed);
var svg = d3.select("div")
.append("svg")
.attr("width",500)
.attr("height",300)
.call(zoom);
var g = svg.append("g");
var rects = g.selectAll(null)
.data(d3.range(750))
.enter()
.append("rect")
.attr("width",17)
.attr("height",17)
.attr("fill","#eee")
.attr("y", function(d) { return Math.floor(d/50) * 20; })
.attr("x", function(d) { return d%50 * 20; })
.on("click", click);
function zoomed() {
g.attr("transform", d3.event.transform);
}
function click(d) {
rects.attr("fill","#eee");
var clicked = d3.select(this);
clicked.attr("fill","orange");
var x = +clicked.attr("x")+10;
var y = +clicked.attr("y")+10;
var k = 5;
// Create a zoom transform from d3.zoomIdentity
var transform = d3.zoomIdentity
.translate(250,150)
.scale(k)
.translate(-x,-y);
// Apply the zoom and trigger a zoom event:
svg.call(zoom.transform, transform);
}
rect {
stroke-width: 1px;
stroke: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>

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>

Is it possible to eliminate lag in rendering the mouse location for d3?

I have some code that displays an SVG item above the cursor location. I've noticed that the object lags behind the cursor by more than I'd expect if it were just a frame behind.
var width = 600,
height = 200;
var mouseLocation = [0,0];
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var cursor = svg.append('circle')
.attr("r", 2)
.attr("fill", 'darkblue');
// update mouse location
d3.select(window).on("mousemove", () => {
mouseLocation = d3.mouse(svg.node());
});
// show mouse
d3.timer(function() {
cursor.attr("transform", 'translate(' + mouseLocation + ')');
});
<script src="http://d3js.org/d3.v4.min.js"></script>
You can see the delay by smoothly sliding the mouse from one side to the other. The blue circle lags behind the cursor by a bit. (At least on Chrome 57 on Windows)
I've tried a couple variations including calling circle.attr within the mousemove callback function. But there's always a bit of a delay.
Edit Things I've tried unsuccessfully:
setting the cursor transform in the mousemove event
Checking the elapsed time since the previous mousemove call to only update it every 16 ms.
svg.style("shape-rendering", "optimizeSpeed");
Using the mousemove event for the svg rather than the window
Using the d3.event.pageX and d3.event.pageY rather than d3.mouse(svg.node())
I get the same issue using <canvas> demo
In the first version of this answer (look at the edit history) I tried d3.interval and setInterval, with no difference.
However, I believe that, maybe, this question has no answer (in the sense of what OP wants), because the lag depends on D3 handling the mousemove event. It's also possible that the issue here is not even D3 related, but depending on how the user agent (browser) controls the mousemove event frequency.
This can be seen if we put the code for moving the circle inside the very mousemove function:
d3.select(window).on("mousemove", () => {
mouseLocation = d3.mouse(svg.node());
cursor.attr("transform", 'translate(' + mouseLocation + ')');
});
And the lag is still the same:
var width = 600,
height = 200;
var mouseLocation = [0,0];
var svg = d3.select("div").append("svg")
.attr("width", width)
.attr("height", height);
var cursor = svg.append('circle')
.attr("r", 2)
.attr("fill", 'darkblue');
// update mouse location
d3.select("div").on("mousemove", () => {
cursor.attr("transform", 'translate(' + (d3.event.pageX - 16) + "," + (d3.event.pageY - 16) + ')');
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<div></div>

Resources