Ceramap / streamlit_app.py
hghodkephd's picture
Rename app.py to streamlit_app.py
f7d14be verified
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Aug 18 12:56:57 2025
@author: harshadghodke
"""
# app.py
import streamlit as st
from pathlib import Path
import numpy as np
import pandas as pd
import geopandas as gpd
import folium
from folium import Choropleth, GeoJson, GeoJsonTooltip
HF_DATA_BASE = "https://huggingface.co/datasets/hghodkephd/ceramap-data/resolve/main"
DEFAULT_POP_PATH = f"{HF_DATA_BASE}/ma_pop_density.geojson"
DEFAULT_INC_PATH = f"{HF_DATA_BASE}/ma_tract_income.geojson"
DEFAULT_ROADS_PATH = f"{HF_DATA_BASE}/ma_major_roads.geojson"
st.set_page_config(page_title="MA Ceramics Map", layout="wide")
DEFAULT_CENTER = (42.30, -71.80)
DEFAULT_ZOOM = 8
def ensure_epsg4326(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
if gdf.crs is None:
return gdf.set_crs(4326, allow_override=True)
try:
if gdf.crs.to_epsg() != 4326:
return gdf.to_crs(4326)
except Exception:
pass
return gdf
def safe_quantile_bins(series: pd.Series, qs=(0, .25, .5, .75, .9, 1)):
vals = series.dropna()
if vals.empty:
return 8
q = vals.quantile(qs).to_numpy()
q = np.unique(q) # strictly increasing for Folium
return q.tolist() if len(q) >= 3 else 8
def render_folium(m: folium.Map, height=820):
html = m.get_root().render()
st.components.v1.html(html, height=height, scrolling=False)
# Sidebar config
st.sidebar.header("Inputs (adjust if you reorganize files)")
pop_path = st.sidebar.text_input("Population density GeoJSON", value=DEFAULT_POP_PATH)
inc_path = st.sidebar.text_input("Income GeoJSON", value=DEFAULT_INC_PATH)
roads_path = st.sidebar.text_input("Major roads GeoJSON (optional)", value=DEFAULT_ROADS_PATH)
show_pop = st.sidebar.checkbox("Show Population Density", value=True)
show_pop_tooltips = st.sidebar.checkbox("Show Pop Tooltips (Layer toggle)", value=True)
show_inc = st.sidebar.checkbox("Show Median Income", value=True)
show_inc_tooltips = st.sidebar.checkbox("Show Income Tooltips (Layer toggle)", value=True)
show_roads = st.sidebar.checkbox("Show Major Roads", value=True)
# Markers (edit here or later move to CSV)
locations = [
# Studios
{"label": "Community Kiln (Framingham)", "lat": 42.28, "lon": -71.42, "group": "Studios"},
{"label": "ClayWorks (Ware)", "lat": 42.26, "lon": -72.25, "group": "Studios"},
{"label": "Mudflat Studio (Somerville)", "lat": 42.3973, "lon": -71.0979, "group": "Studios"},
{"label": "Commonwealth Clayworks (Somerville)", "lat": 42.37, "lon": -71.10, "group": "Studios"},
{"label": "Feet of Clay Pottery (Brookline)", "lat": 42.3429, "lon": -71.1231, "group": "Studios"},
{"label": "Clay Lounge (Boston)", "lat": 42.3502, "lon": -71.0593, "group": "Studios"},
{"label": "Indigo Fire (Belmont)", "lat": 42.3956, "lon": -71.1748, "group": "Studios"},
{"label": "Indigo Fire (Watertown)", "lat": 42.3700, "lon": -71.1820, "group": "Studios"},
{"label": "Local Pottery (Norwell)", "lat": 42.1570, "lon": -70.8212, "group": "Studios"},
{"label": "Clay Dreaming (Beverly)", "lat": 42.5584, "lon": -70.8800, "group": "Studios"},
{"label": "Purple Sage Pottery (Middleton)", "lat": 42.5971, "lon": -70.9968, "group": "Studios"},
{"label": "Rainbows Pottery Studio (Boston)", "lat": 42.3505, "lon": -71.0780, "group": "Studios"},
{"label": "Potters Place Studio (Sharon)", "lat": 42.12, "lon": -71.17, "group": "Studios"},
{"label": "Worcester Center Ceramics", "lat": 42.2659, "lon": -71.8013, "group": "Studios"},
{"label": "Easthampton Clay", "lat": 42.2646, "lon": -72.6687, "group": "Studios"},
{"label": "Workshop13 ClayWorks (Ware)", "lat": 42.2612, "lon": -72.2476, "group": "Studios"},
{"label": "Umbrella Arts Ceramics (Concord)", "lat": 42.4603, "lon": -71.3489, "group": "Studios"},
{"label": "FYACS Pottery Studio (Melrose)", "lat": 42.4566, "lon": -71.0626, "group": "Studios"},
{"label": "Lexington Arts (Lexington)", "lat": 42.4473, "lon": -71.2255, "group": "Studios"},
# Suppliers
{"label": "Sheffield Pottery", "lat": 42.1087, "lon": -73.3614, "group": "Suppliers"},
{"label": "PSH USA / Pottery Supply House", "lat": 43.4500, "lon": -79.6500, "group": "Suppliers"},
{"label": "The Ceramic Shop (PA)", "lat": 40.1220, "lon": -75.3392, "group": "Suppliers"},
{"label": "Bailey Pottery (NY)", "lat": 41.9270, "lon": -74.0053, "group": "Suppliers"},
{"label": "Rusty Kiln (CT)", "lat": 41.7751, "lon": -72.3004, "group": "Suppliers"},
{"label": "Portland Pottery (ME)", "lat": 43.6668, "lon": -70.2544, "group": "Suppliers"},
# Artist Hubs
{"label": "Cape Cod Potters", "lat": 41.65, "lon": -70.25, "group": "Artist Hubs"},
{"label": "Asparagus Valley Pottery Trail", "lat": 42.30, "lon": -72.50, "group": "Artist Hubs"},
# Schools
{"label": "MassArt (Boston)", "lat": 42.3398, "lon": -71.0921, "group": "Ceramics Schools"},
{"label": "SMFA at Tufts (Boston)", "lat": 42.3390, "lon": -71.0985, "group": "Ceramics Schools"},
{"label": "UMass Amherst", "lat": 42.3954, "lon": -72.5199, "group": "Ceramics Schools"},
{"label": "UMass Dartmouth", "lat": 41.5739, "lon": -70.2497, "group": "Ceramics Schools"},
{"label": "Westfield State University", "lat": 42.1251, "lon": -72.7580, "group": "Ceramics Schools"},
{"label": "Harvard Ceramics Program", "lat": 42.3673, "lon": -71.1119, "group": "Ceramics Schools"},
{"label": "Hopkinton Center for the Arts", "lat": 42.2290, "lon": -71.5220, "group": "Ceramics Schools"},
# Home base
{"label": "Hopkinton (Home Base)", "lat": 42.23, "lon": -71.52, "group": "Reference"},
]
# Load data
pop_file = Path(pop_path)
inc_file = Path(inc_path)
roads_file = Path(roads_path) if roads_path else None
if not pop_file.exists() or not inc_file.exists():
st.error("Missing input files. Keep GeoJSONs in the repo root or update paths in the sidebar.")
st.stop()
pop = gpd.read_file(pop_file); pop = ensure_epsg4326(pop)
inc = gpd.read_file(inc_file); inc = ensure_epsg4326(inc)
if "GEOID_Text" in pop.columns: pop["GEOID_Text"] = pop["GEOID_Text"].astype(str)
if "GEOID" in inc.columns: inc["GEOID"] = inc["GEOID"].astype(str)
if "pop_per_sqmi" in pop.columns: pop["pop_per_sqmi"] = pd.to_numeric(pop["pop_per_sqmi"], errors="coerce")
inc["median_income"] = pd.to_numeric(inc["median_income"], errors="coerce")
# Build map
m = folium.Map(location=DEFAULT_CENTER, zoom_start=DEFAULT_ZOOM, tiles="cartodbpositron")
# Pop layer + tooltip
if show_pop:
pop_bins = safe_quantile_bins(pop["pop_per_sqmi"])
Choropleth(
geo_data=pop, name="Population Density", data=pop,
columns=["GEOID_Text", "pop_per_sqmi"], key_on="feature.properties.GEOID_Text",
fill_color="YlOrRd", fill_opacity=0.6, line_opacity=0.2, bins=pop_bins,
legend_name="Population per sq mi",
).add_to(m)
if show_pop_tooltips:
layer = folium.FeatureGroup(name="Pop Density Tooltips", show=False)
GeoJson(
pop,
style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
tooltip=GeoJsonTooltip(
fields=["GEOID_Text", "pop_per_sqmi"],
aliases=["Tract:", "Pop/SqMi:"],
localize=True,
),
).add_to(layer)
layer.add_to(m)
# Income layer + tooltip
if show_inc:
inc_bins = safe_quantile_bins(inc["median_income"])
Choropleth(
geo_data=inc, name="Median Household Income", data=inc,
columns=["GEOID", "median_income"], key_on="feature.properties.GEOID",
fill_color="PuBuGn", fill_opacity=0.6, line_opacity=0.2, bins=inc_bins,
legend_name="Median Household Income ($)",
).add_to(m)
if show_inc_tooltips:
layer = folium.FeatureGroup(name="Income Tooltips", show=False)
GeoJson(
inc,
style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
tooltip=GeoJsonTooltip(
fields=["GEOID", "median_income"],
aliases=["Tract:", "Median Income ($):"],
localize=True,
),
).add_to(layer)
layer.add_to(m)
# Markers by group
groups = {}
for loc in locations:
g = loc["group"]
if g not in groups:
groups[g] = folium.FeatureGroup(name=g, show=True)
color = ("blue" if g == "Studios" else "green" if g == "Suppliers" else
"purple" if g == "Ceramics Schools" else "orange" if g == "Artist Hubs" else "black")
folium.Marker([loc["lat"], loc["lon"]], popup=loc["label"], icon=folium.Icon(color=color)).add_to(groups[g])
for layer in groups.values():
layer.add_to(m)
# Optional roads
if roads_file and roads_file.exists() and show_roads:
folium.GeoJson(
str(roads_file), name="Major Roads",
style_function=lambda x: {"color": "black", "weight": 1, "opacity": 0.4},
tooltip=GeoJsonTooltip(fields=["FULLNAME"], aliases=["Road:"]),
).add_to(m)
folium.LayerControl(collapsed=False).add_to(m)
st.markdown("### Massachusetts Ceramics Map")
render_folium(m)