Jkiefn1
Jkiefn1

Reputation: 143

Plotly - Adding Scatter Geo points and traces on top of Density Mapbox

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

Upvotes: 6

Views: 12151

Answers (1)

Rob Raymond
Rob Raymond

Reputation: 31226

  • 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

Upvotes: 2

Related Questions