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);
}
I'm trying to animate a given element to go around a pre-defined radius and I'm having trouble getting the position of the element at a Y point given.
I'm trying to find each point with the circle equation, but I can only get one point out of the two possible ones.
In Javascript, I use Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) to get the point. assuming the center of the of the circle is 0,0.
but then I need to translate it to pixels on the screen since there are no negative pixels in positions on the browser.
All the sizing is relative to the window. so the radius, for example, is 80% of the height of the window in my tests.
Also, I'm trying to calculate what the distance of the element between each frame should be for the duration, but I'm not using it yet because I try to fix the issue above first.
This is what I have(a cleaned up version):
let height = window.innerHeight * 0.8,
radius = height / 2,
circumferance = (radius * 2) * Math.PI,
container = document.getElementById('container'),
rotating = document.querySelector('.rotating'),
centerX = radius - (rotating.offsetWidth / 2),
centerY = radius - (rotating.offsetHeight / 2),
duration = 10,
stepDistance = circumferance / 16;
// Setting the dimensions of the container element.
container.style.height = height + 'px';
container.style.width = height + 'px';
// return positive X of any given Y.
function getXOffset(y) {
return Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2);
}
// Setting the position of the rotating element to the start.
rotating.style.top = 0 + 'px';
rotating.style.left = centerX + 'px';
setInterval(() => {
let top = parseInt(rotating.style.top),
y = radius - top;
rotating.style.top = (top + 1) + 'px';
rotating.style.left = (centerX + getXOffset(y)) + 'px';
}, 16);
Here is a fiddle with a bit more code for trying to get the right amount of distance between points for a smoother animation(currently needs fixing, but it doesn't bother me yet.)
https://jsfiddle.net/shock/1qcfvr4y/
Last note: I know that there might be other ways to do this with CSS, but I chose to use javascript for learning purposes.
Math.sqrt would only return the positive root. You'll have to account for the negative value based on the application. In this case, you need the positive x value during the 1st half of the cycle and negative during the 2nd half.
To do that, you should implement a method to track the progress and reverse the sign accordingly.
Here is a sample. I modified upon yours.
edit:
Instead of Math.sqrt( Math.pow(radius, 2) - Math.pow(y, 2) , 2) You can use the full formula to get x if you do not want to assume origin as center, which in this case is Math.sqrt( Math.pow(radius, 2) - Math.pow((actualY - centerY), 2) , 2)
explanation:
The original equation (x-a)² + (y'-b)² = r²
becomes x = √(r² - (y'-b)²) + a
Assuming .rotating box have 0 width and height.
The variable equivalents in your code are centerX = a, centerY = b.
By assuming origin as center you're basically doing a pre-calculation so that your y value becomes the equivalent of (y'-b). Hence x = √(r² - y²) + a is valid.
At initial state top = 0
i.e (y'-b) => height - centerY.
In your code y = radius => height/2.
Now (height - centerY) being equal to (height/2) is a side effect of your circle being bound by a square container whose height determines the y value.
In other words, when you use origin as center, you are taking the center offsets outside of circle equation and handling it separately. You could do the same thing by using the whole formula, that is, x = √(r² - (y'-b)²) + a
I'm attempting to make an interactive pan/zoom SVG floorplan/map using the d3.behavior.zoom() functionality. I have based my code loosely on Zoom to Bounding Box II.
I am asynchronously loading a svg via $.get() then using a button.on('click') handler to get the .getBBox() of a specific <g> by element ID, #g-1101 (represented as a red circle on the svg). I then would like to center the viewport of the svg to the middle of #g-1101's bounding box.
As a cursory try I was just trying to translate the top-level svg > g by using g#1101's .getBBox().x && .getBBox().y. It seems to me my math is off.
I've tried incorporating the (svg.node().getBBox().width / 2) - scale * g.getBBox().x) to center the middle point of the bounding box to viewport but it's translation is not even in the ballpark.
Code
(function(){
var svg, g; $.get('http://layersofcomplexity.com/__fp.svg', function(svg){
$('body').append($(svg));
init();
},'text');
function init() {
console.log('init');
svg = d3.select('svg');
g = d3.select('svg > g');
var zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 8])
.on("zoom", zoomed);
svg
.call(zoom)
.call(zoom.event);
$('.pan').on('click', function(){
// var id = '#g-1011';
var scale = 4;
var bbox = $('#g-1101')[0].getBBox();
// d3.select(id).node().getBBox();
var x = bbox.x;
var y = bbox.y;
// var scale = .9 / Math.max(dx / width, dy / height),
// var translate = [width / 2 - scale * x, height / 2 - scale * y];
var width = svg.node().getBBox().width;
var height = svg.node().getBBox().height;
var translate = [-scale*x,-scale*y];
g.transition().duration(750) .call(zoom.translate(translate).scale(scale).event);
});
}
function zoomed() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
})();
-- EDIT JSBin was broken --
What am I missing? JSBin.
One small change in your code to center the marked in red g#g-1101
var bbox = $('#g-1101')[0].getBBox();
var x = bbox.x-((bbox.width));
var y = bbox.y-((bbox.height));
//scale should be 1 less
var translate = [-(x*(scale-1)),-(y*(scale-1))];
working code here
Hope this helps
Currently in d3 if you have a geoJSON object that you are going to draw you have to scale it and translate it in order to get it to the size that one wants and translate it in order to center it. This is a very tedious task of trial and error, and I was wondering if anyone knew a better way to obtain these values?
So for instance if I have this code
var path, vis, xy;
xy = d3.geo.mercator().scale(8500).translate([0, -1200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#vis").append("svg:svg").attr("width", 960).attr("height", 600);
d3.json("../../data/ireland2.geojson", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill", "#85C3C0")
.attr("stroke", "#222");
});
How the hell do I obtain .scale(8500) and .translate([0, -1200]) without going little by little?
My answer is close to Jan van der Laan’s, but you can simplify things slightly because you don’t need to compute the geographic centroid; you only need the bounding box. And, by using an unscaled, untranslated unit projection, you can simplify the math.
The important part of the code is this:
// Create a unit projection.
var projection = d3.geo.albers()
.scale(1)
.translate([0, 0]);
// Create a path generator.
var path = d3.geo.path()
.projection(projection);
// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(state),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
// Update the projection to use computed scale & translate.
projection
.scale(s)
.translate(t);
After comping the feature’s bounding box in the unit projection, you can compute the appropriate scale by comparing the aspect ratio of the bounding box (b[1][0] - b[0][0] and b[1][1] - b[0][1]) to the aspect ratio of the canvas (width and height). In this case, I’ve also scaled the bounding box to 95% of the canvas, rather than 100%, so there’s a little extra room on the edges for strokes and surrounding features or padding.
Then you can compute the translate using the center of the bounding box ((b[1][0] + b[0][0]) / 2 and (b[1][1] + b[0][1]) / 2) and the center of the canvas (width / 2 and height / 2). Note that since the bounding box is in the unit projection’s coordinates, it must be multiplied by the scale (s).
For example, bl.ocks.org/4707858:
There’s a related question where which is how to zoom to a specific feature in a collection without adjusting the projection, i.e., combining the projection with a geometric transform to zoom in and out. That uses the same principles as above, but the math is slightly different because the geometric transform (the SVG "transform" attribute) is combined with the geographic projection.
For example, bl.ocks.org/4699541:
The following seems to do approximately what you want. The scaling seems to be ok. When applying it to my map there is a small offset. This small offset is probably caused because I use the translate command to center the map, while I should probably use the center command.
Create a projection and d3.geo.path
Calculate the bounds of the current projection
Use these bounds to calculate the scale and translation
Recreate the projection
In code:
var width = 300;
var height = 400;
var vis = d3.select("#vis").append("svg")
.attr("width", width).attr("height", height)
d3.json("nld.json", function(json) {
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// add a rectangle to see the bound of the svg
vis.append("rect").attr('width', width).attr('height', height)
.style('stroke', 'black').style('fill', 'none');
vis.selectAll("path").data(json.features).enter().append("path")
.attr("d", path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black")
});
With d3 v4 or v5 its getting way easier!
var projection = d3.geoMercator().fitSize([width, height], geojson);
var path = d3.geoPath().projection(projection);
and finally
g.selectAll('path')
.data(geojson.features)
.enter()
.append('path')
.attr('d', path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black");
Enjoy, Cheers
I'm new to d3 - will try to explain how I understand it but I'm not sure I got everything right.
The secret is knowing that some methods will operate on the cartographic space (latitude,longitude) and others on the cartesian space (x,y on the screen). The cartographic space (our planet) is (almost) spherical, the cartesian space (screen) is flat - in order to map one over the other you need an algorithm, which is called projection. This space is too short to deep into the fascinating subject of projections and how they distort geographic features in order to turn spherical into plane; some are designed to conserve angles, others conserve distances and so on - there is always a compromise (Mike Bostock has a huge collection of examples).
In d3, the projection object has a center property/setter, given in map units:
projection.center([location])
If center is specified, sets the projection’s center to the specified location, a two-element array of longitude and latitude in degrees and returns the projection. If center is not specified, returns the current center which defaults to ⟨0°,0°⟩.
There is also the translation, given in pixels - where the projection center stands relative to the canvas:
projection.translate([point])
If point is specified, sets the projection’s translation offset to the specified two-element array [x, y] and returns the projection. If point is not specified, returns the current translation offset which defaults to [480, 250]. The translation offset determines the pixel coordinates of the projection’s center. The default translation offset places ⟨0°,0°⟩ at the center of a 960×500 area.
When I want to center a feature in the canvas, I like to set the projection center to the center of the feature bounding box - this works for me when using mercator (WGS 84, used in google maps) for my country (Brazil), never tested using other projections and hemispheres. You may have to make adjustments for other situations, but if you nail these basic principles you will be fine.
For example, given a projection and path:
var projection = d3.geo.mercator()
.scale(1);
var path = d3.geo.path()
.projection(projection);
The bounds method from path returns the bounding box in pixels. Use it to find the correct scale, comparing the size in pixels with the size in map units (0.95 gives you a 5% margin over the best fit for width or height). Basic geometry here, calculating the rectangle width/height given diagonally opposed corners:
var b = path.bounds(feature),
s = 0.9 / Math.max(
(b[1][0] - b[0][0]) / width,
(b[1][1] - b[0][1]) / height
);
projection.scale(s);
Use the d3.geo.bounds method to find the bounding box in map units:
b = d3.geo.bounds(feature);
Set the center of the projection to the center of the bounding box:
projection.center([(b[1][0]+b[0][0])/2, (b[1][1]+b[0][1])/2]);
Use the translate method to move the center of the map to the center of the canvas:
projection.translate([width/2, height/2]);
By now you should have the feature in the center of the map zoomed with a 5% margin.
There is a center() method you can use that accepts a lat/lon pair.
From what I understand, translate() is only used for literally moving the pixels of the map. I am not sure how to determine what scale is.
In addition to Center a map in d3 given a geoJSON object, note that you may prefer fitExtent() over fitSize() if you want to specify a padding around the bounds of your object. fitSize() automatically sets this padding to 0.
I was looking around on the Internet for a fuss-free way to center my map, and got inspired by Jan van der Laan and mbostock's answer. Here's an easier way using jQuery if you are using a container for the svg. I created a border of 95% for padding/borders etc.
var width = $("#container").width() * 0.95,
height = $("#container").width() * 0.95 / 1.9 //using height() doesn't work since there's nothing inside
var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width);
var path = d3.geo.path().projection(projection);
var svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
If you looking for exact scaling, this answer won't work for you. But if like me, you wish to display a map that centralizes in a container, this should be enough. I was trying to display the mercator map and found that this method was useful in centralizing my map, and I could easily cut off the Antarctic portion since I didn't need it.
To pan/zoom the map you should look at overlaying the SVG on Leaflet. That will be a lot easier than transforming the SVG. See this example http://bost.ocks.org/mike/leaflet/ and then How to change the map center in leaflet
With mbostocks' answer, and Herb Caudill's comment, I started running into issues with Alaska since I was using a mercator projection. I should note that for my own purposes, I am trying to project and center US States. I found that I had to marry the two answers with Jan van der Laan answer with following exception for polygons that overlap hemispheres (polygons that end up with a absolute value for East - West that is greater than 1):
set up a simple projection in mercator:
projection = d3.geo.mercator().scale(1).translate([0,0]);
create the path:
path = d3.geo.path().projection(projection);
3.set up my bounds:
var bounds = path.bounds(topoJson),
dx = Math.abs(bounds[1][0] - bounds[0][0]),
dy = Math.abs(bounds[1][1] - bounds[0][1]),
x = (bounds[1][0] + bounds[0][0]),
y = (bounds[1][1] + bounds[0][1]);
4.Add exception for Alaska and states that overlap the hemispheres:
if(dx > 1){
var center = d3.geo.centroid(topojson.feature(json, json.objects[topoObj]));
scale = height / dy * 0.85;
console.log(scale);
projection = projection
.scale(scale)
.center(center)
.translate([ width/2, height/2]);
}else{
scale = 0.85 / Math.max( dx / width, dy / height );
offset = [ (width - scale * x)/2 , (height - scale * y)/2];
// new projection
projection = projection
.scale(scale)
.translate(offset);
}
I hope this helps.
For people who want to adjust verticaly et horizontaly, here is the solution :
var width = 300;
var height = 400;
var vis = d3.select("#vis").append("svg")
.attr("width", width).attr("height", height)
d3.json("nld.json", function(json) {
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// adjust projection
var bounds = path.bounds(json);
offset[0] = offset[0] + (width - bounds[1][0] - bounds[0][0]) / 2;
offset[1] = offset[1] + (height - bounds[1][1] - bounds[0][1]) / 2;
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// add a rectangle to see the bound of the svg
vis.append("rect").attr('width', width).attr('height', height)
.style('stroke', 'black').style('fill', 'none');
vis.selectAll("path").data(json.features).enter().append("path")
.attr("d", path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black")
});
How I centered a Topojson, where I needed to pull out the feature:
var projection = d3.geo.albersUsa();
var path = d3.geo.path()
.projection(projection);
var tracts = topojson.feature(mapdata, mapdata.objects.tx_counties);
projection
.scale(1)
.translate([0, 0]);
var b = path.bounds(tracts),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
svg.append("path")
.datum(topojson.feature(mapdata, mapdata.objects.tx_counties))
.attr("d", path)
I am making a small multiples chart that visualizes multiple neighborhoods.
Imagine something like this, but with neighborhoods not a country:
My issue is, when I append each path in an enter() method, each neighborhood skews such that its position reflects its position in the city at large. I want each neighborhood centered.
This image displays my trouble (each neighborhood offset from the others):
I can use Mike Bostock's instructions for centering a map, which are all well and good. But then I need to recenter every neighborhood. How can I do that such that I'm passing an instance of d into the data.bounds() method and redefining each projection?
Here's how the initial definition of centering happens:
(function run() {
d3.json("./data/output.geojson", function(error, map) {
var b = path.bounds(map),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][4] - b[0][5]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][6] + b[0][7])) / 2];
projection
.scale(s)
.translate(t);
var chart = d3.select("#charts").selectAll("svg")
.data(map.features)
.enter()
.append("svg")
.attr("width", mapWidth )
.attr("height", mapHeight )
.append("g")
chart
.append("path")
.attr("d", path)
.style("fill","steelblue");
});
})()
Will it be necessary to make each map projection its own instance of data entry with the .data().enter() pattern?
Edit:
Final version looks like this:
You can use .each():
chart.append("path")
.each(function(d) {
var b = path.bounds(d),
s = 1.5 / Math.max((b[1][0] - b[0][0]) / mapWidth, (b[1][1] - b[0][1]) / mapHeight),
t = [(mapWidth - s * (b[1][0] + b[0][0])) / 2, (mapHeight - s * (b[1][1] + b[0][1])) / 2];
projection.scale(s).translate(t);
d3.select(this).attr("d", path);
});
This doesn't quite work because the bounds of a feature depend on the projection the path is using. By changing the projection, you're also changing the bounds of subsequent features. To fix that, reset the projection parameters to their previous values after setting the d attribute:
var os = projection.scale(),
ot = projection.translate();
// ...
projection.scale(s).translate(t);
d3.select(this).attr("d", path);
projection.scale(os).translate(ot);