hghodkephd commited on
Commit
a54edac
·
verified ·
1 Parent(s): 492a112

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -0
app.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Created on Mon Aug 18 12:56:57 2025
5
+
6
+ @author: harshadghodke
7
+ """
8
+
9
+ # app.py
10
+ import streamlit as st
11
+ from pathlib import Path
12
+ import numpy as np
13
+ import pandas as pd
14
+ import geopandas as gpd
15
+ import folium
16
+ from folium import Choropleth, GeoJson, GeoJsonTooltip
17
+
18
+
19
+
20
+ HF_DATA_BASE = "https://huggingface.co/datasets/hghodkephd/ceramap-data/resolve/main"
21
+
22
+ DEFAULT_POP_PATH = f"{HF_DATA_BASE}/ma_pop_density.geojson"
23
+ DEFAULT_INC_PATH = f"{HF_DATA_BASE}/ma_tract_income.geojson"
24
+ DEFAULT_ROADS_PATH = f"{HF_DATA_BASE}/ma_major_roads.geojson"
25
+
26
+
27
+ st.set_page_config(page_title="MA Ceramics Map", layout="wide")
28
+
29
+ DEFAULT_CENTER = (42.30, -71.80)
30
+ DEFAULT_ZOOM = 8
31
+
32
+ def ensure_epsg4326(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
33
+ if gdf.crs is None:
34
+ return gdf.set_crs(4326, allow_override=True)
35
+ try:
36
+ if gdf.crs.to_epsg() != 4326:
37
+ return gdf.to_crs(4326)
38
+ except Exception:
39
+ pass
40
+ return gdf
41
+
42
+ def safe_quantile_bins(series: pd.Series, qs=(0, .25, .5, .75, .9, 1)):
43
+ vals = series.dropna()
44
+ if vals.empty:
45
+ return 8
46
+ q = vals.quantile(qs).to_numpy()
47
+ q = np.unique(q) # strictly increasing for Folium
48
+ return q.tolist() if len(q) >= 3 else 8
49
+
50
+ def render_folium(m: folium.Map, height=820):
51
+ html = m.get_root().render()
52
+ st.components.v1.html(html, height=height, scrolling=False)
53
+
54
+ # Sidebar config
55
+ st.sidebar.header("Inputs (adjust if you reorganize files)")
56
+ pop_path = st.sidebar.text_input("Population density GeoJSON", value=DEFAULT_POP_PATH)
57
+ inc_path = st.sidebar.text_input("Income GeoJSON", value=DEFAULT_INC_PATH)
58
+ roads_path = st.sidebar.text_input("Major roads GeoJSON (optional)", value=DEFAULT_ROADS_PATH)
59
+
60
+ show_pop = st.sidebar.checkbox("Show Population Density", value=True)
61
+ show_pop_tooltips = st.sidebar.checkbox("Show Pop Tooltips (Layer toggle)", value=True)
62
+ show_inc = st.sidebar.checkbox("Show Median Income", value=True)
63
+ show_inc_tooltips = st.sidebar.checkbox("Show Income Tooltips (Layer toggle)", value=True)
64
+ show_roads = st.sidebar.checkbox("Show Major Roads", value=True)
65
+
66
+ # Markers (edit here or later move to CSV)
67
+ locations = [
68
+ # Studios
69
+ {"label": "Community Kiln (Framingham)", "lat": 42.28, "lon": -71.42, "group": "Studios"},
70
+ {"label": "ClayWorks (Ware)", "lat": 42.26, "lon": -72.25, "group": "Studios"},
71
+ {"label": "Mudflat Studio (Somerville)", "lat": 42.3973, "lon": -71.0979, "group": "Studios"},
72
+ {"label": "Commonwealth Clayworks (Somerville)", "lat": 42.37, "lon": -71.10, "group": "Studios"},
73
+ {"label": "Feet of Clay Pottery (Brookline)", "lat": 42.3429, "lon": -71.1231, "group": "Studios"},
74
+ {"label": "Clay Lounge (Boston)", "lat": 42.3502, "lon": -71.0593, "group": "Studios"},
75
+ {"label": "Indigo Fire (Belmont)", "lat": 42.3956, "lon": -71.1748, "group": "Studios"},
76
+ {"label": "Indigo Fire (Watertown)", "lat": 42.3700, "lon": -71.1820, "group": "Studios"},
77
+ {"label": "Local Pottery (Norwell)", "lat": 42.1570, "lon": -70.8212, "group": "Studios"},
78
+ {"label": "Clay Dreaming (Beverly)", "lat": 42.5584, "lon": -70.8800, "group": "Studios"},
79
+ {"label": "Purple Sage Pottery (Middleton)", "lat": 42.5971, "lon": -70.9968, "group": "Studios"},
80
+ {"label": "Rainbows Pottery Studio (Boston)", "lat": 42.3505, "lon": -71.0780, "group": "Studios"},
81
+ {"label": "Potters Place Studio (Sharon)", "lat": 42.12, "lon": -71.17, "group": "Studios"},
82
+ {"label": "Worcester Center Ceramics", "lat": 42.2659, "lon": -71.8013, "group": "Studios"},
83
+ {"label": "Easthampton Clay", "lat": 42.2646, "lon": -72.6687, "group": "Studios"},
84
+ {"label": "Workshop13 ClayWorks (Ware)", "lat": 42.2612, "lon": -72.2476, "group": "Studios"},
85
+ {"label": "Umbrella Arts Ceramics (Concord)", "lat": 42.4603, "lon": -71.3489, "group": "Studios"},
86
+ {"label": "FYACS Pottery Studio (Melrose)", "lat": 42.4566, "lon": -71.0626, "group": "Studios"},
87
+ {"label": "Lexington Arts (Lexington)", "lat": 42.4473, "lon": -71.2255, "group": "Studios"},
88
+ # Suppliers
89
+ {"label": "Sheffield Pottery", "lat": 42.1087, "lon": -73.3614, "group": "Suppliers"},
90
+ {"label": "PSH USA / Pottery Supply House", "lat": 43.4500, "lon": -79.6500, "group": "Suppliers"},
91
+ {"label": "The Ceramic Shop (PA)", "lat": 40.1220, "lon": -75.3392, "group": "Suppliers"},
92
+ {"label": "Bailey Pottery (NY)", "lat": 41.9270, "lon": -74.0053, "group": "Suppliers"},
93
+ {"label": "Rusty Kiln (CT)", "lat": 41.7751, "lon": -72.3004, "group": "Suppliers"},
94
+ {"label": "Portland Pottery (ME)", "lat": 43.6668, "lon": -70.2544, "group": "Suppliers"},
95
+ # Artist Hubs
96
+ {"label": "Cape Cod Potters", "lat": 41.65, "lon": -70.25, "group": "Artist Hubs"},
97
+ {"label": "Asparagus Valley Pottery Trail", "lat": 42.30, "lon": -72.50, "group": "Artist Hubs"},
98
+ # Schools
99
+ {"label": "MassArt (Boston)", "lat": 42.3398, "lon": -71.0921, "group": "Ceramics Schools"},
100
+ {"label": "SMFA at Tufts (Boston)", "lat": 42.3390, "lon": -71.0985, "group": "Ceramics Schools"},
101
+ {"label": "UMass Amherst", "lat": 42.3954, "lon": -72.5199, "group": "Ceramics Schools"},
102
+ {"label": "UMass Dartmouth", "lat": 41.5739, "lon": -70.2497, "group": "Ceramics Schools"},
103
+ {"label": "Westfield State University", "lat": 42.1251, "lon": -72.7580, "group": "Ceramics Schools"},
104
+ {"label": "Harvard Ceramics Program", "lat": 42.3673, "lon": -71.1119, "group": "Ceramics Schools"},
105
+ {"label": "Hopkinton Center for the Arts", "lat": 42.2290, "lon": -71.5220, "group": "Ceramics Schools"},
106
+ # Home base
107
+ {"label": "Hopkinton (Home Base)", "lat": 42.23, "lon": -71.52, "group": "Reference"},
108
+ ]
109
+
110
+ # Load data
111
+ pop_file = Path(pop_path)
112
+ inc_file = Path(inc_path)
113
+ roads_file = Path(roads_path) if roads_path else None
114
+
115
+ if not pop_file.exists() or not inc_file.exists():
116
+ st.error("Missing input files. Keep GeoJSONs in the repo root or update paths in the sidebar.")
117
+ st.stop()
118
+
119
+ pop = gpd.read_file(pop_file); pop = ensure_epsg4326(pop)
120
+ inc = gpd.read_file(inc_file); inc = ensure_epsg4326(inc)
121
+
122
+ if "GEOID_Text" in pop.columns: pop["GEOID_Text"] = pop["GEOID_Text"].astype(str)
123
+ if "GEOID" in inc.columns: inc["GEOID"] = inc["GEOID"].astype(str)
124
+ if "pop_per_sqmi" in pop.columns: pop["pop_per_sqmi"] = pd.to_numeric(pop["pop_per_sqmi"], errors="coerce")
125
+ inc["median_income"] = pd.to_numeric(inc["median_income"], errors="coerce")
126
+
127
+ # Build map
128
+ m = folium.Map(location=DEFAULT_CENTER, zoom_start=DEFAULT_ZOOM, tiles="cartodbpositron")
129
+
130
+ # Pop layer + tooltip
131
+
132
+ if show_pop:
133
+ pop_bins = safe_quantile_bins(pop["pop_per_sqmi"])
134
+ Choropleth(
135
+ geo_data=pop, name="Population Density", data=pop,
136
+ columns=["GEOID_Text", "pop_per_sqmi"], key_on="feature.properties.GEOID_Text",
137
+ fill_color="YlOrRd", fill_opacity=0.6, line_opacity=0.2, bins=pop_bins,
138
+ legend_name="Population per sq mi",
139
+ ).add_to(m)
140
+
141
+ if show_pop_tooltips:
142
+ layer = folium.FeatureGroup(name="Pop Density Tooltips", show=False)
143
+ GeoJson(
144
+ pop,
145
+ style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
146
+ tooltip=GeoJsonTooltip(
147
+ fields=["GEOID_Text", "pop_per_sqmi"],
148
+ aliases=["Tract:", "Pop/SqMi:"],
149
+ localize=True,
150
+ ),
151
+ ).add_to(layer)
152
+ layer.add_to(m)
153
+
154
+ # Income layer + tooltip
155
+ if show_inc:
156
+ inc_bins = safe_quantile_bins(inc["median_income"])
157
+ Choropleth(
158
+ geo_data=inc, name="Median Household Income", data=inc,
159
+ columns=["GEOID", "median_income"], key_on="feature.properties.GEOID",
160
+ fill_color="PuBuGn", fill_opacity=0.6, line_opacity=0.2, bins=inc_bins,
161
+ legend_name="Median Household Income ($)",
162
+ ).add_to(m)
163
+
164
+ if show_inc_tooltips:
165
+ layer = folium.FeatureGroup(name="Income Tooltips", show=False)
166
+ GeoJson(
167
+ inc,
168
+ style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
169
+ tooltip=GeoJsonTooltip(
170
+ fields=["GEOID", "median_income"],
171
+ aliases=["Tract:", "Median Income ($):"],
172
+ localize=True,
173
+ ),
174
+ ).add_to(layer)
175
+ layer.add_to(m)
176
+
177
+ # Markers by group
178
+ groups = {}
179
+ for loc in locations:
180
+ g = loc["group"]
181
+ if g not in groups:
182
+ groups[g] = folium.FeatureGroup(name=g, show=True)
183
+ color = ("blue" if g == "Studios" else "green" if g == "Suppliers" else
184
+ "purple" if g == "Ceramics Schools" else "orange" if g == "Artist Hubs" else "black")
185
+ folium.Marker([loc["lat"], loc["lon"]], popup=loc["label"], icon=folium.Icon(color=color)).add_to(groups[g])
186
+ for layer in groups.values():
187
+ layer.add_to(m)
188
+
189
+ # Optional roads
190
+ if roads_file and roads_file.exists() and show_roads:
191
+ folium.GeoJson(
192
+ str(roads_file), name="Major Roads",
193
+ style_function=lambda x: {"color": "black", "weight": 1, "opacity": 0.4},
194
+ tooltip=GeoJsonTooltip(fields=["FULLNAME"], aliases=["Road:"]),
195
+ ).add_to(m)
196
+
197
+ folium.LayerControl(collapsed=False).add_to(m)
198
+
199
+ st.markdown("### Massachusetts Ceramics Map")
200
+ render_folium(m)