hghodkephd commited on
Commit
1e09436
·
verified ·
1 Parent(s): 31c16fd

Upload 6 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ma_pop_density.geojson filter=lfs diff=lfs merge=lfs -text
37
+ ma_tract_income.geojson filter=lfs diff=lfs merge=lfs -text
20250818_ma_ceramics_map_MINIMAL.ipynb ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "id": "70964a01",
6
+ "metadata": {},
7
+ "source": [
8
+ "# Massachusetts Ceramics Map — Minimal Final Notebook\n",
9
+ "\n",
10
+ "This minimal notebook generates the final interactive Folium map for Massachusetts ceramics.\n",
11
+ "Edit the **Config** cell (paths + markers) and run the **Build Map** cell.\n"
12
+ ]
13
+ },
14
+ {
15
+ "cell_type": "code",
16
+ "execution_count": 7,
17
+ "id": "930fc5db",
18
+ "metadata": {},
19
+ "outputs": [],
20
+ "source": [
21
+ "# --- CONFIG (edit these paths) ---\n",
22
+ "POP_PATH = \"/Users/harshadghodke/GinkgoClayWorks/Studio/Location/ma_pop_density.geojson\"\n",
23
+ "INC_PATH = \"/Users/harshadghodke/GinkgoClayWorks/Studio/Location/ma_tract_income.geojson\"\n",
24
+ "ROADS_PATH = \"/Users/harshadghodke/GinkgoClayWorks/Studio/Location/ma_major_roads.geojson\" # or None\n",
25
+ "OUT_HTML = \"/Users/harshadghodke/GinkgoClayWorks/Studio/Location/ma_income_pop_studio_map.html\"\n",
26
+ "\n",
27
+ "# --- Markers (edit as needed) ---\n",
28
+ "locations = [\n",
29
+ " # Studios\n",
30
+ " {\"label\": \"Community Kiln (Framingham)\", \"lat\": 42.28, \"lon\": -71.42, \"group\": \"Studios\"},\n",
31
+ " {\"label\": \"ClayWorks (Ware)\", \"lat\": 42.26, \"lon\": -72.25, \"group\": \"Studios\"},\n",
32
+ " {\"label\": \"Mudflat Studio (Somerville)\", \"lat\": 42.3973, \"lon\": -71.0979, \"group\": \"Studios\"},\n",
33
+ " {\"label\": \"Commonwealth Clayworks (Somerville)\", \"lat\": 42.37, \"lon\": -71.10, \"group\": \"Studios\"},\n",
34
+ " {\"label\": \"Feet of Clay Pottery (Brookline)\", \"lat\": 42.3429, \"lon\": -71.1231, \"group\": \"Studios\"},\n",
35
+ " {\"label\": \"Clay Lounge (Boston)\", \"lat\": 42.3502, \"lon\": -71.0593, \"group\": \"Studios\"},\n",
36
+ " {\"label\": \"Indigo Fire (Belmont)\", \"lat\": 42.3956, \"lon\": -71.1748, \"group\": \"Studios\"},\n",
37
+ " {\"label\": \"Indigo Fire (Watertown)\", \"lat\": 42.3700, \"lon\": -71.1820, \"group\": \"Studios\"},\n",
38
+ " {\"label\": \"Local Pottery (Norwell)\", \"lat\": 42.1570, \"lon\": -70.8212, \"group\": \"Studios\"},\n",
39
+ " {\"label\": \"Clay Dreaming (Beverly)\", \"lat\": 42.5584, \"lon\": -70.8800, \"group\": \"Studios\"},\n",
40
+ " {\"label\": \"Purple Sage Pottery (Middleton)\", \"lat\": 42.5971, \"lon\": -70.9968, \"group\": \"Studios\"},\n",
41
+ " {\"label\": \"Rainbows Pottery Studio (Boston)\", \"lat\": 42.3505, \"lon\": -71.0780, \"group\": \"Studios\"},\n",
42
+ " {\"label\": \"Potters Place Studio (Sharon)\", \"lat\": 42.12, \"lon\": -71.17, \"group\": \"Studios\"},\n",
43
+ " {\"label\": \"Worcester Center Ceramics\", \"lat\": 42.2659, \"lon\": -71.8013, \"group\": \"Studios\"},\n",
44
+ " {\"label\": \"Easthampton Clay\", \"lat\": 42.2646, \"lon\": -72.6687, \"group\": \"Studios\"},\n",
45
+ " {\"label\": \"Workshop13 ClayWorks (Ware)\", \"lat\": 42.2612, \"lon\": -72.2476, \"group\": \"Studios\"},\n",
46
+ " {\"label\": \"Umbrella Arts Ceramics (Concord)\", \"lat\": 42.4603, \"lon\": -71.3489, \"group\": \"Studios\"},\n",
47
+ " {\"label\": \"FYACS Pottery Studio (Melrose)\", \"lat\": 42.4566, \"lon\": -71.0626, \"group\": \"Studios\"},\n",
48
+ " {\"label\": \"Lexington Arts (Lexington)\", \"lat\": 42.4473, \"lon\": -71.2255, \"group\": \"Studios\"},\n",
49
+ "\n",
50
+ " # Suppliers\n",
51
+ " {\"label\": \"Sheffield Pottery\", \"lat\": 42.1087, \"lon\": -73.3614, \"group\": \"Suppliers\"},\n",
52
+ " {\"label\": \"PSH USA / Pottery Supply House\", \"lat\": 43.4500, \"lon\": -79.6500, \"group\": \"Suppliers\"},\n",
53
+ " {\"label\": \"The Ceramic Shop (PA)\", \"lat\": 40.1220, \"lon\": -75.3392, \"group\": \"Suppliers\"},\n",
54
+ " {\"label\": \"Bailey Pottery (NY)\", \"lat\": 41.9270, \"lon\": -74.0053, \"group\": \"Suppliers\"},\n",
55
+ " {\"label\": \"Rusty Kiln (CT)\", \"lat\": 41.7751, \"lon\": -72.3004, \"group\": \"Suppliers\"},\n",
56
+ " {\"label\": \"Portland Pottery (ME)\", \"lat\": 43.6668, \"lon\": -70.2544, \"group\": \"Suppliers\"},\n",
57
+ "\n",
58
+ " # Artist Hubs\n",
59
+ " {\"label\": \"Cape Cod Potters\", \"lat\": 41.65, \"lon\": -70.25, \"group\": \"Artist Hubs\"},\n",
60
+ " {\"label\": \"Asparagus Valley Pottery Trail\", \"lat\": 42.30, \"lon\": -72.50, \"group\": \"Artist Hubs\"},\n",
61
+ "\n",
62
+ " # Ceramics Schools\n",
63
+ " {\"label\": \"MassArt (Boston)\", \"lat\": 42.3398, \"lon\": -71.0921, \"group\": \"Ceramics Schools\"},\n",
64
+ " {\"label\": \"SMFA at Tufts (Boston)\", \"lat\": 42.3390, \"lon\": -71.0985, \"group\": \"Ceramics Schools\"},\n",
65
+ " {\"label\": \"UMass Amherst\", \"lat\": 42.3954, \"lon\": -72.5199, \"group\": \"Ceramics Schools\"},\n",
66
+ " {\"label\": \"UMass Dartmouth\", \"lat\": 41.5739, \"lon\": -70.2497, \"group\": \"Ceramics Schools\"},\n",
67
+ " {\"label\": \"Westfield State University\", \"lat\": 42.1251, \"lon\": -72.7580, \"group\": \"Ceramics Schools\"},\n",
68
+ " {\"label\": \"Harvard Ceramics Program\", \"lat\": 42.3673, \"lon\": -71.1119, \"group\": \"Ceramics Schools\"},\n",
69
+ " {\"label\": \"Hopkinton Center for the Arts\", \"lat\": 42.2290, \"lon\": -71.5220, \"group\": \"Ceramics Schools\"},\n",
70
+ "\n",
71
+ " # Home Base\n",
72
+ " {\"label\": \"Hopkinton (Home Base)\", \"lat\": 42.23, \"lon\": -71.52, \"group\": \"Reference\"},\n",
73
+ "]"
74
+ ]
75
+ },
76
+ {
77
+ "cell_type": "code",
78
+ "execution_count": null,
79
+ "id": "6bb2bfb2",
80
+ "metadata": {
81
+ "scrolled": true
82
+ },
83
+ "outputs": [],
84
+ "source": [
85
+ "# Minimal, robust build cell matching your original behavior\n",
86
+ "\n",
87
+ "from pathlib import Path\n",
88
+ "import numpy as np\n",
89
+ "import pandas as pd\n",
90
+ "import geopandas as gpd\n",
91
+ "import folium\n",
92
+ "from folium import Choropleth, GeoJson, GeoJsonTooltip\n",
93
+ "\n",
94
+ "# Helpers\n",
95
+ "def ensure_epsg4326(gdf):\n",
96
+ " if gdf.crs is None:\n",
97
+ " return gdf.set_crs(4326, allow_override=True)\n",
98
+ " try:\n",
99
+ " if gdf.crs.to_epsg() != 4326:\n",
100
+ " return gdf.to_crs(4326)\n",
101
+ " except Exception:\n",
102
+ " pass\n",
103
+ " return gdf\n",
104
+ "\n",
105
+ "def safe_quantile_bins(series, qs=(0, .25, .5, .75, .9, 1)):\n",
106
+ " vals = series.dropna()\n",
107
+ " if vals.empty:\n",
108
+ " return 8\n",
109
+ " q = vals.quantile(qs).to_numpy()\n",
110
+ " q = np.unique(q) # strictly increasing for Folium\n",
111
+ " return q.tolist() if len(q) >= 3 else 8\n",
112
+ "\n",
113
+ "# --- Load data (and normalize) ---\n",
114
+ "pop_density = gpd.read_file(POP_PATH)\n",
115
+ "income = gpd.read_file(INC_PATH)\n",
116
+ "\n",
117
+ "# Normalize keys/CRS just in case\n",
118
+ "if \"GEOID_Text\" in pop_density.columns:\n",
119
+ " pop_density[\"GEOID_Text\"] = pop_density[\"GEOID_Text\"].astype(str)\n",
120
+ "pop_density = ensure_epsg4326(pop_density)\n",
121
+ "\n",
122
+ "if \"GEOID\" in income.columns:\n",
123
+ " income[\"GEOID\"] = income[\"GEOID\"].astype(str)\n",
124
+ "income = ensure_epsg4326(income)\n",
125
+ "\n",
126
+ "# Ensure numerics\n",
127
+ "if \"pop_per_sqmi\" in pop_density.columns:\n",
128
+ " pop_density[\"pop_per_sqmi\"] = pd.to_numeric(pop_density[\"pop_per_sqmi\"], errors=\"coerce\")\n",
129
+ "income[\"median_income\"] = pd.to_numeric(income[\"median_income\"], errors=\"coerce\")\n",
130
+ "\n",
131
+ "# --- Define quantile bins (safe) ---\n",
132
+ "pop_bins = safe_quantile_bins(pop_density[\"pop_per_sqmi\"])\n",
133
+ "income_bins= safe_quantile_bins(income[\"median_income\"])\n",
134
+ "\n",
135
+ "# --- Initialize map ---\n",
136
+ "m = folium.Map(location=[42.3, -71.8], zoom_start=8, tiles=\"cartodbpositron\")\n",
137
+ "\n",
138
+ "# --- Population Choropleth + Tooltip overlay ---\n",
139
+ "Choropleth(\n",
140
+ " geo_data=pop_density,\n",
141
+ " name=\"Population Density\",\n",
142
+ " data=pop_density,\n",
143
+ " columns=[\"GEOID_Text\", \"pop_per_sqmi\"],\n",
144
+ " key_on=\"feature.properties.GEOID_Text\",\n",
145
+ " fill_color=\"YlOrRd\",\n",
146
+ " fill_opacity=0.6,\n",
147
+ " line_opacity=0.2,\n",
148
+ " bins=pop_bins,\n",
149
+ " legend_name=\"Population per sq mi\",\n",
150
+ " # nan_fill_opacity=0.1, # uncomment if your folium version supports it\n",
151
+ ").add_to(m)\n",
152
+ "\n",
153
+ "pop_tooltip_layer = folium.FeatureGroup(name=\"Pop Density Tooltips\", show=False)\n",
154
+ "GeoJson(\n",
155
+ " pop_density,\n",
156
+ " style_function=lambda x: {\"fillOpacity\": 0, \"color\": \"transparent\"},\n",
157
+ " tooltip=GeoJsonTooltip(\n",
158
+ " fields=[\"GEOID_Text\", \"pop_per_sqmi\"],\n",
159
+ " aliases=[\"Tract:\", \"Pop/SqMi:\"],\n",
160
+ " localize=True,\n",
161
+ " ),\n",
162
+ ").add_to(pop_tooltip_layer)\n",
163
+ "pop_tooltip_layer.add_to(m)\n",
164
+ "\n",
165
+ "# --- Income Choropleth + Tooltip overlay ---\n",
166
+ "Choropleth(\n",
167
+ " geo_data=income,\n",
168
+ " name=\"Median Household Income\",\n",
169
+ " data=income,\n",
170
+ " columns=[\"GEOID\", \"median_income\"],\n",
171
+ " key_on=\"feature.properties.GEOID\",\n",
172
+ " fill_color=\"PuBuGn\",\n",
173
+ " fill_opacity=0.6,\n",
174
+ " line_opacity=0.2,\n",
175
+ " bins=income_bins,\n",
176
+ " legend_name=\"Median Household Income ($)\",\n",
177
+ " # nan_fill_opacity=0.1, # uncomment if your folium version supports it\n",
178
+ ").add_to(m)\n",
179
+ "\n",
180
+ "income_tooltip_layer = folium.FeatureGroup(name=\"Income Tooltips\", show=False)\n",
181
+ "GeoJson(\n",
182
+ " income,\n",
183
+ " style_function=lambda x: {\"fillOpacity\": 0, \"color\": \"transparent\"},\n",
184
+ " tooltip=GeoJsonTooltip(\n",
185
+ " fields=[\"GEOID\", \"median_income\"],\n",
186
+ " aliases=[\"Tract:\", \"Median Income ($):\"],\n",
187
+ " localize=True,\n",
188
+ " ),\n",
189
+ ").add_to(income_tooltip_layer)\n",
190
+ "income_tooltip_layer.add_to(m)\n",
191
+ "\n",
192
+ "# --- Markers grouped by category ---\n",
193
+ "layer_groups = {}\n",
194
+ "for loc in locations:\n",
195
+ " grp = loc[\"group\"]\n",
196
+ " if grp not in layer_groups:\n",
197
+ " layer_groups[grp] = folium.FeatureGroup(name=grp, show=True)\n",
198
+ "\n",
199
+ " color = (\"blue\" if grp == \"Studios\" else\n",
200
+ " \"green\" if grp == \"Suppliers\" else\n",
201
+ " \"purple\" if grp == \"Ceramics Schools\" else\n",
202
+ " \"orange\" if grp == \"Artist Hubs\" else\n",
203
+ " \"black\")\n",
204
+ "\n",
205
+ " folium.Marker(\n",
206
+ " location=[loc[\"lat\"], loc[\"lon\"]],\n",
207
+ " popup=loc[\"label\"],\n",
208
+ " icon=folium.Icon(color=color),\n",
209
+ " ).add_to(layer_groups[grp])\n",
210
+ "\n",
211
+ "for grp in layer_groups.values():\n",
212
+ " grp.add_to(m)\n",
213
+ "\n",
214
+ "# --- Major Roads (optional) ---\n",
215
+ "if ROADS_PATH and Path(ROADS_PATH).exists():\n",
216
+ " folium.GeoJson(\n",
217
+ " ROADS_PATH,\n",
218
+ " name=\"Major Roads\",\n",
219
+ " style_function=lambda x: {\"color\": \"black\", \"weight\": 1, \"opacity\": 0.4},\n",
220
+ " tooltip=folium.GeoJsonTooltip(fields=[\"FULLNAME\"], aliases=[\"Road:\"]),\n",
221
+ " ).add_to(m)\n",
222
+ "\n",
223
+ "# --- Layer Control ---\n",
224
+ "folium.LayerControl(collapsed=False).add_to(m)\n",
225
+ "\n",
226
+ "# --- Save & display ---\n",
227
+ "Path(OUT_HTML).parent.mkdir(parents=True, exist_ok=True)\n",
228
+ "m.save(OUT_HTML)\n",
229
+ "print(f\"[ok] Saved map to: {OUT_HTML}\")\n",
230
+ "m # display inline"
231
+ ]
232
+ },
233
+ {
234
+ "cell_type": "code",
235
+ "execution_count": null,
236
+ "id": "c22644b4",
237
+ "metadata": {},
238
+ "outputs": [],
239
+ "source": []
240
+ }
241
+ ],
242
+ "metadata": {
243
+ "kernelspec": {
244
+ "display_name": "Python 3",
245
+ "language": "python",
246
+ "name": "python3"
247
+ },
248
+ "language_info": {
249
+ "codemirror_mode": {
250
+ "name": "ipython",
251
+ "version": 3
252
+ },
253
+ "file_extension": ".py",
254
+ "mimetype": "text/x-python",
255
+ "name": "python",
256
+ "nbconvert_exporter": "python",
257
+ "pygments_lexer": "ipython3",
258
+ "version": "3.8.5"
259
+ }
260
+ },
261
+ "nbformat": 4,
262
+ "nbformat_minor": 5
263
+ }
app.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ st.set_page_config(page_title="MA Ceramics Map", layout="wide")
19
+
20
+ DEFAULT_CENTER = (42.30, -71.80)
21
+ DEFAULT_ZOOM = 8
22
+
23
+ def ensure_epsg4326(gdf: gpd.GeoDataFrame) -> gpd.GeoDataFrame:
24
+ if gdf.crs is None:
25
+ return gdf.set_crs(4326, allow_override=True)
26
+ try:
27
+ if gdf.crs.to_epsg() != 4326:
28
+ return gdf.to_crs(4326)
29
+ except Exception:
30
+ pass
31
+ return gdf
32
+
33
+ def safe_quantile_bins(series: pd.Series, qs=(0, .25, .5, .75, .9, 1)):
34
+ vals = series.dropna()
35
+ if vals.empty:
36
+ return 8
37
+ q = vals.quantile(qs).to_numpy()
38
+ q = np.unique(q) # strictly increasing for Folium
39
+ return q.tolist() if len(q) >= 3 else 8
40
+
41
+ def render_folium(m: folium.Map, height=820):
42
+ html = m.get_root().render()
43
+ st.components.v1.html(html, height=height, scrolling=False)
44
+
45
+ # Sidebar config
46
+ st.sidebar.header("Inputs (adjust if you reorganize files)")
47
+ pop_path = st.sidebar.text_input("Population density GeoJSON", value="ma_pop_density.geojson")
48
+ inc_path = st.sidebar.text_input("Income GeoJSON", value="ma_tract_income.geojson")
49
+ roads_path = st.sidebar.text_input("Major roads GeoJSON (optional)", value="ma_major_roads.geojson")
50
+
51
+ show_pop = st.sidebar.checkbox("Show Population Density", value=True)
52
+ show_pop_tooltips = st.sidebar.checkbox("Show Pop Tooltips (Layer toggle)", value=True)
53
+ show_inc = st.sidebar.checkbox("Show Median Income", value=True)
54
+ show_inc_tooltips = st.sidebar.checkbox("Show Income Tooltips (Layer toggle)", value=True)
55
+ show_roads = st.sidebar.checkbox("Show Major Roads", value=True)
56
+
57
+ # Markers (edit here or later move to CSV)
58
+ locations = [
59
+ # Studios
60
+ {"label": "Community Kiln (Framingham)", "lat": 42.28, "lon": -71.42, "group": "Studios"},
61
+ {"label": "ClayWorks (Ware)", "lat": 42.26, "lon": -72.25, "group": "Studios"},
62
+ {"label": "Mudflat Studio (Somerville)", "lat": 42.3973, "lon": -71.0979, "group": "Studios"},
63
+ {"label": "Commonwealth Clayworks (Somerville)", "lat": 42.37, "lon": -71.10, "group": "Studios"},
64
+ {"label": "Feet of Clay Pottery (Brookline)", "lat": 42.3429, "lon": -71.1231, "group": "Studios"},
65
+ {"label": "Clay Lounge (Boston)", "lat": 42.3502, "lon": -71.0593, "group": "Studios"},
66
+ {"label": "Indigo Fire (Belmont)", "lat": 42.3956, "lon": -71.1748, "group": "Studios"},
67
+ {"label": "Indigo Fire (Watertown)", "lat": 42.3700, "lon": -71.1820, "group": "Studios"},
68
+ {"label": "Local Pottery (Norwell)", "lat": 42.1570, "lon": -70.8212, "group": "Studios"},
69
+ {"label": "Clay Dreaming (Beverly)", "lat": 42.5584, "lon": -70.8800, "group": "Studios"},
70
+ {"label": "Purple Sage Pottery (Middleton)", "lat": 42.5971, "lon": -70.9968, "group": "Studios"},
71
+ {"label": "Rainbows Pottery Studio (Boston)", "lat": 42.3505, "lon": -71.0780, "group": "Studios"},
72
+ {"label": "Potters Place Studio (Sharon)", "lat": 42.12, "lon": -71.17, "group": "Studios"},
73
+ {"label": "Worcester Center Ceramics", "lat": 42.2659, "lon": -71.8013, "group": "Studios"},
74
+ {"label": "Easthampton Clay", "lat": 42.2646, "lon": -72.6687, "group": "Studios"},
75
+ {"label": "Workshop13 ClayWorks (Ware)", "lat": 42.2612, "lon": -72.2476, "group": "Studios"},
76
+ {"label": "Umbrella Arts Ceramics (Concord)", "lat": 42.4603, "lon": -71.3489, "group": "Studios"},
77
+ {"label": "FYACS Pottery Studio (Melrose)", "lat": 42.4566, "lon": -71.0626, "group": "Studios"},
78
+ {"label": "Lexington Arts (Lexington)", "lat": 42.4473, "lon": -71.2255, "group": "Studios"},
79
+ # Suppliers
80
+ {"label": "Sheffield Pottery", "lat": 42.1087, "lon": -73.3614, "group": "Suppliers"},
81
+ {"label": "PSH USA / Pottery Supply House", "lat": 43.4500, "lon": -79.6500, "group": "Suppliers"},
82
+ {"label": "The Ceramic Shop (PA)", "lat": 40.1220, "lon": -75.3392, "group": "Suppliers"},
83
+ {"label": "Bailey Pottery (NY)", "lat": 41.9270, "lon": -74.0053, "group": "Suppliers"},
84
+ {"label": "Rusty Kiln (CT)", "lat": 41.7751, "lon": -72.3004, "group": "Suppliers"},
85
+ {"label": "Portland Pottery (ME)", "lat": 43.6668, "lon": -70.2544, "group": "Suppliers"},
86
+ # Artist Hubs
87
+ {"label": "Cape Cod Potters", "lat": 41.65, "lon": -70.25, "group": "Artist Hubs"},
88
+ {"label": "Asparagus Valley Pottery Trail", "lat": 42.30, "lon": -72.50, "group": "Artist Hubs"},
89
+ # Schools
90
+ {"label": "MassArt (Boston)", "lat": 42.3398, "lon": -71.0921, "group": "Ceramics Schools"},
91
+ {"label": "SMFA at Tufts (Boston)", "lat": 42.3390, "lon": -71.0985, "group": "Ceramics Schools"},
92
+ {"label": "UMass Amherst", "lat": 42.3954, "lon": -72.5199, "group": "Ceramics Schools"},
93
+ {"label": "UMass Dartmouth", "lat": 41.5739, "lon": -70.2497, "group": "Ceramics Schools"},
94
+ {"label": "Westfield State University", "lat": 42.1251, "lon": -72.7580, "group": "Ceramics Schools"},
95
+ {"label": "Harvard Ceramics Program", "lat": 42.3673, "lon": -71.1119, "group": "Ceramics Schools"},
96
+ {"label": "Hopkinton Center for the Arts", "lat": 42.2290, "lon": -71.5220, "group": "Ceramics Schools"},
97
+ # Home base
98
+ {"label": "Hopkinton (Home Base)", "lat": 42.23, "lon": -71.52, "group": "Reference"},
99
+ ]
100
+
101
+ # Load data
102
+ pop_file = Path(pop_path)
103
+ inc_file = Path(inc_path)
104
+ roads_file = Path(roads_path) if roads_path else None
105
+
106
+ if not pop_file.exists() or not inc_file.exists():
107
+ st.error("Missing input files. Keep GeoJSONs in the repo root or update paths in the sidebar.")
108
+ st.stop()
109
+
110
+ pop = gpd.read_file(pop_file); pop = ensure_epsg4326(pop)
111
+ inc = gpd.read_file(inc_file); inc = ensure_epsg4326(inc)
112
+
113
+ if "GEOID_Text" in pop.columns: pop["GEOID_Text"] = pop["GEOID_Text"].astype(str)
114
+ if "GEOID" in inc.columns: inc["GEOID"] = inc["GEOID"].astype(str)
115
+ if "pop_per_sqmi" in pop.columns: pop["pop_per_sqmi"] = pd.to_numeric(pop["pop_per_sqmi"], errors="coerce")
116
+ inc["median_income"] = pd.to_numeric(inc["median_income"], errors="coerce")
117
+
118
+ # Build map
119
+ m = folium.Map(location=DEFAULT_CENTER, zoom_start=DEFAULT_ZOOM, tiles="cartodbpositron")
120
+
121
+ # Pop layer + tooltip
122
+
123
+ if show_pop:
124
+ pop_bins = safe_quantile_bins(pop["pop_per_sqmi"])
125
+ Choropleth(
126
+ geo_data=pop, name="Population Density", data=pop,
127
+ columns=["GEOID_Text", "pop_per_sqmi"], key_on="feature.properties.GEOID_Text",
128
+ fill_color="YlOrRd", fill_opacity=0.6, line_opacity=0.2, bins=pop_bins,
129
+ legend_name="Population per sq mi",
130
+ ).add_to(m)
131
+
132
+ if show_pop_tooltips:
133
+ layer = folium.FeatureGroup(name="Pop Density Tooltips", show=False)
134
+ GeoJson(
135
+ pop,
136
+ style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
137
+ tooltip=GeoJsonTooltip(
138
+ fields=["GEOID_Text", "pop_per_sqmi"],
139
+ aliases=["Tract:", "Pop/SqMi:"],
140
+ localize=True,
141
+ ),
142
+ ).add_to(layer)
143
+ layer.add_to(m)
144
+
145
+ # Income layer + tooltip
146
+ if show_inc:
147
+ inc_bins = safe_quantile_bins(inc["median_income"])
148
+ Choropleth(
149
+ geo_data=inc, name="Median Household Income", data=inc,
150
+ columns=["GEOID", "median_income"], key_on="feature.properties.GEOID",
151
+ fill_color="PuBuGn", fill_opacity=0.6, line_opacity=0.2, bins=inc_bins,
152
+ legend_name="Median Household Income ($)",
153
+ ).add_to(m)
154
+
155
+ if show_inc_tooltips:
156
+ layer = folium.FeatureGroup(name="Income Tooltips", show=False)
157
+ GeoJson(
158
+ inc,
159
+ style_function=lambda x: {"fillOpacity": 0, "color": "transparent"},
160
+ tooltip=GeoJsonTooltip(
161
+ fields=["GEOID", "median_income"],
162
+ aliases=["Tract:", "Median Income ($):"],
163
+ localize=True,
164
+ ),
165
+ ).add_to(layer)
166
+ layer.add_to(m)
167
+
168
+ # Markers by group
169
+ groups = {}
170
+ for loc in locations:
171
+ g = loc["group"]
172
+ if g not in groups:
173
+ groups[g] = folium.FeatureGroup(name=g, show=True)
174
+ color = ("blue" if g == "Studios" else "green" if g == "Suppliers" else
175
+ "purple" if g == "Ceramics Schools" else "orange" if g == "Artist Hubs" else "black")
176
+ folium.Marker([loc["lat"], loc["lon"]], popup=loc["label"], icon=folium.Icon(color=color)).add_to(groups[g])
177
+ for layer in groups.values():
178
+ layer.add_to(m)
179
+
180
+ # Optional roads
181
+ if roads_file and roads_file.exists() and show_roads:
182
+ folium.GeoJson(
183
+ str(roads_file), name="Major Roads",
184
+ style_function=lambda x: {"color": "black", "weight": 1, "opacity": 0.4},
185
+ tooltip=GeoJsonTooltip(fields=["FULLNAME"], aliases=["Road:"]),
186
+ ).add_to(m)
187
+
188
+ folium.LayerControl(collapsed=False).add_to(m)
189
+
190
+ st.markdown("### Massachusetts Ceramics Map")
191
+ render_folium(m)
ma_major_roads.geojson ADDED
The diff for this file is too large to render. See raw diff
 
ma_pop_density.geojson ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eed9af25c19a4ef0f827ddd8e644c66ee35477198c7eec6c309f2cf74744b0c0
3
+ size 37266790
ma_tract_income.geojson ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:647bcaac7121b1ba82c32e6871b404723281fcab53bfa4929333fe393a27fa93
3
+ size 12546077
requirements.txt CHANGED
@@ -1,3 +1,9 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
1
+ streamlit>=1.36
2
+ geopandas>=0.13
3
+ folium>=0.14
4
+ pandas>=2.0
5
+ numpy>=1.24
6
+ fiona>=1.9
7
+ shapely>=2.0
8
+ pyproj>=3.6
9
+ rtree>=1.0