Spaces:
Runtime error
Runtime error
| #!/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) |