Synchronize Zoom/Pan for Two Mapbox Plots

I am trying to figure out how to synchronize zoom and pan levels between subplots. I have read the following:


I have not been able to get mapbox choropleth to work with subplots, and the open issue above makes me think this is not possible at the moment. Is there any kind of event return for plot zoom that I could use as an input to set the extents of the other map? If not, it would seem like a zoom in/out button would be required, but even then, I would need a way to set the limits of one or both map plots.

Thanks in advance for any help.

Hi @allenite,

You can create subplots of choroplethmapbox as follows:

from plotly.subplots import make_subplots
import plotly.graph_objects as go
import pandas as pd
import json
import urllib.request
mapboxt = open(".mapbox_token").read().rstrip() #my mapbox_access_token 
fig = make_subplots(
    rows=1, cols=2, subplot_titles=('Map1', 'Map2'),
    specs=[[{"type": "mapbox"}, {"type": "mapbox"}]]
)

swiss_url = 'https://raw.githubusercontent.com/empet/Datasets/master/swiss-cantons.geojson'
with urllib.request.urlopen(swiss_url) as url:
    jdata = json.loads(url.read().decode())

data_url = "https://raw.githubusercontent.com/empet/Datasets/master/Swiss-synthetic-data.csv"

df = pd.read_csv(data_url)

fig.add_trace(go.Choroplethmapbox(geojson=jdata, 
                                  locations=df['canton-id'], 
                                  z=df['2018'],
                                  featureidkey='properties.id',
                                  colorscale='Viridis',
                                  colorbar=dict(thickness=20, x=0.46),
                                  marker=dict(opacity=0.75)), row=1, col=1)
fig.add_trace(go.Choroplethmapbox(geojson=jdata, 
                                  locations=df['canton-id'], 
                                  z=df['2019'],
                                  featureidkey='properties.id',
                                  colorscale='matter_r',
                                  colorbar=dict(thickness=20, x=1.02),
                                  marker=dict(opacity=0.75, line_width=0.5)), row=1, col=2);


fig.update_mapboxes(
        bearing=0,
        accesstoken=mapboxt,
        center = {"lat": 46.8181877 , "lon":8.2275124 },
 )
fig.update_layout(margin=dict(l=0, r=0, t=50, b=10));

#HERE YOU CAN CONTROL zoom
fig.update_layout(mapbox1=dict(zoom=5.9, style='carto-positron'),
                  mapbox2=dict(zoom=5.3, style='light'))

For mapbox maps you cannot set the range for lon and lat (I suppose that you are referring to range when you are talking about extent). You can control the map extent by setting its center and zoom in fig.layout.mapbox.

I tried to define some buttons that set the zoom parameter for each subplot, but there is an issue that must be opened on plotly.js , because only the mapbox2.zoomis updated:

button1= dict(method = "relayout",
              args = [{"mapbox1.zoom": 5.9,
                       "mapbox2.zoom": 5.3}], 
              label = "zoom1=5.9<br>zoom2=5.3"
           )
button2= dict(method = "relayout",
              args=[{"mapbox1.zoom": 4.75,
                     "mapbox2.zoom": 6.2}], 
              label="zoom1=4.75<br>zoom2=6.2"
             )

button3= dict(method = "relayout",
              args=[{"mapbox1.zoom": 4.,
                     "mapbox2.zoom": 6.35}], 
              label="zoom1=4<br>zoom2=6.35"
             )

fig.update_layout(updatemenus=[dict(active=0,
                                    buttons=[button1, button2, button3],
                                x=0.99, y=0.99, xanchor='right', yanchor='top')
                              ])

empet,

Thanks so much! A few questions:

Can you use go.Choroplethmapbox and specify the mapbox base to be OSM (so this works without a mapbox key, for example)?

I really like the idea of using zoom buttons to control the zoom and center. I assume there is no callback for the pan/zoom action on a map that can be used to update the other map, in addition to a zoom button?

Hi @allenite

The mapbox style 'carto-positron' does not require the access token. Hence replacing:

  mapbox2=dict(zoom=5.3, style='light')

by

  mapbox2=dict(zoom=5.3, style='carto-positron')

and commenting or removing the line:

accesstoken=mapboxt

works without access token.

Hi @empet,

is there a way to have a figure_facrtory.create_hexbin_mapbox() in fig.add_trace() ?

Hi @lorenzo,

I cannot answer your question because I don’t know, what your fig constains. If fig has as data a choroplethmapbox, then if you define

import plotly.figure_factory as ff
import plotly.express as px

px.set_mapbox_access_token(open(".mapbox_token").read())
df = px.data.carshare()

fighex = ff.create_hexbin_mapbox(
    data_frame=df, lat="centroid_lat", lon="centroid_lon", color_continuous_scale='ice_r',
    nx_hexagon=18, opacity=0.8, labels={"color": "n_cars"},
    min_count=1,  original_data_marker=dict(marker_size=0, marker_line_width=0)
)
fighex.update_traces(marker_line_width=0.5)
fighex.update_mapboxes(style='stamen-terrain')

then fighex.data[0] is also a choroplethmapbox and in this case

fig.add_trace(fighex.data[0])

will cover the former choroplethmapbox.

Many thanks, @empet ,

I finally also figured almost the same way. I need to loop on the fig.data since the number of traces can vary upon the filter imposed by dash controls.

for i in range(len(fig.data)):           
            fig.add_trace(fig.data[i])