Skip to content

Commit 9575a26

Browse files
committed
Fixed the issue where panning the map would suddenly skip or jump back.
1 parent d753eba commit 9575a26

File tree

1 file changed

+67
-27
lines changed

1 file changed

+67
-27
lines changed

resources/js/charts.js

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -199,70 +199,110 @@ function WorldMap() {
199199

200200
function setBounds(projection, maxLat) {
201201
const [yaw] = projection.rotate();
202-
// Top left corner
202+
// Top-left corner of the “viewable” lat
203203
const xymax = projection([-yaw + 180 - 1e-6, -maxLat]);
204-
// Bottom right corner
204+
// Bottom-right corner
205205
const xymin = projection([-yaw - 180 + 1e-6, maxLat]);
206-
return [xymin, xymax];
206+
return [xymin, xymax]; // [ [xMin, yMin], [xMax, yMax] ]
207+
}
208+
209+
function clampVertical(projection, maxLat, height) {
210+
const b = setBounds(projection, maxLat);
211+
const [xMin, yMin] = b[0]; // top-left
212+
const [xMax, yMax] = b[1]; // bottom-right
213+
214+
// Current translation
215+
const t = projection.translate();
216+
217+
// If the top (yMin) is now below 0, push it down.
218+
if (yMin > 0) {
219+
t[1] -= yMin; // shift map upward
220+
}
221+
// If the bottom (yMax) is now above the container height, push it up.
222+
else if (yMax < height) {
223+
t[1] += (height - yMax); // shift map downward
224+
}
225+
226+
projection.translate(t);
207227
}
208228

209229
function zoomed(event, projection, path, scaleExtent, g) {
210-
const newX = event.transform.x % width;
211-
const newY = event.transform.y;
212230
const scale = event.transform.k;
231+
const containerCenter = [innerW() / 2, height / 2]; // Use the center of the container
232+
let anchor = containerCenter; // Always use the container's center as the anchor
233+
234+
if (Math.abs(scale - slast) > 1e-3) {
235+
// Get the geographic coordinate at the container center
236+
const geoCoord = projection.invert(anchor);
213237

214-
if (scale != slast) {
215-
// Adjust the scale of the projection based on the zoom level
238+
// Update projection scale based on the new zoom factor
216239
projection.scale(scale * (innerW() / (2 * Math.PI)));
240+
241+
// Re-project the same geographic coordinate to determine its new position
242+
const newPixel = projection(geoCoord);
243+
const dxAnchor = anchor[0] - newPixel[0];
244+
const dyAnchor = anchor[1] - newPixel[1];
245+
246+
// Shift translation so that the geoCoord remains at the chosen anchor
247+
const t = projection.translate();
248+
projection.translate([t[0] + dxAnchor, t[1] + dyAnchor]);
249+
250+
// Clamp vertical so the map doesn't float off the top/bottom
251+
clampVertical(projection, maxLat, height);
217252
} else {
218-
// Calculate the new longitude based on the x-coordinate
219-
let [longitude] = projection.rotate();
253+
// --- Pure Panning (scale unchanged) ---
254+
const newX = event.transform.x;
255+
const newY = event.transform.y;
256+
const dx = newX - tlast[0];
257+
const dy = newY - tlast[1];
220258

221-
// Use the X translation to rotate, based on the current scale
222-
longitude += 360 * ((newX - tlast[0]) / width) * (scaleExtent[0] / scale);
259+
// Infinite horizontal scroll: adjust rotation (longitude)
260+
let [longitude] = projection.rotate();
261+
longitude += 360 * (dx / width) * (scaleExtent[0] / scale);
223262
projection.rotate([longitude, 0, 0]);
224263

225-
// Calculate the new latitude based on the y-coordinate
226-
const b = setBounds(projection, maxLat);
227-
let dy = newY - tlast[1];
228-
if (b[0][1] + dy > 0)
229-
dy = -b[0][1];
230-
else if (b[1][1] + dy < height)
231-
dy = height - b[1][1];
232-
projection.translate([projection.translate()[0], projection.translate()[1] + dy]);
264+
// Vertical shift: adjust translation
265+
const t = projection.translate();
266+
t[1] += dy;
267+
projection.translate(t);
268+
269+
// Clamp vertical
270+
clampVertical(projection, maxLat, height);
233271
}
234272

235-
// Redraw paths with the updated projection
273+
// Redraw paths
236274
g.selectAll('path').attr('d', path);
237275

238-
// Save last values
276+
// Save state for next event
277+
tlast = [event.transform.x, event.transform.y];
239278
slast = scale;
240-
tlast = [newX, newY];
241279
}
242280

243281
function createSVG(selection) {
244282
const svg = d3.select(selection)
245283
.append('svg')
246284
.attr('class', 'map')
247-
.attr('width', width)
285+
.attr('style', 'display:block; margin:auto;')
286+
.attr('width', innerW())
248287
.attr('height', height)
249288
.lower();
250289

251290
const g = svg.append('g')
252-
.attr('transform', `translate(${margin.left}, 0)`)
291+
.attr('transform', `translate(0, 0)`)
253292
.attr('transform-origin', '50% 50%');
254293

255294
projection = d3.geoMercator()
256295
.center([0, 15])
257-
.scale([(innerW()) / (2 * Math.PI)])
296+
.scale((innerW()) / (2 * Math.PI))
258297
.translate([(innerW()) / 2, height / 1.5]);
298+
259299
path = d3.geoPath().projection(projection);
260300

261301
// Calculate scale extent and initial scale
262302
const bounds = setBounds(projection, maxLat);
263-
const s = width / (bounds[1][0] - bounds[0][0]);
303+
const s = innerW() / (bounds[1][0] - bounds[0][0]);
264304
// The minimum and maximum zoom scales
265-
const scaleExtent = [s, 5 * s];
305+
const scaleExtent = [s, 6 * s];
266306

267307
const zoom = d3.zoom()
268308
.scaleExtent(scaleExtent)

0 commit comments

Comments
 (0)