How the Streamlit app.py Frontend Provides an Interactive Interface for Prettymaps

The Prettymaps Streamlit frontend uses app.py to wrap the core library with reactive UI widgets, session state for caching generated maps, and download buttons for PNG/SVG export—delegating heavy rendering to prettymaps.plot() while keeping the interface responsive.

The streamlit app in the Prettymaps repository transforms the Python plotting library into a browser-based map design tool. Located at app.py in the repository root, this single-file frontend bridges user interactions with the underlying prettymaps package, enabling non-coders to generate stylized OpenStreetMap visualizations through point-and-click controls.

Architecture Overview of the Prettymaps Streamlit Frontend

The app.py implementation follows a three-layer architecture that separates presentation, state, and execution concerns:

Layer Responsibility Key Code Location
UI definition Renders input widgets, preset selectors, and download buttons app.py lines 18-50
State management Persists generated images and file paths in st.session_state to prevent redundant re-rendering app.py lines 25-28
Map generation Collects widget values, invokes prettymaps.plot(), and serializes outputs app.py lines 45-71

This separation ensures that adjusting a slider or color picker does not trigger expensive OSM data fetching—only clicking the Generate button does.

Page Configuration and Preset Loading

The frontend initializes with explicit Streamlit configuration to maximize usable screen space:

st.set_page_config(layout="wide")
sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
import prettymaps
presets = prettymaps.presets().to_dict()
st.title("prettymaps")

The sys.path manipulation ensures the local prettymaps package takes precedence over any installed version. The presets dictionary loads JSON configurations from prettymaps/presets/*.json, populating the preset dropdown with curated map styles like "minimal" or "macao".

Input Widgets and Layer Controls

The interface splits into two columns via cols = st.columns([1, 2]), allocating one-third width to controls and two-thirds to output.

Left Column: Map Parameters

The control panel exposes these Streamlit widgets:

  • st.text_area("Location") — OSM query string (e.g., "Paris, France")
  • st.slider("Radius", 0.1, 5.0) — map radius in kilometers (converted to meters for prettymaps.plot())
  • st.checkbox("Circular map") — toggles circular crop versus rectangular bounds
  • st.selectbox("Preset", presets["preset"]) — selects predefined style configurations
  • Dynamic color pickers — builds custom_palette dictionary for building colors
  • st.selectbox("Page size") and st.number_input("DPI", 50, 1200) — controls figsize passed to Matplotlib

Layer Toggles

A dictionary of checkboxes enables selective rendering of OSM features:

layers = {
    "building": st.checkbox("Buildings", value=True),
    "water": st.checkbox("Water", value=True),
    "forest": st.checkbox("Forest", value=True),
    # ... additional layers

}

Each checkbox value determines whether the layer appears in the final map. Unchecked layers get value False (suppressed); checked layers receive empty dictionaries {} (enabled with default styling).

Map Generation Trigger

The Generate button sits in the right column with primary styling:

button = st.button(
    "Generate",
    key="generate_map",
    help="Click to generate the map",
    type="primary",
    icon=":material/map:",
    use_container_width=True,
)

When activated, the callback executes this sequence:

Step 1: Prepare Layer Dictionary

layers_dict = {k: (False if v == False else {}) for k, v in layers.items()}

This transforms checkbox booleans into the format prettymaps.plot() expects—False to disable, {} to enable with defaults.

Step 2: Create Matplotlib Figure

fig, ax = plt.subplots(figsize=(width, height), dpi=300)

The figsize tuple derives from page size selection and DPI input.

Step 3: Invoke Core Library

prettymaps.plot(
    query,
    radius=1000 * radius,  # km to meters

    circle=circular,
    layers=layers_dict,
    style={"building": {"palette": list(custom_palette.values())}},
    figsize=(width, height),
    preset=selected_preset,
    show=False,
    ax=ax,
)

This calls the main entry point in prettymaps/draw.py lines 61-73, which handles OSM data retrieval via OSMnX, preset application, and layered rendering.

Step 4: Serialize and Cache Results

buf = io.BytesIO()
plt.savefig(buf, format="png", bbox_inches="tight", dpi=300)
buf.seek(0)

# Write temporary files for download

with open("/tmp/generated_map.png", "wb") as f:
    f.write(buf.getvalue())

# Generate SVG via helper

download_svg(fig, "/tmp/generated_map_download.svg")

# Update session state

st.session_state["last_image"] = buf
st.session_state["last_png_path"] = "/tmp/generated_map.png"
st.session_state["last_svg_path"] = "/tmp/generated_map_download.svg"

The io.BytesIO buffer enables immediate display without filesystem I/O; temporary files satisfy Streamlit's st.download_button requirements.

Download Buttons and Image Display

Two download buttons appear below the generate button, conditionally enabled:

png_ready = "last_png_path" in st.session_state and os.path.exists(st.session_state["last_png_path"])
svg_ready = "last_svg_path" in st.session_state and os.path.exists(st.session_state["last_svg_path"])

st.download_button("Download PNG", open(st.session_state["last_png_path"], "rb"), disabled=not png_ready)
st.download_button("Download SVG", open(st.session_state["last_svg_path"], "rb"), disabled=not svg_ready)

The generated map renders via:

st.image(st.session_state.last_image, use_container_width=True)

Using use_container_width=True ensures responsive scaling across devices.

Reactive Interaction Flow

The Prettymaps Streamlit frontend implements this execution model:

  1. Widget change → Streamlit re-runs app.py, preserving session_state values
  2. Generate click → Heavy prettymaps.plot() executes once, results cached
  3. State update → st.session_state receives new image buffer and file paths
  4. UI refresh → Image display and download buttons activate automatically
  5. Parameter adjustment → Clears previous buffer implicitly (re-execution), requiring new generation

This pattern—computation on demand with full-result caching—eliminates lag during exploration while maintaining output persistence.

Code Example: Minimal Reproduction

This condensed snippet mirrors the app.py logic for custom deployments:

import streamlit as st
import prettymaps
import io
import matplotlib.pyplot as plt

st.set_page_config(layout="wide")
st.title("prettymaps demo")

# Widgets

query = st.text_input("Location", "Heerhugowaard, Netherlands")
radius = st.slider("Radius (km)", 0.5, 2.0, 0.75)
circular = st.checkbox("Circular map")
preset = st.selectbox("Preset", list(prettymaps.presets()["preset"]))

# Generation

if st.button("Generate"):
    fig, ax = plt.subplots(figsize=(8, 8), dpi=200)
    prettymaps.plot(
        query,
        radius=1000 * radius,
        circle=circular,
        preset=preset,
        ax=ax,
        show=False,
    )
    buf = io.BytesIO()
    plt.savefig(buf, format="png", bbox_inches="tight")
    buf.seek(0)
    st.image(buf)
    st.download_button("Download", buf, file_name="map.png")

Key Source Files in the Prettymaps Repository

File Purpose
app.py Streamlit frontend implementation — UI widgets, session handling, download logic
prettymaps/draw.py Core rendering engine — plot() function for OSM fetching and layer drawing
prettymaps/__init__.py Public API exports — plot(), preset(), presets()
prettymaps/presets/*.json Style configurations — color schemes, layer defaults, visual themes
prettymaps/utils.py Timing utilities and logging helpers

Summary

  • The Prettymaps Streamlit frontend in app.py acts as a thin controller around the core library, not a reimplementation
  • st.session_state caches generated maps, enabling download buttons and preventing re-rendering on widget adjustments
  • Layer toggles and preset selection expose the full flexibility of prettymaps.plot() through intuitive widgets
  • PNG and SVG export uses temporary files bridged to st.download_button for browser-native downloads
  • All heavy computation—OSM queries, geometry processing, Matplotlib rendering—delegates to prettymaps/draw.py

Frequently Asked Questions

How does the Prettymaps Streamlit frontend prevent duplicate map generation?

The frontend uses st.session_state to store the PNG buffer and file paths after the first successful prettymaps.plot() call. When users adjust widgets, Streamlit re-executes app.py but preserves these cached values. Only clicking Generate again clears and replaces the cached output, ensuring expensive rendering runs exactly when requested.

Can I customize the color palette beyond the preset options?

Yes—the app.py interface includes dynamic color pickers that build a custom_palette dictionary. These values pass into prettymaps.plot() via the style parameter: style={"building": {"palette": list(custom_palette.values())}}. You can extend this pattern in your own Streamlit apps by adding more color inputs for additional layers like water or forest.

Where does the actual map rendering happen in the codebase?

The heavy lifting occurs in prettymaps/draw.py specifically in the plot() function at lines 61-73. This function coordinates OSM data fetching through OSMnX, applies preset configurations, renders individual layers, and returns a Plot object. The Streamlit frontend merely collects inputs and calls this core function.

How does the frontend handle SVG export differently from PNG?

PNG generation uses io.BytesIO for in-memory buffering with matplotlib.pyplot.savefig(), while SVG export calls a separate download_svg() helper that writes directly to /tmp/generated_map_download.svg. This distinction exists because Matplotlib's SVG backend requires filesystem access for proper metadata embedding, whereas PNG can stream entirely in memory.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →