D3 Map disable zoom on city label - d3.js

i'm stuck in this problem for a few days, and i hope someone can help me.
Basically this is the problem:
I've a UK map , with city label and marker. I've enabled zoom and pan on this, but when i zoom in even marker and label grow in dimension.
I'd like to preserve marker poisiton during zoom and pan (London is always in its place) but don't scale the dimension of marker and label.
This is my zoom function
var zoomBehav= function(){
var t = d3.event.translate,
s = d3.event.scale;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
zoom.translate(t);
g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
};
Anyone can help me?
ADDITIONAL INFO
I think i should write more info.
My svg is defined as
this.svg = d3.select("#"+this.config.mapContainer).append("svg")
.attr("id","svgMap")
.attr("width",width)
.attr("height",height)
.style("background-color", "lightblue")
.append("g").attr('id','first_g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
//Adding Zoom Rect to svg
this.svg.append("rect")
.attr("id", "d3e_zoomArea")
.attr("class", "d3e_zoomArea")
.attr("x", -width / 2)
.attr("y", -height / 2)
.attr("width", width)
.attr("height", height)
.attr('onmousedown', 'return false');
//Insert g elementi in svg
g = this.svg.append("g").attr('id','g_pathContainer');
//Set Actual Map Right Projection
/ create a first guess for the projection
subunits = topojson.feature(map, map.objects[level]);
center = d3.geo.centroid(subunits);
projection = d3.geo.mercator()
.scale(width / 2 / Math.PI) //Crea la scala di visualizzazione
.center(center) //Centra la posizione sulla mappa
.translate([0, 0]);
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(subunits);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate([0, 0]);
path = path.projection(projection);
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([this.config.scaleRange.scaleMin , this.config.scaleRange.scaleMax])
.on("zoom", zoomed);
d3.select("#first_g").call(zoom);
JSFIDDLE added
I've created a jsfiddle (http://jsfiddle.net/z9S7y/) to reproduce my problem. I've applied some initial transformation to base projection in order to center and scale the map in relation to actual country.

Related

D3JS v5 Zoom - The transform value changes between zoom on button and mouse wheel

I'm working on a project using D3js V5 that takes the input of a CSV file and returns a Workflow diagram, everything is working fine besides on it's own.
My problem is when I fit the workflow, through a Fit Button, and after that I use the mouse wheel to zoom in or out it just jumps to the previous zoom and position settings.
After some research I've found that after D3v4+ that "Zoom behaviours no longer store the active zoom transform (i.e., the visible region; the scale and translate) internally. The zoom transform is now stored on any elements to which the zoom behaviour has been applied.(link)"
That being said I cannot make it work and that's why I'm asking for your help.
The code is has follows:
document.getElementById('fitBTN').addEventListener('click', zoomFit);
function zoomFit() {
let bounds = document.getElementById('svgContent').getBBox();
let parent = document.getElementById('svgContent').parentElement;
let fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
let width = bounds.width,
height = bounds.height;
let midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
let scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
let translate = [
fullWidth / 2 - scale * midX,
fullHeight / 2 - scale * midY
];
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
translate[0] +
',' +
translate[1] +
') scale(' +
scale +
')'
);
}
svg
.call(
zoom()
.on('zoom', () => {
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
currentEvent.transform.x * 0.125 +
',' +
currentEvent.transform.y * 0.125 +
') scale(' +
currentEvent.transform.k +
')'
);
})
.scaleExtent([0.1, 1])
)
.on('dblclick.zoom', false)
.on('wheel', function() {
currentEvent.preventDefault();
})
.on('wheel', false);
I believe that I have to had some variable that is able to read the state of the transform translate, but how?
So the answer provided by #AndrewReid has helped me resolve my issue and the corrected version of the code is here:
function zoomFit() {
let bounds = document.getElementById('svgContent').getBBox();
let parent = document.getElementById('svgContent').parentElement;
let fullWidth = parent.clientWidth,
fullHeight = parent.clientHeight;
let width = bounds.width,
height = bounds.height;
let midX = bounds.x + width / 2,
midY = bounds.y + height / 2;
if (width == 0 || height == 0) return; // nothing to fit
let scale = 0.95 / Math.max(width / fullWidth, height / fullHeight);
let translate = [
fullWidth / 2 - scale * midX,
fullHeight / 2 - scale * midY
];
document
.getElementById('svgContent')
.setAttribute(
'transform',
'translate(' +
translate[0] +
',' +
translate[1] +
') scale(' +
scale +
')'
);
// Create a zoom transform from d3.zoomIdentity
var transform = zoomIdentity
.scale(scale)
.translate(-translate[0], -translate[1]);
// Apply the zoom and trigger a zoom event:
svg.call(zoom().transform, transform);
}
var zooming = zoom()
.scaleExtent([0.1, 1])
.translateExtent([[0, 0], [840, 740]])
.on('zoom', zoomed);
svg.call(zooming);
function zoomed() {
svg
.attr('transform', currentEvent.transform)
.on('dblclick.zoom', false)
.on('wheel', function() {
currentEvent.preventDefault();
});
}
But another issue got in the way which is the panning function was lost.
I'll have to figure that one out and I'll update this answer has soon has the solutions is found.
Best regards,

D3.js: How to Center svg map after Drag, Zoom, or MouseMove Events

My map projection and display goes awry when users drag, move the map (see: https://realtimeceap.brc.tamus.edu).
For example (duplicate event):
1. Select a field condition from dropdownlist.
2. Select a State from dropdownlist.
3. Select a County from dropdownlist.
4. Drag the map or move the mousewheel.
5. Then, select another State from dropdownlist. The map is not centered at the middle of the svg element and the scale is off, instead of scale 1.
Appreciate any help.
I reset the map on selecting a state as follows:
function resetMap() {
svg = d3.select("#svgMap2");
var w = 728;
var h = 500;
var project = d3.geoAlbersUsa()
.scale(1000)
.translate([w / 2, h / 2]);
var t = project.translate(); // the projection's default translation
var scale = project.scale;
//reset all features to original scale
d3.select("#svgMap2").select("#counties").selectAll(".county")
.transition()
.duration(750)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + scale + ")");
}
Been working on this for a while and found the solution is so simple. I needed to reset the g element's transform attribute to null to redefine it. The id of my svg = svgMap2 and the id of my g element = counties. BTW, I'm using D3.js version 4.
function resetMap() {
var k = 1;
var t = [0, 0];
svg = d3.select("#svgMap2")
d3.select("#svgMap2").select("#counties").selectAll(".county").classed("active", false);
d3.select("#counties").attr("transform", null);
svg.select("#counties").selectAll(".county") //must use svg or it disables pan, zoom
.attr("width", width)
.attr("height", height)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + k + ")");
}

D3js projection issues when fitting to BBox

(My code is at the end)
My goal is to display a country map (provided in a topojson file) which automatically scale and translate to fit into an area and then display few dots on it, representing some cities (given their lat/long coordinates).
First part was easy. I found (don't remember if it was on SO or on bl.ocks.org) that we can use bounds to compute scale and translate. That works perfectly and my country adapt to its parent area.
First Question: Why the country doesn't behave the same if I scale/translate it with its transform attribute or with projection.scale().translate() ? I mean, when I use transform attribute the country adapts perfectly whereas projection.scale().translate() displays a small country in a corner.
Second part is displaying some cities on my map. My cities has coordinates (which are real ones) :
var cities = {
features: [
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [2.351828, 48.856578] // Longitude, Latitude
},
'properties':{}
},
{
'type':'Feature',
'geometry':{
'type':'Polygon',
'coordinates': [5.726945, 45.187778] // Longitude, Latitude
},
'properties':{}
},
};
When I try to apply scale and translate parameters (to adapt with my country which has been scaled and translated) either with projection.scale().translate() or with transform attribute my cities are far far away from where they should be.
Second Question: Why I cannot use same scale/translate parameters on country and cities ? How can I properly display my cities where they should be ?
function computeAutoFitParameters(bounds, width, height) {
var dx = bounds[1][0] - bounds[0][0];
var dy = bounds[1][1] - bounds[0][1];
var x = (bounds[0][0] + bounds[1][0]) / 2;
var y = (bounds[0][1] + bounds[1][1]) / 2;
var scale = 0.9 / Math.max(dx / width, dy / height);
var translate = [width / 2 - scale * x, height / 2 - scale * y];
return {
scale : scale,
translate: translate
};
}
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var scale = params.scale;
var translate = params.translate;
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + translate + ')scale(' + scale + ')');
svg.selectAll('circle')
.data(cities.features) // city is defined in the code above
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
)
.attr('r', '6px');
EDIT: I had removed too much code to simplify it. It's fixed now. The difference is that I have an array of cities to display rather than just one.
Thanks in advance.
I found out that I had to add null parameters to my projection. To sum up :
Create a minimal projection (and a path)
Apply null scale and translate parameters to the projection : projection.scale(1).translate([0, 0])
Compute real scale and translate parameters according to the bounding box
Display the country's map as before (no changes here)
Set computed scale and translate parameters to the projection : projection.scale(params.scale).translate(params.translate);
Draw the cities dots.
`
// element is the HTML area where the country has to fit.
var height = element.height();
var width = element.width();
var projection = d3.geo.miller();
var path = d3.geo.path().projection(projection);
projection.scale(1).translate([0, 0]) // This is new
// data is my country (a topojson file with BBox)
var topojsonCountry = topojson.feature(data, data.objects[country.id]).features;
var bounds = path.bounds(topojsonCountry[0]);
var params = computeAutoFitParameters(bounds, width, height);
var svg = d3.select(element[0]).append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');
svg.append('g')
.selectAll('path')
.data(topojsonCountry)
.enter()
.append('path')
.attr('d', path)
.attr('transform', 'translate(' + params.translate + ')scale(' + params.scale + ')');
projection.scale(params.scale).translate(params.translate); // This is new
svg.selectAll('circle')
.data(cities.features)
.enter()
.append('circle')
.attr('transform', function(d) {
return 'translate(' + projection(d.geometry.coordinates) + ')';
})
.attr('r', '6px')
.attr('fill', 'red');

D3 Geo Tile Baselayer Offset

I'm using d3.geo.tile() and have used it successfully before but this time the tile layer doesn't seem to draw at the same scale and translate as the point layer. The below code creates a map that pans and draws just fine, but draws the circles, which should be in the Mediterranean, in Africa. If I zoom in, it scales the tiles and circles just fine, it's as if my xy coordinates are off, but they aren't.
I get the feeling that it's actually drawing the base layer without offsetting and scaling it properly because it should be centering on the coordinates 12,42, but it's a great big mystery to me since this exact same code works fine in a different application.
If someone can spot some problem, or just a hint, that would help.
function createNewMap(){
width = 1200, height = 800;
var tile = d3.geo.tile()
.size([1200, 800]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 10, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#newMapId").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var raster = svg.append("g");
var vector = svg.append("g");
vector.selectAll("g").data(dataModule.polisData).enter().append("g")
.attr("class", "sites")
.attr("transform", function(d) {return "translate(" + (projection([d.xcoord,d.ycoord])[0]) + "," + (projection([d.xcoord,d.ycoord])[1]) + ")scale("+(projection.scale())+")"})
.append("circle")
.attr("class", "sitecirc");
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 10 / zoom.scale());
}
Your code appears to be based on my example that changes the SVG transform on zoom. Changing the transform is a nice technique when you have complex geometry that you just want to scale and translate when you pan or zoom — it’s typically faster than reprojecting everything — but it’s also more complex than changing the projection on zoom.
The code doesn’t change very much if you want to change the projection on zoom. In essence:
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
And then re-run your d3.geo.path to re-render. As shown in bl.ocks.org/9535021:
Also, fixing the projection and changing the transform can cause precision problems if you zoom in a lot. Another reason to only use that technique when it offers substantial performance gains by avoid reprojection. And here reprojecting is super-cheap because it’s just a handful of points.

D3: What projection am I using? / How to simplify with a null projection?

I am attempting to simplify a d3 map on zoom, and I am using this example as a starting point. However, when I replace the json file in the example with my own (http://weather-bell.com/res/nws_regions.topojson), I get a tiny upside-down little map.
Here is my jsfiddle: http://jsfiddle.net/8ejmH
code:
var width = 900,
height = 500;
var chesapeake = [-75.959, 38.250];
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var simplify = d3.geo.transform({
point: function (x, y, z) {
if (z < visibleArea) return;
x = x * scale + translate[0];
y = y * scale + translate[1];
if (x >= 0 && x <= width && y >= 0 && y <= height || z >= invisibleArea) this.stream.point(x, y);
}
});
var zoom = d3.behavior.zoom()
.size([width, height])
.on("zoom", zoomed);
// This projection is baked into the TopoJSON file,
// but is used here to compute the desired zoom translate.
var projection = d3.geo.mercator().translate([0, 0])
var canvas = d3.select("#map").append("canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(simplify)
.context(context);
d3.json("http://weather-bell.com/res/nws_regions.topojson", function (error, json) {
canvas.datum(topojson.mesh(topojson.presimplify(json)))
.call(zoomTo(chesapeake, 0.05).event)
.transition()
.duration(5000)
.each(jump);
});
function zoomTo(location, scale) {
var point = projection(location);
return zoom.translate([width / 2 - point[0] * scale, height / 2 - point[1] * scale])
.scale(scale);
}
function zoomed(d) {
translate = zoom.translate();
scale = zoom.scale();
visibleArea = 1 / scale / scale;
invisibleArea = 200 * visibleArea;
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(chesapeake, 100).event)
.transition()
.call(zoomTo(chesapeake, 0.05).event)
.each("end", repeat);
})();
}
My guess is that the topojson file I am using already has the projection built in, so I should be using a null projection in d3.
The map renders properly if I do not use a projection at all: (http://jsfiddle.net/KQfrK/1/) - but then I cannot simplify on zoom.
I feel like I am missing something basic... perhaps I just need to somehow rotate and zoom into the map in my first fiddle.
Either way, I'd appreciate some help. Been struggling with this one.
Edit: I used QGIS to save the geojson file with a "EPSG:3857 - WGS 84 / Pseudo Mercator" projection.
However, when I convert this to topojson with the topojson command-line utility and then display it with D3 using the same code as above I get a blank screen.
Should I specify the projection within the topojson command-line utility? I tried to do that but I got an error message:
topojson --projection EPSG:3857 E:\gitstore\public\res\nws.geojson -o E:\gitstore\public\res\nws.topojson --id-property NAME
[SyntaxError: Unexpected token :]
The TopoJSON file doesn't have a projection built-in, you're simply using the default projection when you don't specify one (which is albersUsa, see the documentation). You can retrieve this projection by calling d3.geo.projection() without an argument. Then you can modify this projection in the usual way for zoom etc.
I set up this fiddle using the Mercator projection and I took a different approach to zooming in and out based on this block, which to me was a simpler approach. I have a feeling that there was an issue in the zoomTo function in the translate bit, but I could exactly what it was. So I replaced with the code below and included a recursive call:
function clicked(k) {
if (typeof k === 'undefined') k = 8;
g.transition()
.duration(5000)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -projection(chesapeake)[0] + "," + -projection(chesapeake)[1] + ")")
.each("end", function () {
(k === 8) ? k = 1 : k = 8;
clicked(k);
});

Resources