Drag issue with d3.js - javascript

Hi If someone can help me with this would be great. I did a zoom pan with d3.js which is working fine:
function zoom() {
var e = d3.event
var scale = d3.event.scale;
canvas.save();
canvas.clearRect(0, 0, width, height);
canvas.beginPath();
canvas.translate(e.translate[0], e.translate[1]);
canvas.scale(scale, scale);
draw();
canvas.restore();
}
then I wanted to have the image only inside the canvas area and I did it like this:
function zoom() {
var scale = d3.event.scale;
var e = d3.event,
tx = Math.min(0, Math.max(e.translate[0], width - imgBG.width * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], height - imgBG.height * e.scale))
canvas.save();
canvas.clearRect(0, 0, width, height);
canvas.beginPath();
canvas.translate(tx, ty);
canvas.scale(scale, scale);
draw();
canvas.restore();
}
Here is a the working code: https://jsfiddle.net/ux7gbedj/
The problem is that: for example when the fiddle is loaded and I start dragging from left to right, let say 2 times, the Image is not moving which is fine, but then when I try to drag from right to left I have to drag at least 3 times to start moving again, so I think I am not doing something very correct.

You need to feed the restricted translate coordinates (tx, ty) back into the zoom behaviour object, otherwise the d3.event translate coordinates are unbounded and eventually you'll find the image sticks at one of the corners/sides. i.e. you'll be trying to restrict the image dragging to a window of say -200<x<0 with your min/max's but your translate.x coordinate could be at -600 after some continuous dragging. Even if you then drag back 50 pixels to -550, the image will not move, as it will max() to -200 in your code.
Logic taken from http://bl.ocks.org/mbostock/4987520
...
// declare the zoom behaviour separately so you can reference it later
var zoomObj = d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom);
var canvas = d3.select("canvas")
.attr("width", width)
.attr("height", height)
.call(zoomObj)
.node().getContext("2d");
function zoom() {
var scale = d3.event.scale;
var e = d3.event,
tx = Math.min (0, Math.max(e.translate[0], width - imgBG.width * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], height - imgBG.height * e.scale));
zoomObj.translate( [tx,ty]); // THIS
canvas.save();
canvas.clearRect(0, 0, width, height);
canvas.beginPath();
canvas.translate(tx, ty);
canvas.scale(scale, scale);
draw();
canvas.restore();
}
...
https://jsfiddle.net/ux7gbedj/1/

Related

panning and zooming with scrollbars in d3 v3

I am trying to implement panning, zooming and scrolling with scroll bars.
I have two problems with zoom,pan and scroll bars combination:
1.pan/drag the svg is working with moving scrollbars, when you pan/drag the chart->scroll with scrollbars->pan/drag with cursor on svg, It goes to old position where last pan/drag is performed.
2.Scrollbar with zoom is not working wonders, when you zoom->scroll->zoom it zooms at the old location where the first zoom happened.
how to implement this lines "wrapper.call(d3.zoom().translateTo, x / scale, y / scale);" in d3.v3. It would be great help if anyone shows the solution to implement in d3 version 3. below code is for zooming, panning and scrolling.
function zoomed() {
var scale = d3.event.scale;
const scaledWidth = (newWidth+1000) * scale;
const scaledHeight = height * scale;
// Change SVG dimensions.
d3.select('#hierarchyChart svg')
.attr('width', scaledWidth)
.attr('height', scaledHeight);
// Scale the image itself.
d3.select('#hierarchyChart svg g').attr('transform', "scale("+scale+")");
// Move scrollbars.
const wrapper = d3.select('#hierarchyChart')[0];
if(d3.event.translate >= [0,0] && d3.event.translate <=[scaledWidth,scaledHeight]){
wrapper[0].scrollLeft = -d3.event.translate[0];
wrapper[0].scrollTop = -d3.event.translate[1];
}
console.log(wrapper[0].scrollLeft);
console.log(wrapper[0].scrollTop);
// If the image is smaller than the wrapper, move the image towards the
// center of the wrapper.
const dx = d3.max([0, wrapper[0].clientWidth/ 2 - scaledWidth / 2]);
const dy = d3.max([0, wrapper[0].clientHeight / 2 - scaledHeight / 2]);
// d3.select('svg').attr('transform', "translate(" + dx + "," + dy + ")");
}
function scrolled() {
const x = wrapper[0].scrollLeft + wrapper[0].clientWidth / 2;
const y = wrapper[0].scrollTop + wrapper[0].clientHeight / 2;
const scale = d3.event.scale;
// Update zoom parameters based on scrollbar positions.
// wrapper.call(d3.zoom().translateTo, x / scale, y / scale);
}

Scaling mouse drawings elements on a html canvas

Is there any way to scale drawing elements on a canvas? I'm creating an application where then user can place points on a canvas, but when I try to resize the browser window all the elements disappear.
My initial tentative was to calculate the screen difference before and after the resizing. After I get the percentages, I just sat the scale on the canvas and place the coordinates that was saved from the first time I drew on the canvas, but still doesn't work, if the points appear on the canvas, it is on the same place without scaling. Can someone give me little line of thought?
private calculateCanvasScreenDifference(previousSize, guestScreen) {
return ((controlScreen - currentSize) * 100) / controlScreen;
}
let difWidthPercent = Math.abs(this.calculateCanvasScreenDifference(canvasPreviousWidth, canvasWidth) * 0.01);
let difHeightPercent = Math.abs(this.calculateCanvasScreenDifference(canvasPreviousHeight, canvasHeight) * 0.01);
let scaleX = ((Math.abs(difWidthPercent) <= 1) ? 1.00 - difWidthPercent : difWidthPercent - 1.00);
let scaleY = ((Math.abs(difHeightPercent) <= 1) ? 1.00 - difHeightPercent : difHeightPercent - 1.00);
this.cx.scale(Number(scaleX), Number(scaleY));
...
...
// then start recreating the drawing that was previous saved on an array of object(x, y values)
this.cx.beginPath();
this.cx.arc(coord.x, coord.y, 7, 0, Math.PI * 2, true);
this.cx.stroke();
Keep track of your canvas width and start with a scale factor of 1.
let originalWidth = canvas.width;
let scale = 1;
On resize calculate the new scale factor. And update tracked canvas size.
let scale = newWidth / originalWidth;
originalWidth = newWidth;
Use the scale factor for all drawing at all times. e.g.
context.arc(coord.x * scale, coord.y * scale, radius, 0, Math.PI*2, false);
Note: This approach assumes the original and new canvas sizes are proportional. If not then you will need to track width and height, and calculate separate x and y scale factors.

Rectangle clip not scaling with canvas when zoom

Here is how I created a Rectangular clip to drop images in specific portion and now extending it a bit. Added zoom functionality to it.
Below is how I created a clip.
function clipByName(ctx) {
this.setCoords();
var clipRect = findByClipName(this.clipName);
var scaleXTo1 = (canvasScale/ this.scaleX);
var scaleYTo1 = (canvasScale / this.scaleY);
ctx.save();
var ctxLeft = -(this.width / 2) + clipRect.strokeWidth;
var ctxTop = -(this.height / 2) + clipRect.strokeWidth;
var ctxWidth = clipRect.width - clipRect.strokeWidth;
var ctxHeight = clipRect.height - clipRect.strokeWidth;
ctx.translate(ctxLeft, ctxTop);
ctx.scale(scaleXTo1, scaleYTo1);
ctx.rotate(degToRad(this.angle * -1));
ctx.beginPath();
ctx.rect(
clipRect.left - this.oCoords.tl.x,
clipRect.top - this.oCoords.tl.y,
clipRect.width,
clipRect.height
);
ctx.closePath();
ctx.restore();
}
However this clip is not getting zoomed with Canvas. This is how it looks like after zooming it.
Full code and demo is here : https://jsfiddle.net/yxuoav39/2/
It should look like below even after zoom. Adding a small video clip to demonstrate the issue.
Played around scaleX and scaleY and not succeed. Any pointers would be much appreciated to trace the bug.
The clip is set to the transform that is current when you create the path for the clip. Any changes after the clip path has been created do not effect the path and thus the clip.
Eg
ctx.save()
ctx.scale(2,2);
ctx.beginPath();
ctx.rect(10,10,20,20); // scale makes the clip 20,20,40,40
// no transforms from here on will change the above rect
ctx.scale(0.5,0.5); // << this does not change the above rect
ctx.clip(); //<< clip is at the scale of when rect was called

D3 v4 invert function

I am trying to project a JPG basemap onto an Orthographic projection using the inverse projection. I have been able to get it working in v3 of D3, but I am having an issue in v4 of D3. For some reason, v4 gives me the edge of the source image as the background (rather than the black background I have specified). Are there any known issues with the inverse projection in v4 or any fixes for this?
D3 v4 JSBin Link
<title>Final Project</title>
<style>
canvas {
background-color: black;
}
</style>
<body>
<div id="canvas-image-orthographic"></div>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
// Canvas element width and height
var width = 960,
height = 500;
// Append the canvas element to the container div
var div = d3.select('#canvas-image-orthographic'),
canvas = div.append('canvas')
.attr('width', width)
.attr('height', height);
// Get the 2D context of the canvas instance
var context = canvas.node().getContext('2d');
// Create and configure the Equirectangular projection
var equirectangular = d3.geoEquirectangular()
.scale(width / (2 * Math.PI))
.translate([width / 2, height / 2]);
// Create and configure the Orthographic projection
var orthographic = d3.geoOrthographic()
.scale(Math.sqrt(2) * height / Math.PI)
.translate([width / 2, height / 2])
.clipAngle(90);
// Create the image element
var image = new Image(width, height);
image.crossOrigin = "Anonymous";
image.onload = onLoad;
image.src = 'https://tatornator12.github.io/classes/final-project/32908689360_24792ca036_k.jpg';
// Copy the image to the canvas context
function onLoad() {
// Copy the image to the canvas area
context.drawImage(image, 0, 0, image.width, image.height);
// Reads the source image data from the canvas context
var sourceData = context.getImageData(0, 0, image.width, image.height).data;
// Creates an empty target image and gets its data
var target = context.createImageData(image.width, image.height),
targetData = target.data;
// Iterate in the target image
for (var x = 0, w = image.width; x < w; x += 1) {
for (var y = 0, h = image.height; y < h; y += 1) {
// Compute the geographic coordinates of the current pixel
var coords = orthographic.invert([x, y]);
// Source and target image indices
var targetIndex,
sourceIndex,
pixels;
// Check if the inverse projection is defined
if ((!isNaN(coords[0])) && (!isNaN(coords[1]))) {
// Compute the source pixel coordinates
pixels = equirectangular(coords);
// Compute the index of the red channel
sourceIndex = 4 * (Math.floor(pixels[0]) + w * Math.floor(pixels[1]));
sourceIndex = sourceIndex - (sourceIndex % 4);
targetIndex = 4 * (x + w * y);
targetIndex = targetIndex - (targetIndex % 4);
// Copy the red, green, blue and alpha channels
targetData[targetIndex] = sourceData[sourceIndex];
targetData[targetIndex + 1] = sourceData[sourceIndex + 1];
targetData[targetIndex + 2] = sourceData[sourceIndex + 2];
targetData[targetIndex + 3] = sourceData[sourceIndex + 3];
}
}
}
// Clear the canvas element and copy the target image
context.clearRect(0, 0, image.width, image.height);
context.putImageData(target, 0, 0);
}
</script>
The problem is that the invert function is not one to one. There are two ways that I'm aware of that can solve the problem. One, calculate the area of the disc that makes up the projection and skip pixels that are outside of that radius. Or two (which I use below), calculate the forward projection of your coordinates and see if they match the x,y coordinates that you started with:
if (
(Math.abs(x - orthographic(coords)[0]) < 0.5 ) &&
(Math.abs(y - orthographic(coords)[1]) < 0.5 )
)
Essentially this asks is [x,y] equal to projection(projection.invert([x,y])). By ensuring that this statement is equal (or near equal) then the pixel is indeed in the projection disc. This is needed as multiple svg points can represent a given lat long but projection() returns only the one you want.
There is a tolerance factor there for rounding errors in the code block above, as long as the forward projection is within half a pixel of the original x,y coordinate it'll be drawn (which appears to work pretty well):
I've got an updated bin here (click run, I unchecked auto run).
Naturally this is the more computationally involved process when compared to calculating the radius of the projection disc (but that method is limited to projections that project to a disc).
This question's two answers might be able to explain further - they cover both approaches.

D3.js map zoom behaviors in canvas and svg

Thanks for reading.
The Goal
I would like to compare a working SVG click-to-zoom from Mike Bostock's blocks to a canvas-based system. I've placed the working SVG on the top, and a canvas on the bottom. When a user clicks on a state in the upper SVG, I would like the lower canvas element to "follow", or mimic, the zooming and panning. For example, clicking Minnesota in the upper SVG will also cause the lower canvas to zoom and pan to Minnesota.
The Problem
My canvas element draws fine after loading the topojson, but it does not animate. I would like it to animate. I believe this is because I do not fully understand zoom behaviors and path-based projections.
http://jsfiddle.net/30w8nv4t/2/
function zoomed(d) {
g.style("stroke-width", 1.5 / d3.event.scale + "px");
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
// the zTranslate and zScale variables appear to be ok,
// but `d` is null. I'm not sure how to redraw.
var zTranslate = zoom.translate();
var zScale = zoom.scale();
console.log(zTranslate, zScale, d);
context.clearRect(0, 0, width, height);
context.beginPath();
canvasPath(d);
context.stroke();
}
In this case, d is clearly null, and I'm not able to redraw anything. I assume my problem could be in either the zoomed method or in the clicked function.
The Reason
I am using this side-by-side approach because I would like to learn how paths, projections, and zoom behaviors work together. I admire how well canvas performs against SVG, but the lack of interactivity is daunting. Fortunately, being able to zoom and pan to arbitrary geometry cuts my problem in half.
Thank you for reading. The link to JSFiddle is at the top of this post.
The canvas drawing function uses the projected values of the lat/long co-ordinates, but you're not updating the scale and translate of your projection in your zoom handler.
One way to get the behavior that you're after, is to switch from a transform on the svg in the zoom handler, to a transform on the projection.
I have done just that in this updated fiddle: http://jsfiddle.net/30w8nv4t/7/
The differences are:
Update the zoom behavior to use the projection translate and scale as the default values, and to set the scaleExtent values to be based on the projection as well.
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([projection.scale()/5, projection.scale()*5])
.on("zoom", zoomed);
Update your zoomed function to translate and scale the projection and then redraw the svg based paths.
function zoomed(d) {
//g.style("stroke-width", 1.5 / d3.event.scale + "px");
//g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
var zTranslate = zoom.translate();
var zScale = zoom.scale();
console.log(zTranslate, zScale, d);
context.clearRect(0, 0, width, height);
context.beginPath();
canvasPath(states);
context.stroke();
}
Update your clicked function accordingly.
function clicked(d) {
if (active.node() === this) {
zoom.scale(500).translate([width/2, height/2]);
active.classed("active", false);
active = d3.select(null);
} else {
var centroid = path.centroid(d),
translate = zoom.translate(),
bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
scale = .9/ Math.max(dx / width, dy / height);
zoom.scale(scale * zoom.scale())
.translate([
translate[0] - centroid[0] * scale + width * scale / 2,
translate[1] - centroid[1] * scale + height * scale / 2]);
active.classed("active", false);
active = d3.select(this).classed("active", true);
}
zoom.event(svg);
}
This is probably one of the major components of the change, as the scale and translate are applied to the zoom behavior, and when they are, they have to be scaled by the current zoom scale. The clicked function then fires the zoomed function to redraw the svg and canvas elements.
As you can see, your canvas drawing code was correct. It was just that the drawing code of was using the projection to determine the x and y positions of the points to draw based on the projection which wasn't being updated by the zoom handler.
It would also be possible to have a separate projection for the canvas, and update that in the zoom handler before calling the canvas redraw functions. I'll leave that as an exercise for the reader!

Categories