6

I am trying to add a Scattergeo trace or overlay on top of a white-bg density mapbox to get a heat map over a generic USA states outline.

The reason for my use of scattergeo is I'd like to plot a star symbol on top of the density mapbox, and the only symbol accepted via add_scattermapbox is a dot. If you choose the star symbol, there is no symbol added.

I'm also aware that star symbols are acceptable for the p mapbox_styles of add_scattermapbox or density_scattermapbox but at the present time I am not in the position to pay per web load after the trial amount runs out.

Is there a clever way to add a star symbol on top of a density_mapbox plot?

Working ScatterGeo

fig = go.Figure(go.Scattergeo())

fig.add_scattergeo(lat = [30, 40]
                      ,lon = [-90, -80]
                      ,hoverinfo = 'none'
                      ,marker_size = 10
                      ,marker_color = 'rgb(65, 105, 225)' # blue
                      ,marker_symbol = 'star'
                      ,showlegend = False
                     )

fig.update_geos(
    visible=False, resolution=110, scope="usa",
    showcountries=True, countrycolor="Black",
    showsubunits=True, subunitcolor="Black"
)

fig.show()

add picture here

Working Density Mapbox

d = {'Location': ['Point A', 'Point B'], 'lat': [30, 40], 'long': [-90, -80], 'z': [100,200]}

df = pd.DataFrame(data=d)

fig = px.density_mapbox(df
                        ,lat='lat'
                        ,lon='long'
                        ,z='z'
                        ,hover_name='Location'
                        ,center=dict(lat=38.5, lon=-96)
                        ,range_color = [0, 200]
                        ,zoom=2
                        ,radius=50
                        ,opacity=.5
                        ,mapbox_style='open-street-map')

fig.add_scattermapbox(lat = [30, 40]
                      ,lon = [-90, -80]
                      ,hoverinfo = 'none'
                      ,marker_size = 6
                      ,marker_color = 'rgb(0, 0, 0)'
#                       ,marker_symbol = 'star'
                      ,showlegend = False
                     )

fig.show()

insert 2nd picture here



Attempt #1 - Just set marker_symbol = 'star'

Un-commenting the marker_symbol = 'star', which would work for the premium styles of mapbox, completely removes the scatter point.

d = {'Location': ['Point A', 'Point B'], 'lat': [30, 40], 'long': [-90, -80], 'z': [100,200]}

df = pd.DataFrame(data=d)

fig = px.density_mapbox(df
                        ,lat='lat'
                        ,lon='long'
                        ,z='z'
                        ,hover_name='Location'
                        ,center=dict(lat=38.5, lon=-96)
                        ,range_color = [0, 200]
                        ,zoom=2
                        ,radius=50
                        ,opacity=.5
                        ,mapbox_style='open-street-map')

fig.add_scattermapbox(lat = [30, 40]
                      ,lon = [-90, -80]
                      ,hoverinfo = 'none'
                      ,marker_size = 6
                      ,marker_color = 'rgb(0, 0, 0)'
                      ,marker_symbol = 'star'
                      ,showlegend = False
                     )

fig.show()

insert 3rd picture here

Attempt #2 - Adding a density mapbox on top of the scatter geo

Adding a density_mapbox on top of the scattergeo produces the same geo plot, but nothing more. The density mapbox legend is there, but no heat map.

d = {'Location': ['Point A', 'Point B'], 'lat': [30, 40], 'long': [-90, -80], 'z': [100,200]}

df = pd.DataFrame(data=d)

fig = go.Figure(go.Scattergeo())

fig.add_scattergeo(lat = [30, 40]
                      ,lon = [-90, -80]
                      ,hoverinfo = 'none'
                      ,marker_size = 10
                      ,marker_color = 'rgb(65, 105, 225)' # blue
                      ,marker_symbol = 'star'
                      ,showlegend = False
                     )

fig.add_densitymapbox(lat=df['lat'],
                     lon=df['long'],
                      z=df['z'],
                      radius=50,
                      opacity=.5
                     )

fig.update_geos(
    visible=False, resolution=110, scope="usa",
    showcountries=True, countrycolor="Black",
    showsubunits=True, subunitcolor="Black"
)

fig.show()

insert 4th picture here

Jkiefn1
  • 91
  • 3
  • 16

1 Answers1

2
  • tile maps and layer maps do not work together. Hence you cannot use markers from geo on mapbox

  • thinking laterally, you can add your own geojson layers onto mapbox plots

  • generate geometry. Have provided two options for this

    1. a simple triangle
      • get_geom(df["long"], df["lat"], marker=None, size=k)
    2. https://labs.mapbox.com/maki-icons/
      • get_geom(df["long"], df["lat"], marker="star", size=k) where marker is the MAKI icon name. NB icons with holes can be filled in - for example caution
  • adding layers to mapbox figure layout. This is parameterised to generate multiple layers to support different zoom levels. More layers, more overhead.

import geopandas as gpd
import pandas as pd
import shapely.geometry
import math
import json
import plotly.express as px
import svgpath2mpl
import requests
import numpy as np

d = {
    "Location": ["Point A", "Point B"],
    "lat": [30, 40],
    "long": [-90, -80],
    "z": [100, 200],
}
df = pd.DataFrame(data=d)

fig = px.density_mapbox(
    df,
    lat="lat",
    lon="long",
    z="z",
    hover_name="Location",
    center=dict(lat=38.5, lon=-96),
    range_color=[0, 200],
    zoom=2,
    radius=50,
    opacity=0.5,
    mapbox_style="open-street-map",
)

# https://stackoverflow.com/questions/23411688/drawing-polygon-with-n-number-of-sides-in-python-3-2
def polygon(sides, radius=1, rotation=0, translation=None):
    one_segment = math.pi * 2 / sides

    points = [(math.sin(one_segment * i + rotation) * radius,
               math.cos(one_segment * i + rotation) * radius,)
              for i in range(sides)]

    if translation:
        points = [[sum(pair) for pair in zip(point, translation)] for point in points]

    return shapely.geometry.Polygon(points)

def makimarker(makiname="star", geo=(0, 0), size=0.1):
    url = f"https://raw.githubusercontent.com/mapbox/maki/main/icons/{makiname}.svg"
    svgpath = pd.read_xml(requests.get(url).text).loc[0, "d"]
    p = svgpath2mpl.parse_path(svgpath).to_polygons()
    # need centroid to adjust marked to be centred on geo location
    c = shapely.affinity.scale(
        shapely.geometry.Polygon(p[0]), xfact=size, yfact=size
    ).centroid
    # centre and place marker
    marker = shapely.geometry.Polygon(
        [[sum(triple) for triple in zip(point, geo, (-c.x, -c.y))] for point in p[0]]
    )
    # finally size geometry
    return shapely.affinity.scale(marker, xfact=size, yfact=size)


def get_geom(long_a: list, lat_a: list, marker=None, size=0.15) -> list:
    if marker:
        geo = [
            makimarker(marker, geo=(long, lat), size=size)
            for long, lat in zip(long_a, lat_a)
        ]
    else:
        geo = [
            polygon(3, translation=(long, lat), radius=size*10)
            for long, lat in zip(long_a, lat_a)
        ]
    return json.loads(gpd.GeoDataFrame(geometry=geo).to_json())

# basing math on this https://wiki.openstreetmap.org/wiki/Zoom_levels
# dict is keyed by size with min/max zoom levels covered by this size
MINZOOM=.1
MAXZOOM=18
LAYERS=7
zoom = 512**np.linspace(math.log(MINZOOM,512), math.log(MAXZOOM, 512), LAYERS)
zoom = {
    (200/(2**(np.percentile(zoom[i:i+2],25)+9))): {"minzoom":zoom[i], "maxzoom":zoom[i+1], "name":i}
    for i in range(LAYERS-1)
}

# add a layers to density plot that are the markers
fig.update_layout(
    mapbox={
        "layers": [
            {
                "source": get_geom(df["long"], df["lat"], marker="star", size=k),
                "type": "fill",
                "color": "blue",
                **zoom[k],
            }
            for k in zoom.keys()
        ]
    },
    margin={"t": 0, "b": 0, "l": 0, "r": 0},
)
fig
Rob Raymond
  • 29,118
  • 3
  • 14
  • 30
  • The first solution is a good workaround, but ideally I'd love the size of the symbol to scale while zooming in and out. So it doesn't quite fit the need. The second solution I'm sure would work great, but for whatever reason I get the 'pandas can't read_xml' attribute error. Pandas version 1.3.2. – Jkiefn1 Aug 20 '21 at 04:05
  • I know `read_xml()` is a recent add to pandas.. 1.3.2 is 100% unto date. what's the error? I can't think of a way to scale geojson with zoom, will investigate more – Rob Raymond Aug 20 '21 at 05:38
  • I've somewhat refactored. now supports zoom to an extent by adding multiple layers that have **minzoom** and **maxzoom** where geometry has been sized to *mostly* work with zoom level – Rob Raymond Aug 20 '21 at 09:50
  • Rob, is there anyway you can post the original solution as well, without the zoomed layers? – Jkiefn1 Oct 02 '21 at 19:13
  • I don't have it anymore.... would need to rebuild it. above solution can be pulled back to one layer by setting `LAYER` – Rob Raymond Oct 02 '21 at 20:03