I'm trying to find a way to reset the zoom property of my svg whenever I click on an object and leave it as is. For example in this jsfiddle if you zoom out and click on a square, the zoom gets reset but then when I try to pan the screen the zoom goes back to what it was before I clicked on the square. Is there a way such that when I click on a square, the zoom goes back to 100% and stays at 100% even when panning?
JSFiddle: http://jsfiddle.net/nrabinowitz/p3m8A/
Here is my zoom call:
svg.call(d3.behavior.zoom().on("zoom", redraw));
The key is to tell the zoom behaviour that you're resetting scale and translation. To achieve this, simply save it in a variable and set the values appropriately as you're setting the transform attribute of the SVG.
var zoom = d3.behavior.zoom();
svg.call(zoom.on("zoom", redraw));
// ...
.on("click", function () {
counter++;
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
zoom.scale(1);
zoom.translate([offset * counter, 0]);
drawBox(x, y);
});
Updated jsfiddle here.
Related
When I use d3-zoom and programatically call the scaleTo function using zoomIdentity I cannot zoom using the mouse wheel anymore.
How do I fix this issue?
https://observablehq.com/d/8a5dfbc7d858a16b
// mouse wheel zoom not working because use of zoomIdentity
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const zoom = d3Zoom.zoom()
svg.call(zoom);
const zoomArea = svg.append('g');
zoom.on('zoom', (e) => {
zoomArea.attr("transform", e.transform)
})
zoomArea.append('circle')
.attr("cx", width/2)
.attr("cy", height/2)
.attr("r", 20)
zoom.scaleTo(svg, d3Zoom.zoomIdentity)
return svg.node();
}
The second parameter of zoom.scaleTo(svg, d3Zoom.zoomIdentity) accepts a k scaling factor (e.g., 2 for 2x zoom). The method zoom.scaleTo is intended to be used when you want to set the zoom level, but not the translation (x and y positions).
If you want to set the whole transform to the zoom identity (which resets both the zoom level and the x and y positions), the method is zoom.transform(svg, d3Zoom.zoomIdentity).
If you indeed just want to reset the scale, you can use zoom.scaleTo(svg, d3Zoom.zoomIdentity.k), or simply zoom.scaleTo(svg, 1).
I'm hoping to configure the zoom behavior of a plot to have three kinds of interaction:
It should be possible to pan from left to right with the scroll wheel.
It should be possible to pan from left to right with mousedown drag events.
it should be possible to zoom in 1-D with the pinch event (i.e. scroll wheel with control key pressed).
Right now, I can get the latter two to work in this Fiddle: https://jsfiddle.net/s1t7mrpw/6/
The zoomed function looks like this:
function zoomed() {
if (d3.event.sourceEvent.ctrlKey || d3.event.sourceEvent.type === 'mousemove') {
view.attr("transform", d3.event.transform);
centerline.call(xAxis.scale(d3.event.transform.rescaleX(x)));
d3.event.transform.y = 0;
g.attr("transform", d3.event.transform);
} else {
current_transform = d3.zoomTransform(view);
current_transform.x = current_transform.x - d3.event.sourceEvent.deltaY;
// what do I do here to pan the axis and update `view`'s transform?
// centerline.call(xAxis.scale(d3.event.transform.rescaleX(x))); ?
// view.attr('transform', current_transform); ?
g.attr('transform', current_transform);
}
}
It uses the rescale nomenclature from these blocks:
https://bl.ocks.org/mbostock/db6b4335bf1662b413e7968910104f0f
https://bl.ocks.org/rutgerhofste/5bd5b06f7817f0ff3ba1daa64dee629d
And it uses d3-xyzoom for independent scaling in the x and y directions.
https://bl.ocks.org/etiennecrb/863a08b5be3eafe7f1d61c85d724e6c4
But I can't figure out how to pan the axis in the else bracket, when the user is just scrolling (without pressing the control key).
I had previously used a separate trigger for wheel.zoom but then I need to handle the control key in that other function as well.
I basically want the default zoom behaviors for mousemove and when the control key is pressed, but panning rather than zooming on scroll when the control key is not pressed.
I am using d3-zoom on this answer as it is more common and part of the d3 bundle. d3-xyzoom, as an add-on module, adds some useful functionality using d3-zoom as a base, so this solution should work with xyzoom with minor revisions - but I don't believe its use is necessary
The key challenge lies in using the wheel to apply a translate change not a scale change. Roughly speaking, when a scroll event without control occurs you grab the transform on the selection (this should be the node) and update it by taking the scroll and creating an x offset from it to translate the view rectangle. But, you don't update the zoomTransform to nullify the change in scale caused by the zoom - so when you pan by dragging, the rectangle changes size because you haven't updated the zoomTransform used by the zoom behavior. Likewise, the translate can be off if it doesn't account for scale.
The disassociation between zoom transform applied to an element as a "transform" attribute and the zoom transform tracked by the zoom behavior is the cause of many headaches on SO - but it allows freer implementation options as one not need use an element's transform attribute to apply a zoom behavior (useful in canvas, zooming unorthodox things like color, etc).
I'm going to propose a basic solution here.
First, the zoom transform tracked by the zoom behavior can be updated by modifying the d3.event.transform object itself (or with d3.zoomTransform(selection.node) where selection is the selection that the zoom was called on originally).
Secondly, as the d3.event.transform object registers scrolling as scaling, we need to override this and convert this scaling into translating - when needed. To do so, we need to keep track of the current (pre-event) translate and scale - so we can modify them manually:
// Keep track of zoom state:
var k = 1;
var tx = 0;
function zoomed() {
var t = d3.event.transform;
// If control is not pressed and a wheel was turned, set the scale to last known scale, and modify transform.x by some amount:
if(!d3.event.sourceEvent.ctrlKey && d3.event.sourceEvent.type == "wheel") {
t.k > k ? tx += 40/k : tx -= 40/k;
t.k = k;
t.x = tx;
}
// otherwise, proceed as normal, but track current k and tx:
else {
k = t.k;
tx = t.x;
}
// Apply the transform:
view.attr("transform", "translate("+[t.x,0]+")scale("+[t.k,1]+")");
axisG.call(xAxis.scale(t.rescaleX(xScale)));
}
Above, I modify the transform t.k and t.k for events where wheeling/scrolling should translate. By comparing t.k with the stored k value I can determine direction: would it normally be a zoom out or zoom in event, and then I can convert this into a change in the current translate value. By setting t.k back to the original k value, I can stop the change in scaling that would otherwise occur.
For other zoom events it is business as usual, except, I store the t.x and t.k values for later, for when wheel events should translate rather than scale.
Note: I've set the y scaling and translate in the transform to a hard coded zero, so we don't need to worry about the y component at all.
var width = 500;
var height = 120;
// Keep track of zoom state:
var k = 1;
var tx = 0;
// Some text to show the transform parameters:
var p = d3.select("body")
.append("p");
// Nothing unordinary here:
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var xScale = d3.scaleLinear()
.domain([0,100])
.range([20,width-20])
var xAxis = d3.axisTop(xScale);
var axisG = svg.append("g")
.attr("transform","translate(0,50)")
.call(xAxis);
var view = svg.append("rect")
.attr("width", 20)
.attr("height",20)
.attr("x", width/2)
.attr("y", 60)
var zoom = d3.zoom()
.on("zoom",zoomed);
svg.call(zoom)
function zoomed() {
var t = d3.event.transform;
// If control is not pressed and a wheel was turned, set the scale to last known scale, and modify transform.x by some amount:
if(!d3.event.sourceEvent.ctrlKey && d3.event.sourceEvent.type == "wheel") {
t.k > k ? tx += 40/k : tx -= 40/k;
t.k = k;
t.x = tx;
}
// otherwise, proceed as normal, but track current k and tx:
else {
k = t.k;
tx = t.x;
}
// Apply the transform:
view.attr("transform", "translate("+[t.x,0]+")scale("+[t.k,1]+")");
axisG.call(xAxis.scale(t.rescaleX(xScale)));
// Show current zoom transform:
p.text("transform.k:" + t.k + ", transform.x: " + t.x);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.6.0/d3.min.js"></script>
I have a D3.js draw in a svg and I want to be able to pan and zoom on it. I also want a rectangle to be drawn around the initial window. I want the rectangle to become smaller as I zoom out, larger as I zoom in and to move normally as I pan around. Usual stuff.
I also want to be able to redraw the rectangle to make it fit again the actual window (so it will be smaller than the first if I zoomed in between the redraw for example). I didn't included this part in the code example but it explains why I need a way to get the zoom properties.
To properly trace the rectangle I found the zoom.translate() and zoom.scale() property, supposed to give me the parameters I need to calculate the coordinates of the rectangle's parts. However since I added this part to my code the panning sensibility became to shift as I zoom in and out: The more I zoom in, the less sensible is the panning, and the more I zoom out the more sensible it becomes.
In my mind, zoom.translate() and zoom.scale() were only supposed to fetch the parameters, not to change the way zooming and panning work, how can I fix that?
I also have inexplicably a rectangle that doesn't fit the window: it is a bit larger and shorter.
Here my piece of code:
var svg;
var map;
var zoom;
var graph = d3.select("#graph");
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(zoom);
map = svg.append("g");
Here is the acquisition of the rectangle's coordinates:
var w_b = {
x_min: (0 - zoom.translate()[0])/zoom.scale(),
x_max: (svg.attr("width") - zoom.translate()[0])/zoom.scale(),
y_min: (0 - zoom.translate()[1])/zoom.scale(),
y_max: (svg.attr("height") - zoom.translate()[1])/zoom.scale()
And here is the drawing of the rectangle:
map.selectAll("line").remove();
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_min'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_max']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_min']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_max']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
As a bonus question, this doesn't work at all on Chrome, any idea about what it could be ?
To work correctly, the zoom must be defined like this:
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
map.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
Notice the "map.attr" instead of "svg.attr".
However it doesn't solve the issues about the size of the window and the fact it's not working on Chrome.
Want to add a circle on the links between nodes on click and I should be able to attach a drag event to the circle so that when I drag a circle, the link should move to . where I am going wrong in this?
var dragCircle = d3.behavior.drag()
.on('dragstart', function(){
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(d,i){
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
});
//I want to attach circle to the link so that when I drag circle, line should move too.
function drawCircle(x, y, size) {
svg.selectAll(".edge").append("circle")
.attr('class', 'linkcirc')
.attr("cx", x)
.attr("cy", y)
.attr("r", size)
.style("cursor", "pointer")
.call(dragCircle);
}
//catching the mouse position to decide to place the circle
edge.on("click",function() {
var coords = d3.mouse(this);
drawCircle(coords[0], coords[1],3);
});
SVG will not allow you to create a circle as a child of a line (and your code is creating one circle for every link on every click). Instead of this:
svg.selectAll(".edge").append("circle") # appends one circle to each edge
Try this:
svg.append("circle") # appends a single circle to the SVG image
After changing your fiddle accordingly I was able to fire the drag event, but it still needs work. Using the drag behaviour you probably want to look at the event.dx and event.dy values rather than the absolute values, and you can simply change the circle's cx and cy instead of applying a translation (if that's easier). See https://jsfiddle.net/pzej8tkq/3/ for ideas.
I am learning D3. I know easy things like making a scatter plot and all. My next step is trying some simple interactive moves. For example, after I have an svg appended, axes and grids made, now I wish to make a circle by clicking a point within the svg canvas. I guess I will have to record coordinate of the clicked point and then append a circle with its cx nad cy, but how? How to record the coordinate?
I appreciate you show me a tutorial, give a hint or best of all an example.
If you are familiar with JQuery then D3 should have a friendly feel to it, as it shares a similar API. Specifically in regards to .on(action, fn) syntax for attaching an event listener to a DOM selection.
If you check out the jsFiddle I have created which implements a very basic implementation of what you are after then you can see this in motion in 21 lines of JS.
(function() {
var svg = d3.select('svg');
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
function drawCircle(x, y, size) {
console.log('Drawing circle at', x, y, size);
svg.append("circle")
.attr('class', 'click-circle')
.attr("cx", x)
.attr("cy", y)
.attr("r", size);
}
svg.on('click', function() {
var coords = d3.mouse(this);
console.log(coords);
drawCircle(coords[0], coords[1], getRandom(5,50));
});
})();
The most important aspects of this snippet are on lines 18-20 (.on(...) and d3.mouse(this)). The on('click',..) event is attached to the svg element. When a click occurs d3.mouse is called with the current scope as its argument. It then returns an array with x and y coords of the mouse event. This information is then passed to drawCircle, along with a random radius to draw a circle on the current SVG canvas.
I advise you to take the jsFiddle and have a play!