d3.js - scaling canvas points to overlay on svg map during zoom - javascript

I am rendering a raster map using d3.geo.tile, like in this example here (the raster images don't work on that page, so I cloned it over here). Unlike this example I have thousands of points to show, so I am rendering them using HTML5 Canvas instead of SVG. I positioned the canvas directly over the map.
The points are rendering correctly, and I am able to pan the map. However, if I zoom in the points are not translated to the proper coordinates.
Here is the function that draws these points:
function set_scales() {
var translate = zoom.sub_regions.translate(),
scale = zoom.sub_regions.scale(),
width = sub_region.get('width'),
height = sub_region.get('height');
var x1 = -translate[0]+width/2;
var y1 = -translate[1]+height/2;
var x2 = width + x1;
var y2 = height + y1;
var x = d3.scale.linear().domain([x1, x2]).range([0, width]);
var y = d3.scale.linear().domain([y1, y2]).range([0, height ]);
draw_canvas();
function draw_canvas() {
sub_region.var.context.clearRect(0, 0, width, height);
var data = sub_region.get('points');
if (!data) {
return;
}
var i = -1, n = data.length, d, cx, cy;
var canvas = sub_region.get('context');
canvas.fillStyle = '#0A00FF';
canvas.beginPath();
while (++i < n) {
d = data[i];
cx = x(d[0]);
cy = y(d[1]);
canvas.moveTo(cx, cy);
canvas.arc(cx, cy, 1, 0, 2 * Math.PI);
}
canvas.fill()
}
}
I know that I should be using the scale variable in there somehow, but I can't figure out how. The scale at which the points are positioned properly is 4096. I tried making a variable called zoom_factor setting it to 4096/scale, and multiplying the x1, x2, y1, and y2 coordinates by it, but that didn't work. Perhaps I didn't do it correctly.
I took some screenshots:
This is before the zoom (points render correctly):
This is after the zoom (points translated to incorrect coordinates):
If you want to see it in all its broken glory go here and click on the United States, then select "Stations."
I could really use some help on this one; been running in circles for days!

Related

Show data labels inside donut pie chart p5js

I'm building a p5js donut chart, but I'm struggling to show the data labels in the middle. I think I have managed to get the boundaries right for it, but how would match the angle that I'm in? Or is there a way of matching just through the colours?
https://i.stack.imgur.com/enTBo.png
I have started by trying to match the boundaries of the chart to the pointer, which I managed to do using mouseX and mouseY. Any suggestions, please?
if(mouseX >= width / 2 - width * 0.2 && mouseY >= height / 2 - width * 0.2
&& mouseX <= width / 2 + width * 0.2 && mouseY <= height / 2 + width * 0.2)
{
//console.log("YAY!!! I'm inside the pie chart!!!");
}
else
{
textSize(14);
text('Hover over to see the labels', width / 2, height / 2);
}
};
[1]: https://i.stack.imgur.com/enTBo.png
While you could theoretically use the get() function to check the color of the pixel under the mouse cursor and correlate that with one of the entries in your dataset, I think you would be much better off doing the math to determine which segment the mouse is currently over. And conveniently p5.js provides helper functions that make it very easy.
In the example you showed you are only checking if the mouse cursor is in a rectangular region. But in reality you want to check if the mouse cursor is within a circle. To do this you can use the dist(x1, y1, x2, y2) function. Once you've established that the mouse cursor is over your pie chart, you'll want to determine which segment it is over. This can be done by finding the angle between a line draw from the center of the chart to the right (or whichever direction is where you started drawing the wedges), and a line drawn from the center of the chart to the mouse cursor. This can be accomplished using the angleBetween() function of p5.Vector.
Here's a working example:
const colors = ['red', 'green', 'blue'];
const thickness = 40;
let segments = {
foo: 34,
bar: 55,
baz: 89
};
let radius = 80, centerX, centerY;
function setup() {
createCanvas(windowWidth, windowHeight);
noFill();
strokeWeight(thickness);
strokeCap(SQUARE);
ellipseMode(RADIUS);
textAlign(CENTER, CENTER);
textSize(20);
centerX = width / 2;
centerY = height / 2;
}
function draw() {
background(200);
let keys = Object.keys(segments);
let total = keys.map(k => segments[k]).reduce((v, s) => v + s, 0);
let start = 0;
// Check the mouse distance and angle
let mouseDist = dist(centerX, centerY, mouseX, mouseY);
// Find the angle between a vector pointing to the right, and the vector
// pointing from the center of the window to the current mouse position.
let mouseAngle =
createVector(1, 0).angleBetween(
createVector(mouseX - centerX, mouseY - centerY)
);
// Counter clockwise angles will be negative 0 to PI, switch them to be from
// PI to TWO_PI
if (mouseAngle < 0) {
mouseAngle += TWO_PI;
}
for (let i = 0; i < keys.length; i++) {
stroke(colors[i]);
let angle = segments[keys[i]] / total * TWO_PI;
arc(centerX, centerY, radius, radius, start, start + angle);
// Check mouse pos
if (mouseDist > radius - thickness / 2 &&
mouseDist < radius + thickness / 2) {
if (mouseAngle > start && mouseAngle < start + angle) {
// If the mouse is the correct distance from the center to be hovering over
// our "donut" and the angle to the mouse cursor is in the range for the
// current slice, display the slice information
push();
noStroke();
fill(colors[i]);
text(`${keys[i]}: ${segments[keys[i]]}`, centerX, centerY);
pop();
}
}
start += angle;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/p5.js"></script>
I think I know the source of the problem was that #thenewbie experienced: it is the p5 library being used. I was using the p5.min.js and experiencing the same problem. Once I started using the full p5.js library, the issue was resolved and #Paul's script worked.
Here is a link I came across while researching this which put me onto the solution:
https://github.com/processing/p5.js/issues/3973
Thanks Paul for the clear explanations and code above.

Problems fitting a map in a container

In a previous question, a user informed me of a great function to center a map and adapt its size to the container.
"There is this nice gist from nrabinowitz, which provides a function which scales and translate a projection to fit a given box.
It goes through each of the geodata points (data parameter), projects it (projection parameter), and incrementally update the necessary scale and translation to fit all points in the container (box parameter) while maximizing the scale:
function fitProjection(projection, data, box, center) {
...
return projection.scale(scale).translate([transX, transY])
}
I love this function but for now I would not mind using something that solves my problem. This works for any map, but specifically for the one in Colombia it does not work for me.
I'm trying to center the map to the container so that it fits the center and the size is the right one to the container. but I can not get it to adapt. I have also tried with a .translate and it does not work for me. Is something wrong?
Here is my code:
function fitProjection(projection, data, box, center) {
// get the bounding box for the data - might be more efficient approaches
var left = Infinity,
bottom = -Infinity,
right = -Infinity,
top = Infinity;
// reset projection
projection
.scale(1)
.translate([0, 0]);
data.features.forEach(function(feature) {
d3.geo.bounds(feature).forEach(function(coords) {
coords = projection(coords);
var x = coords[0],
y = coords[1];
if (x < left) left = x;
if (x > right) right = x;
if (y > bottom) bottom = y;
if (y < top) top = y;
});
});
// project the bounding box, find aspect ratio
function width(bb) {
return (bb[1][0] - bb[0][0])
}
function height(bb) {
return (bb[1][1] - bb[0][1]);
}
function aspect(bb) {
return width(bb) / height(bb);
}
var startbox = [[left, top], [right, bottom]],
a1 = aspect(startbox),
a2 = aspect(box),
widthDetermined = a1 > a2,
scale = widthDetermined ?
// scale determined by width
width(box) / width(startbox) :
// scale determined by height
height(box) / height(startbox),
// set x translation
transX = box[0][0] - startbox[0][0] * scale,
// set y translation
transY = box[0][1] - startbox[0][1] * scale;
// center if requested
if (center) {
if (widthDetermined) {
transY = transY - (transY + startbox[1][1] * scale - box[1][1])/2;
} else {
transX = transX - (transX + startbox[1][0] * scale - box[1][0])/2;
}
}
return projection.scale(scale).translate([transX, transY])
}
var width = document.getElementById('statesvg').offsetWidth;
var height =document.getElementById('statesvg').offsetHeight;
/*// Define path generator
var path = d3.geo.path() // path generator that will convert GeoJSON to SVG paths
.projection(projection); // tell path generator to use albersUsa projection
*/
//remove svg
d3.select("#statesvg svg").remove();
var svg = d3.select("#statesvg")
.append("svg")
.attr("width", width+"px")
.attr("height", height+"px");
d3.json("https://rawgit.com/john-guerra/43c7656821069d00dcbc/raw/be6a6e239cd5b5b803c6e7c2ec405b793a9064dd/Colombia.geo.json", function(data) {
var features = data.features;
var projection=fitProjection(d3.geo.mercator(), data, [[0, 0], [width, height]], true)
var path = d3.geo.path()
.projection(projection);
svg.selectAll('path')
.data(features)
.enter().append('path')
.classed('map-layer', true)
.attr('d', path)
.attr('vector-effect', 'non-scaling-stroke')
});
http://plnkr.co/edit/JWL6L7NnhOpwkJeTfO6h?p=preview
You said that the function...
works for any map, but specifically for the one in Colombia it does not work for me.
This makes no sense: what makes you think that the function has personal issues with Colombia?
The problem is just those islands at the top left corner, the Archipelago of San Andrés, Providencia and Santa Catalina. Let's remove them:
data.features = data.features.filter(function(d){
return d.properties.DPTO !== "88"
});
Here is the result in my browser:
Here is your updated Plunker: http://plnkr.co/edit/1G0xY7CCCoJv070pdcx4?p=preview

Advanced image skewing in JavaScript, fill polygon with image [duplicate]

I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms

Zoom in in a canvas at a certain point

I try to let the user zoom in the canvas with a pinch gesture, it's a Javascript Canvas Game (using Intel XDK)
I got the point coordinates (relativley to the window document, saved in an array) and the scale "strength".
var scale = 1;
function scaleCanvas(sc, point) { //point["x"] == 200
//sc has value like 0.5, 1, 1.5 and so on
x = sc/scale;
scale = sc;
ctx.scale(x, x);
}
I know that I have to translate the canvas to the point coordinates, and then retranslate it again. My problem is, that the canvas is already translated. The translation values are saved in the vars dragOffX and dragOffY. Furthermore, the initial translation may be easy, but when the canvas is already scaled, every coordinate is changed.
This is the translation of the canvas when dragging/shifting the content:
var dragOffX = 0;
var dragOffY = 0;
function dragCanvas(x,y) {
dragOffX = dragOffX + x;
dragOffY = dragOffY + y;
x = x* 1/scale;
y = y* 1/scale;
ctx.translate(x,y);
}
So when the player is dragging the content for e.g. 100px to the right, dragOffX gets the value 100.
How do I translate my canvas to the correct coordinates?
It will probably be easier if you store the transformation matrix and use setTransform each time you change it - that resets the canvas transformation matrix first before applying the new transformation, so that you have easier control over the way that the different transformations accumulate.
var transform = {x: 0, y: 0, scale: 1}
function scaleCanvas(scale, point) {
var oldScale = transform.scale;
transform.scale = scale / transform.scale;
// Re-centre the canvas around the zoom point
// (This may need some adjustment to re-centre correctly)
transform.x += point.x / transform.scale - point.x / oldScale
transform.y += point.y / transform.scale - point.y / oldScale;
setTransform();
}
function dragCanvas(x,y) {
transform.x += x / transform.scale;
transform.y += y / transform.scale;
setTransform();
}
function setTransform() {
ctx.setTransform(transform.scale, 0, 0, transform.scale, transform.x, transform.y);
}
JSFiddle
Simply Use this to scale canvas on pivot point
function scaleCanvasOnPivotPoint(s, p_x , p_y) {
ctx.translate(p_x, p_y);
ctx.scale(s);
ctx.translate( -p_x, -p_y);
}

transform matrix

I have an svg map with a g element container.
inside the g element I have items with x, y positions.
I am trying to implement a mouse wheel zoom that pans the g element so that the object under the mouse is always under the mouse. similar to the way Google maps pans the map when zooming via the mouse wheel so that you zoom to the mouse position.
I have exhausted all searches and tried many different ways to calculate out the mouse position verses the g element position.
I've tried:
var xPan = (mouse.x - (matrix.scale * mouse.x)) - matrix.panX;
var yPan = (mouse.y - (matrix.scale * mouse.y)) - matrix.panY;
pan(xPan, yPan);
I had an similar problem some time ago, with the difference that I am using canvas but because I use svg to save my transform matrix it may help you, if I post the necessary part of my code:
window.transform = svg.createSVGMatrix();
window.pt = svg.createSVGPoint();
transformedPoint = function (x, y) {
window.pt.x = x; window.pt.y = y;
return pt.matrixTransform(window.transform.inverse());
}
translate = function(dx, dy) {
window.transform = window.transform.translate(dx, dy);
}
scale = function (scaleX, scaleY) {
window.transform = window.transform.scaleNonUniform(scaleX, scaleY);
};
zoom = function (scaleX, scaleY, x, y) { //use real x and y i.e. mouseposition on element
var p = transformedPoint(x, y);
translate(x, y);
scale(scaleX, scaleY);
translate(-x, -y);
}
I hope you can use some of this code and get it to work for you
Credits going to Phrogz and his outstanding example here: https://stackoverflow.com/a/5527449/1293849

Categories

Resources