I am currently trying to plot a few maps by using Plotly Express and Mapbox in Python.
I need to have a big map with the country and, next to it, an inset map with a “zoom” to a particular region (see Image attached as an example) INSET MAP EXAMPLE.
I managed to plot the bigger map (i.e. the whole country) but can’t find a proper way to create the inset map.
I tried to make 2 subplot (1 row, 2 cols), but did not work as expected. Also tried to plot two figures (one bigger, the whole country, and one smaller, the inset map) at the same time, but the two of them “collide” (the inset map turns out to be on top of the other one).
Any idea or possible turnaround for this?
Thank you.
Advertisement
Answer
- have used dash to enable callbacks from main choropeth to update inset choropeth (The way I have structured dash HTML means it is outset…)
- to bring it all together I have used some geo mapping data from UK government sources
- have just modified center of choropeth in callback. Could also modify zoom and/or range
get some geo data
import requests import geopandas as gpd import pandas as pd # https://ons-inspire.esriuk.com/arcgis/rest/services/Administrative_Boundaries/Local_Authority_Districts_December_2017_Boundaries/MapServer/3/query?where=1%3D1&outFields=*&outSR=4326&f=json # res = requests.get("https://opendata.arcgis.com/datasets/ae90afc385c04d869bc8cf8890bd1bcd_3.geojson") res = requests.get( "https://opendata.arcgis.com/datasets/69dc11c7386943b4ad8893c45648b1e1_0.geojson" ) # use 2019 so Buckinghamshire is included rescounties = requests.get( "https://opendata.arcgis.com/datasets/37363d379f4f40fa8c3f0d28eedfdd37_0.geojson" ) gdf = pd.concat( [ gpd.GeoDataFrame.from_features(res.json()["features"], crs="CRS84") .pipe(lambda d: d.rename(columns={c: c.lower() for c in d.columns}).drop(columns="lad20nmw")) .rename(columns={"lad20cd": "areaCode", "lad20nm": "areaName"}), gpd.GeoDataFrame.from_features(rescounties.json()["features"], crs="CRS84") .pipe(lambda d: d.rename(columns={c: c.lower() for c in d.columns})) .rename(columns={"cty19cd": "areaCode","cty20cd": "areaCode","cty19nm": "areaName","st_areashape": "shape__area", "st_lengthshape": "shape__length",}), ] ).set_index("areaCode").assign(country=lambda d: d.index.str[0])
build base figure
import plotly.express as px fig = px.choropleth_mapbox( gdf, geojson=gdf.geometry, locations=gdf.index, hover_name="areaName", color="country", color_discrete_sequence=["green", "yellow", "orange", "red", "magenta"], center={"lat": (gdf.total_bounds[1] + gdf.total_bounds[3])/2, "lon": (gdf.total_bounds[0] + gdf.total_bounds[2])/2}, mapbox_style="carto-positron", zoom=5, ).update_layout(margin={"l":0,"r":0,"b":0,"t":0})
dash app
import json import numpy as np import plotly.graph_objects as go import dash_core_components as dcc import dash_html_components as html import dash_bootstrap_components as dbc import dash_table from dash.dependencies import Input, Output, State from jupyter_dash import JupyterDash ifig = go.Figure(fig.data).update_layout(fig.layout).update_layout(showlegend=False) # Build App app = JupyterDash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) app.layout = html.Div( [ dbc.Row( [ dbc.Col( dcc.Graph(id="map", figure=fig, style={"height": "90vh"}), width=9, ), dbc.Col( dcc.Graph(id="insetmap", style={"height": "25vh"}), width=3, ), ] ), ], style={"font-family": "Arial", "font-size": "0.9em"}, ) @app.callback( Output("insetmap", "figure"), Input("map", "selectedData"), ) def mapSelect(selectData): global ifig if selectData and "range" in selectData.keys(): r = selectData["range"]["mapbox"] ifig = ifig.update_layout( mapbox={ "center": { "lon": (r[0][0] + r[1][0]) / 2, "lat": (r[0][1] + r[1][1]) / 2, } } ) print(ifig.layout["mapbox"]) return ifig return ifig # Run app and display result inline in the notebook app.run_server(mode="inline")