Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

plotly automatic zooming for "Mapbox maps"

In the plotly website Map Configuration and Styling in Python is described how to automatically zoom a "Geo map":

import plotly.express as px

fig = px.line_geo(lat=[0,15,20,35], lon=[5,10,25,30])            # Creates a "Geo map" figure
fig.update_geos(fitbounds="locations")                           # Automatic Zooming !!!!
fig.show()

and this works, moreover if I try to the same on a "Mapbox map" it does not apply auto zoom:

fig = px.scatter_mapbox(filtered_df, lat="latitude", lon="longitude", color="ID")  # Creates a "Mapbox map" figure
fig.update_layout(mapbox_style="open-street-map")
fig.update_geos(fitbounds="locations")                                             # Automatic Zooming not working!!!

There is not information of how to do it in the Mapbox Map Layers in Python.

like image 560
Antonio Ramírez Avatar asked Sep 08 '20 05:09

Antonio Ramírez


2 Answers

I wrote my own function along with other geojson compatible functions in rv_geojson.py

It takes a list of locations and finds the geometric height and width of the rectangular binding box, good for using with mercator projection. It returns zoom and center.

def zoom_center(lons: tuple=None, lats: tuple=None, lonlats: tuple=None,
        format: str='lonlat', projection: str='mercator',
        width_to_height: float=2.0) -> (float, dict):
    """Finds optimal zoom and centering for a plotly mapbox.
    Must be passed (lons & lats) or lonlats.
    Temporary solution awaiting official implementation, see:
    https://github.com/plotly/plotly.js/issues/3434
    
    Parameters
    --------
    lons: tuple, optional, longitude component of each location
    lats: tuple, optional, latitude component of each location
    lonlats: tuple, optional, gps locations
    format: str, specifying the order of longitud and latitude dimensions,
        expected values: 'lonlat' or 'latlon', only used if passed lonlats
    projection: str, only accepting 'mercator' at the moment,
        raises `NotImplementedError` if other is passed
    width_to_height: float, expected ratio of final graph's with to height,
        used to select the constrained axis.
    
    Returns
    --------
    zoom: float, from 1 to 20
    center: dict, gps position with 'lon' and 'lat' keys

    >>> print(zoom_center((-109.031387, -103.385460),
    ...     (25.587101, 31.784620)))
    (5.75, {'lon': -106.208423, 'lat': 28.685861})
    """
    if lons is None and lats is None:
        if isinstance(lonlats, tuple):
            lons, lats = zip(*lonlats)
        else:
            raise ValueError(
                'Must pass lons & lats or lonlats'
            )
    
    maxlon, minlon = max(lons), min(lons)
    maxlat, minlat = max(lats), min(lats)
    center = {
        'lon': round((maxlon + minlon) / 2, 6),
        'lat': round((maxlat + minlat) / 2, 6)
    }
    
    # longitudinal range by zoom level (20 to 1)
    # in degrees, if centered at equator
    lon_zoom_range = np.array([
        0.0007, 0.0014, 0.003, 0.006, 0.012, 0.024, 0.048, 0.096,
        0.192, 0.3712, 0.768, 1.536, 3.072, 6.144, 11.8784, 23.7568,
        47.5136, 98.304, 190.0544, 360.0
    ])
    
    if projection == 'mercator':
        margin = 1.2
        height = (maxlat - minlat) * margin * width_to_height
        width = (maxlon - minlon) * margin
        lon_zoom = np.interp(width , lon_zoom_range, range(20, 0, -1))
        lat_zoom = np.interp(height, lon_zoom_range, range(20, 0, -1))
        zoom = round(min(lon_zoom, lat_zoom), 2)
    else:
        raise NotImplementedError(
            f'{projection} projection is not implemented'
        )
    
    return zoom, center

Use it as

zoom, center = zoom_center(
    lons=[5, 10, 25, 30],
    lats=[0, 15, 20, 35]
)
fig = px.scatter_mapbox(
    filtered_df, lat="latitude", lon="longitude", color="ID",
    zoom=zoom, center=center
)  # Creates a "Mapbox map" figure

like image 90
RichieV Avatar answered Oct 26 '22 22:10

RichieV


The Mapbox API documentation shows that zooms are essentially on a log scale. So after some trial and error the following function worked for me:

max_bound = max(abs(x1-x2), abs(y1-y2)) * 111
zoom = 11.5 - np.log(max_bound)

Notes:

  • In this example, the xy (lon/lat) coordinates are in decimal degrees
  • The 111 is a constant to convert decimal degrees to kilometers
  • The value of 11.5 worked for my desired level of zoom/cropping, but I first experimented with values between 10-12
like image 23
Carl Cervone Avatar answered Oct 26 '22 22:10

Carl Cervone