consistent zooming in deck.gl (or: how to zoom by 1 level)
written on 2025-04-27, last updated 2025-04-27
deck.gl is perhaps my favourite library when it comes to maps, or even data visualisation. it's super fast, very powerful, and nicely customisable.
it does, however, have its quirks. among these, one of them is the way scrollwheel zooming is controlled: it is impossible to tell the library that something along the lines of "on each scroll, increase / decrease the zoom level by 1".
indeed, its Controller class does have a scrollZoom.speed
option, but it does not directly correspond to zoom levels. instead, it plugs into the following formula:
let scale = 2 / (1 + Math.exp(-Math.abs(delta * speed)));
if (delta < 0 && scale !== 0) {
scale = 1 / scale;
}
here, delta
is calculated by mjolnir.js based on the deltaY
of the WheelEvent but with additional calculations to normalise the value across different scenarios. the exact calculations can be found here but they are not important for us: the main point is that this makes it impossible to normalise scroll wheel behaviour using scrollZoom.speed
due to the potential differences in deltaY
across browsers/devices.
additionally, the resulting scale
variable also behaves somewhat strangely: while you may expect that a value of +1 would increase the zoom level by 1, this is not the case at all. instead, if we examine the zoom function of the MapController
, we find the following code:
let zoom = (startZoom as number) + Math.log2(scale);
zoom = clamp(zoom, minZoom, maxZoom);
in other words, to increase the zoom level by 1, scale needs to be 2, and to decrease it by 1, it needs to be 0.5
with this knowledge, it's fairly easy to write a custom controller that gives us the desired results just by rewriting MapController's _onWheel
handler a bit:
import {MapController} from "deck.gl";
import type {MjolnirWheelEvent} from "mjolnir.js";
export default class CustomController extends MapController{
constructor(props: any){
super(props);
}
// Rewriting the onWheel event to guarantee a zoom by +/-1
protected _onWheel(event: MjolnirWheelEvent): boolean {
if (!this.scrollZoom){ // Scrollwheel zoom is not enabled
return false;
}
event.srcEvent.preventDefault();
const {smooth = false} = this.scrollZoom === true ? {} : this.scrollZoom;
const pos = this.getCenter(event);
if (!this.isPointInBounds(pos, event)) return false;
let scale = Math.pow(2, Math.sign(event.delta));
const newControllerState = this.controllerState.zoom({pos, scale});
this.updateViewport(
newControllerState,
{...this._getTransitionProps({around: pos}), transitionDuration: smooth ? 250 : 1},
{
isZooming: true,
isPanning: true
}
);
return true;
}
}
this came in quite handy for my quiz website, helloquiz. i will update this post with a link to the github repo when it goes open source :)