Map View and Zoom in JavaScript

How to control the initial view of maps in JavaScript. Examples of auto-fitting a map to its data, setting an explicit view, and limiting how far users can zoom.


Plotly Studio: Transform any dataset into an interactive data application in minutes with AI. Try Plotly Studio now.

Since Plotly.js v4, both outline-based geo subplots and tile-based map subplots auto-fit their initial view to the data they plot. Before v4, both opened on a default world view and you had to supply a view yourself to frame your data.

Auto-fitting is controlled by fitbounds:

  • layout.geo.fitbounds defaults to 'locations' and also accepts 'geojson' (to frame the whole GeoJSON feature set rather than just the visible locations) and false.
  • layout.map.fitbounds defaults to 'locations' and accepts false. choroplethmap traces do not participate in auto-fitting, so a subplot containing one opens on the default view.

Supplying your own view attributes turns auto-fitting off for that subplot. On a tile map that means map.center or map.zoom. On an outline map, which attributes count depends on the subplot:

SubplotAttributes that turn auto-fitting off
Any scope other than 'world' center.lon, center.lat, projection.scale
'world' scope, clipped projection (orthographic, conic conformal, stereographic, azimuthal, …) the above, plus projection.rotation.lon, projection.rotation.lat, lonaxis.range, lataxis.range
'world' scope, unclipped projection (equirectangular, mercator, robinson, …) the above, plus projection.rotation.lon, lonaxis.range, lataxis.range

Note the first row: on a scoped subplot whose projection is clipped, lonaxis.range and lataxis.range do not turn auto-fitting off — they still clip the drawing region, but the view is centered on the data. Set fitbounds: false to honor the ranges as the view.

Setting fitbounds: false without supplying a view restores the pre-v4 default view. The 'albers usa', 'craig', and 'satellite' projections never auto-fit.

With no center or zoom given, a map subplot frames the trace data on the first render. It re-fits when the data changes — through Plotly.restyle of lon/lat, Plotly.addTraces, or Plotly.deleteTraces — until the user pans, zooms, rotates, or tilts the map, after which the chosen view is preserved. Data that straddles the antimeridian is framed the short way around, not across the whole globe.

var data = [{
  type: 'scattermap',
  mode: 'markers+text',
  lon: [-122.4, -73.9, -87.6],
  lat: [37.8, 40.7, 41.9],
  text: ['San Francisco', 'New York', 'Chicago'],
  textposition: 'top right',
  marker: {size: 12, color: '#d62728'}
}];

var layout = {
  map: {style: 'basic'},
  showlegend: false,
  width: 700,
  height: 450,
  margin: {t: 30, b: 0, l: 0, r: 0}
};

Plotly.newPlot('myDiv', data, layout);

Passing map.center or map.zoom opts the subplot out of auto-fitting and opens it on exactly the view you give, even if that view is the default zoom: 1 centered on (0, 0). Setting map.fitbounds: false without a view has the same effect as the pre-v4 default: a world view at zoom: 1.

map.bounds, which restricts how far users can pan, is independent of auto-fitting and can be combined with either.

var data = [{
  type: 'scattermap',
  mode: 'markers+text',
  lon: [-122.4, -73.9, -87.6],
  lat: [37.8, 40.7, 41.9],
  text: ['San Francisco', 'New York', 'Chicago'],
  textposition: 'top right',
  marker: {size: 12, color: '#d62728'}
}];

var layout = {
  map: {
    style: 'basic',
    center: {lon: -98, lat: 39},
    zoom: 2.5
  },
  showlegend: false,
  width: 700,
  height: 450,
  margin: {t: 30, b: 0, l: 0, r: 0}
};

Plotly.newPlot('myDiv', data, layout);

layout.geo.fitbounds defaults to 'locations', so an outline map frames the locations it plots rather than the whole world. Set fitbounds: false to get the pre-v4 world view, or supply your own view attributes — center and projection.scale always turn auto-fitting off, and projection.rotation, lonaxis.range, and lataxis.range do too except on a scoped subplot with a clipped projection. See Map View and Zoom for the full breakdown.

When fitbounds is active, the modebar's Reset view button returns to the fitted view rather than the world view.

Note that a location whose territory crosses the ±180° antimeridian — the United States (the Aleutian Islands), Russia, Fiji, New Zealand, and Antarctica — spans most of the globe's longitude, so a figure that includes one is fitted very wide. Set the view explicitly in that case.

var data = [{
  type: 'choropleth',
  locations: ['BRA', 'ARG', 'CHL', 'PER', 'COL', 'BOL'],
  locationmode: 'ISO-3',
  z: [6, 5, 4, 3, 2, 1],
  colorscale: 'Blues',
  showscale: false
}];

var layout = {
  title: {text: 'Fitted to the Plotted Locations'},
  geo: {
    projection: {type: 'equirectangular'},
    showcountries: true,
    countrycolor: 'rgb(255, 255, 255)'
  },
  width: 700,
  height: 450
};

Plotly.newPlot('myDiv', data, layout);

projection.minscale and projection.maxscale, new in Plotly.js v4, clamp how far users can zoom a geo subplot. Both are multipliers of projection.scale: minscale: 0.5 stops zooming out past half the base scale and maxscale: 4 stops zooming in past four times it. The defaults — minscale: 0 and no maxscale — leave zoom unrestricted.

Try scrolling on the map below: it will not zoom out below half, or in beyond four times, its initial scale.

If the initial projection.scale falls outside the range you set, Plotly clamps it on the first render by dispatching a zoom event, which emits one extra plotly_relayout. Listeners that distinguish user zooms from programmatic ones should tolerate that single event at startup.

var data = [{
  type: 'scattergeo',
  mode: 'markers+text',
  lon: [2.35, 13.4, 12.5, -3.7],
  lat: [48.86, 52.52, 41.9, 40.42],
  text: ['Paris', 'Berlin', 'Rome', 'Madrid'],
  textposition: 'top center',
  marker: {size: 10, color: '#636efa'}
}];

var layout = {
  title: {text: 'Zoom Limited to 0.5x - 4x'},
  geo: {
    scope: 'europe',
    resolution: 50,
    showland: true,
    landcolor: 'rgb(235, 235, 235)',
    projection: {
      scale: 1,
      minscale: 0.5,
      maxscale: 4
    }
  },
  showlegend: false,
  width: 700,
  height: 500
};

Plotly.newPlot('myDiv', data, layout);