Dynamic Zoom for mapbox

Hello I noticed a couple posts on this subject that got no responses but I think it would be a great feature. I don’t think plotly or dash have an autozoom feature for mapboxes, as I’ve been looking for a while. But does anyone have any ideas on how I can implement this on my own, maybe creating a function that I can use to determine the appropriate zoom level on graph creation?

Hey,

im one of them that got no response :smiley:
But my solution for this was to just calculate the zoom level with the screen size. I created a small react component which gave me the screen size and used that to calculate the zoom. There are a lot of react screen size components out there easy to use. This way at least on page load, the map is at the zoom its supposed to be. For my use-case i didnt need any dynamic zooming on screen resizing.

Ah okay! I’m not sure that will work for my case though, since I could have any number of points on the map ranging from zero to nine thousand. I would like the map to automatically fit all the points on the screen with the least amount of extra space. So say I go from 500 points to 15, I want the map to zoom to those 15 points as close as possible while still making sure all 15 points are visible to the user.

Edit: Right now I’m attempting something a little different. I’m creating a bounding box around my points, calculating the area and attempting to map the area to zoom levels. Its not consistent now, but if anyone has any pointers on how best to go about this I would appreciate it. Also here is the function:

def determine_zoom_level(latitudes, longitudes):
    all_pairs=[]
    for lon, lat in zip(longitudes, latitudes):
        all_pairs.append((lon,lat))
    b_box = planar.BoundingBox(all_pairs)
    if b_box.is_empty:
        return 100, (-81.90, 41.2606)
    area = b_box.height * b_box.width
    zoom = numpy.interp(area, [0,.000004],[0, 20])
    print(zoom)
    return zoom, b_box.center

We’re working on adding this to the core library but it might be a few weeks before it’s available :slight_smile:

I don’t have a great workaround to propose in the meantime though…

2 Likes

Hey that is great to hear! This interpolating business is giving me a bit of headache. There is a lot of variance in the size of these bounding boxes.

So here is my solution for my use case. Of course this may not work for everyone but I’m going to post it here in case someone finds it useful in their project. Once the dash team implements autozoom into the default behavior of a mapbox, this will be obsolete, and I am sure looking forward to that!

# THIS IS A TEMPORARY SOLUTION UNTIL THE DASH TEAM IMPLEMENTS DYNAMIC ZOOM
def determine_zoom_level(latitudes, longitudes):
    all_pairs=[]
    if not latitudes or not longitudes:
        return 0, (0,0)
    for lon, lat in zip(longitudes, latitudes):
        all_pairs.append((lon,lat))
    b_box = planar.BoundingBox(all_pairs)
    if b_box.is_empty:
        return 0, (0, 0)
    area = b_box.height * b_box.width
    zoom = numpy.interp(area, [0, 5**-10, 4**-10, 3**-10, 2**-10, 1**-10, 1**-5], 
                              [20, 17,   16,     15,     14,     7,      5])
    print(zoom)
    return zoom, b_box.center

I wanted to test your solution but got problems with the planar module that wont install via pip. Any clue why?

Not sure. I’m pretty sure I just did pip3 install planar and it installed okay. Also fair warning, that function there works okay for my data set, but you may need to mess with the values in the numpy.interp() function to get it to work for your data.

Thanks @Krichardson for your nice workaround!
I’ve implemented it in my workflow, here is my upgraded version of your function which might help some other fellows:

def get_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/7

    # 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 function
    if lonlat_pairs is None:
        # Check whether both latitudes and longitudes have been passed,
        # or if the list lenghts don't match
        if ((latitudes is None or longitudes is None)
                or (len(latitudes) != len(longitudes))):
            # Otherwise, return the default values of 0 zoom and the coordinate origin as center point
            return 0, (0, 0)

        # Instantiate collator list for all coordinate-tuples
        lonlat_pairs = [(longitudes[i], latitudes[i]) for i in range(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 well
    if b_box.is_empty:
        return 0, (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 zpom-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 coordinates
    return zoom, b_box.center

As for the installation of the planar package:
(@seferoezcan)
I’ve installed it both via
pip3 install planar
and
pip install planar
since it was not available on anaconda, which I normally use.

The latter installation worked well via pip, as for some reason my pip3-installations cannot be found when trying to import the package installed with pip3 in particular (Lubuntu 18.04 - user).

2 Likes

Considering very elongated polygons, i would recommand to use the len of the longest line in the polygon and use the scale (https://wiki.openstreetmap.org/wiki/Zoom_levels)

    zoom = np.interp(x=len,
                     xp=[0.00025, 0.0005, 0.001, 0.003, 0.005, 0.011, 0.022, 0.044, 0.088, 0.176, 0.352, 0.703, 1.406,
                         2.813, 5.625,11.25, 22.5, 45],
                     fp=[20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3])
1 Like

@nicolaskruchten has there been any progress on implementing dynamic zoom in Plotly?

2 Likes

Andy’s excellent function had the insight to simply calculate the zoomlevel from appropriate scaling of the desired area. I adapted it (see below) to

  • not use the planar library, just calculate from latitudes and longitudes series and use a dict
  • alter the default zoom level a bit to zoom out from his defaults (end user may wish to do zoom levels + or - based on what they see)
def get_plotting_zoom_level_and_center_coordinates_from_lonlat_tuples(longitudes=None, latitudes=None):
    """Function documentation:\n
    Basic framework adopted from Krichardson under the following thread:
    https://community.plotly.com/t/dynamic-zoom-for-mapbox/32658/7

    # 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 both latitudes and longitudes have been passed,
    # or if the list lenghts don't match
    if ((latitudes is None or longitudes is None)
            or (len(latitudes) != len(longitudes))):
        # Otherwise, return the default values of 0 zoom and the coordinate origin as center point
        return 0, (0, 0)

    # Get the boundary-box 
    b_box = {} 
    b_box['height'] = latitudes.max()-latitudes.min()
    b_box['width'] = longitudes.max()-longitudes.min()
    b_box['center']= (np.mean(longitudes), np.mean(latitudes))

    # 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 zpom-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, 15,    14,     13,     12,     7,      5])

    # Finally, return the zoom level and the associated boundary-box center coordinates
    return zoom, b_box['center']
3 Likes