Chris Parmer — home

Street-level map

a few downtown blocks: trees by trunk size, food trucks by name

Example from Plotly for highly customizable print-ready data visualization · shared helpers

Street-level map — a few downtown blocks: trees by trunk size, food trucks by name

Python Code

"""Street-level map — a few downtown blocks: trees by trunk size, food trucks by name.

The same MapLibre `map` trace at zoom 15, where the basemap's own street
names do the wayfinding. Every street tree sized by trunk diameter, the
permitted food trucks as labelled markers, a scale bar drawn from two
points a known distance apart, and a light basemap so the ink reads.
"""
from pathlib import Path

from _shared import fetch_csv, save, base_layout, titles, INK, MUTED, GREEN, hex_to_rgba

import numpy as np
import pandas as pd
import plotly.graph_objects as go

CHART_NUM = 31
TREES = "https://data.sfgov.org/api/views/tkzw-k3nq/rows.csv?accessType=DOWNLOAD"
TRUCKS = "https://data.sfgov.org/api/views/rqzj-sfat/rows.csv?accessType=DOWNLOAD"
LAT0, LAT1, LON0, LON1 = 37.784, 37.797, -122.412, -122.394   # the Financial District
TRUCK = "#d1495b"


def generate():
    trees = fetch_csv(TREES, usecols=["species", "latitude", "longitude", "mapdbh"], low_memory=False).dropna(subset=["latitude", "longitude"])
    trees = trees[trees["latitude"].between(LAT0, LAT1) & trees["longitude"].between(LON0, LON1)].copy()
    trees["common"] = trees["species"].str.split(" :: ").str[-1]
    trees["dbh"] = trees["mapdbh"].fillna(trees["mapdbh"].median()).clip(1, 60)
    trucks = fetch_csv(TRUCKS)
    trucks = trucks[(trucks["Status"] == "APPROVED") & trucks["Latitude"].between(LAT0, LAT1) & trucks["Longitude"].between(LON0, LON1)]
    trucks = trucks.drop_duplicates(subset=["Latitude", "Longitude"])

    fig = go.Figure()
    fig.add_trace(go.Scattermap(
        lat=trees["latitude"], lon=trees["longitude"], mode="markers", name="Street tree (size = trunk diameter)",
        marker=dict(size=3 + np.sqrt(trees["dbh"]) * 2.2, color=hex_to_rgba(GREEN, 0.65)),
        customdata=np.c_[trees["common"], trees["dbh"]],
        hovertemplate="%{customdata[0]}<br>trunk %{customdata[1]:.0f} in<extra></extra>",
    ))
    fig.add_trace(go.Scattermap(
        lat=trucks["Latitude"], lon=trucks["Longitude"], mode="markers+text", name="Permitted food truck",
        marker=dict(size=13, color=TRUCK, opacity=0.95),
        text=trucks["Applicant"].str.replace(r" (LLC|Inc\.?)$", "", regex=True), textposition="top right",
        textfont=dict(size=11, color=TRUCK),
        customdata=np.c_[trucks["Address"], trucks["FoodItems"].fillna("").str.slice(0, 60)],
        hovertemplate="<b>%{text}</b><br>%{customdata[0]}<br>%{customdata[1]}<extra></extra>",
    ))
    # Scale bar: 500 m along a parallel is 500 / (111,320 · cos φ) degrees of longitude.
    lat_bar, lon_bar = LAT0 + 0.0012, LON1 - 0.0075
    dlon = 500 / (111_320 * np.cos(np.radians(lat_bar)))
    # (A text layer without markers trips MapLibre's layer builder, so the bar carries its end caps as markers.)
    fig.add_trace(go.Scattermap(lat=[lat_bar, lat_bar], lon=[lon_bar, lon_bar + dlon], mode="lines+markers+text", line=dict(color=INK, width=3),
                                marker=dict(size=6, color=INK), text=["", "500 m"], textposition="top left",
                                textfont=dict(size=11, color=INK), hoverinfo="skip", showlegend=False))

    fig.update_layout(**base_layout(margin=dict(l=20, r=20, t=130, b=60), showlegend=True,
                                    legend=dict(x=0.01, y=0.99, xanchor="left", yanchor="top", bgcolor="rgba(255,255,255,0.9)",
                                                font=dict(size=12), itemsizing="constant", bordercolor="#d9d9e0", borderwidth=1)))
    fig.update_layout(map=dict(style="carto-positron", center=dict(lat=(LAT0 + LAT1) / 2, lon=(LON0 + LON1) / 2), zoom=15.05),
                      map_domain=dict(x=[0, 1], y=[0, 1]))
    big = trees.nlargest(1, "dbh").iloc[0]
    titles(fig, "Downtown San Francisco at street level, tree by tree",
           f"San Francisco's Financial District at street level: {len(trees):,} street trees sized by trunk diameter (the largest, a {big['common']}, is {big['dbh']:.0f} inches across),<br>"
           f"and the {len(trucks)} food trucks with a permit to park here.",
           "Source: DataSF Street Tree List (tkzw-k3nq) and Mobile Food Facility Permits (rqzj-sfat). Basemap: CARTO Positron on MapLibre, © OpenStreetMap contributors.", lift=18)
    save(CHART_NUM, fig, width=1280, height=1000)

Made with Plotly