Skip to content Skip to sidebar Skip to footer

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], l

Solution 1:

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.

defzoom_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 isNoneand lats isNone:
        ifisinstance(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

Solution 2:

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)) * 111zoom = 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

Solution 3:

Based on this question on plotly.com with the first version of the function below I came up with the following final solution:

defget_plotting_zoom_level_and_center_coordinates_from_lonlat_tuples(
        longitudes=None, latitudes=None, lonlat_pairs=None):
    """Function documentation:\n
    Basic framework adopted from Krichardson under the following thread:
    https://community.plotly.com/t/dynamic-zoom-for-mapbox/32658/6

    # NOTE:
    # THIS IS A TEMPORARY SOLUTION UNTIL THE DASH TEAM IMPLEMENTS DYNAMIC ZOOM
    # in their plotly-functions associated with mapbox, such as go.Densitymapbox() etc.

    Returns the appropriate zoom-level for these plotly-mapbox-graphics along with
    the center coordinate tuple of all provided coordinate tuples.
    """# Check whether the list hasn't already be prepared outside this functionif lonlat_pairs isNone:
        # Check whether both latitudes and longitudes have been passed,# or if the list lenghts don't matchif ((latitudes isNoneor longitudes isNone)
                or (len(latitudes) != len(longitudes))):
            # Otherwise, return the default values of 0 zoom and the coordinate origin as center pointreturn0, (0, 0)

        # Instantiate collator list for all coordinate-tuples
        lonlat_pairs = [(longitudes[i], latitudes[i]) for i inrange(len(longitudes))]

    # Get the boundary-box via the planar-module
    b_box = planar.BoundingBox(lonlat_pairs)

    # In case the resulting b_box is empty, return the default 0-values as wellif b_box.is_empty:
        return0, (0, 0)

    # Otherwise, get the area of the bounding box in order to calculate a zoom-level
    area = b_box.height * b_box.width

    # * 1D-linear interpolation with numpy:# - Pass the area as the only x-value and not as a list, in order to return a scalar as well# - The x-points "xp" should be in parts in comparable order of magnitude of the given area# - The zoom-levels are adapted to the areas, i.e. start with the smallest area possible of 0# which leads to the highest possible zoom value 20, and so forth decreasing with increasing areas# as these variables are antiproportional
    zoom = np.interp(x=area,
                     xp=[0, 5**-10, 4**-10, 3**-10, 2**-10, 1**-10, 1**-5],
                     fp=[20, 17, 16, 15, 14, 7, 5])

    # Finally, return the zoom level and the associated boundary-box center coordinatesreturn zoom, b_box.center

Post a Comment for "Plotly Automatic Zooming For "mapbox Maps""