@@ -1063,28 +1063,35 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
10631063 if closest_distances_deg is None :
10641064 return
10651065
1066- # Convert to meters and categorize by distance thresholds
1066+ # Convert to meters and categorize by distance ranges
10671067 distance_thresholds = [100 , 150 , 200 , 250 , 300 ]
10681068 distance_thresholds_deg = [d / 100000 for d in distance_thresholds ]
10691069
1070- # Categorize buildings by closest shelter distance
1070+ # Categorize buildings by closest shelter distance into ranges
10711071 categories = []
10721072 for i , dist_deg in enumerate (closest_distances_deg ):
10731073 if dist_deg > distance_thresholds_deg [- 1 ]:
10741074 categories .append ('no_shelter' )
1075- else :
1076- # Find which threshold it falls into (closest one)
1077- for j , threshold in enumerate (distance_thresholds_deg ):
1078- if dist_deg <= threshold :
1079- categories .append (f'{ distance_thresholds [j ]} m' )
1080- break
1075+ elif dist_deg <= distance_thresholds_deg [0 ]:
1076+ categories .append ('<100m' )
1077+ elif dist_deg <= distance_thresholds_deg [1 ]:
1078+ categories .append ('100-150m' )
1079+ elif dist_deg <= distance_thresholds_deg [2 ]:
1080+ categories .append ('150-200m' )
1081+ elif dist_deg <= distance_thresholds_deg [3 ]:
1082+ categories .append ('200-250m' )
1083+ else : # dist_deg <= distance_thresholds_deg[4] (300m)
1084+ categories .append ('250-300m' )
10811085
10821086 categories = np .array (categories )
10831087
10841088 # Separate densities by category
10851089 density_by_category = {}
1086- for threshold in distance_thresholds :
1087- density_by_category [f'{ threshold } m' ] = local_densities [categories == f'{ threshold } m' ]
1090+ density_by_category ['<100m' ] = local_densities [categories == '<100m' ]
1091+ density_by_category ['100-150m' ] = local_densities [categories == '100-150m' ]
1092+ density_by_category ['150-200m' ] = local_densities [categories == '150-200m' ]
1093+ density_by_category ['200-250m' ] = local_densities [categories == '200-250m' ]
1094+ density_by_category ['250-300m' ] = local_densities [categories == '250-300m' ]
10881095 density_by_category ['no_shelter' ] = local_densities [categories == 'no_shelter' ]
10891096
10901097 _ , ax = plt .subplots (figsize = (10 , 6 ))
@@ -1116,14 +1123,14 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11161123
11171124 data_layers_all = [
11181125 density_by_category ['no_shelter' ],
1119- density_by_category ['300m' ],
1120- density_by_category ['250m' ],
1121- density_by_category ['200m' ],
1122- density_by_category ['150m' ],
1123- density_by_category ['100m' ],
1126+ density_by_category ['250- 300m' ],
1127+ density_by_category ['200- 250m' ],
1128+ density_by_category ['150- 200m' ],
1129+ density_by_category ['100- 150m' ],
1130+ density_by_category ['< 100m' ],
11241131 ]
11251132
1126- labels_all = ['No shelter' , '300m' , '250m' , '200m' , '150m' , '100m' ]
1133+ labels_all = ['No shelter' , '250- 300m' , '200- 250m' , '150- 200m' , '100- 150m' , '< 100m' ]
11271134
11281135 ax .hist (data_layers_all , bins = bins ,
11291136 color = colors_all , alpha = 0.8 , edgecolor = 'none' , stacked = True , label = labels_all )
@@ -1149,11 +1156,11 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11491156
11501157 # Combine all shelter categories into one "has shelter" category
11511158 has_shelter_densities = np .concatenate ([
1152- density_by_category ['100m' ],
1153- density_by_category ['150m' ],
1154- density_by_category ['200m' ],
1155- density_by_category ['250m' ],
1156- density_by_category ['300m' ]
1159+ density_by_category ['< 100m' ],
1160+ density_by_category ['100- 150m' ],
1161+ density_by_category ['150- 200m' ],
1162+ density_by_category ['200- 250m' ],
1163+ density_by_category ['250- 300m' ]
11571164 ])
11581165
11591166 colors_simple = [
@@ -1193,11 +1200,169 @@ def create_local_density_distribution(theme, local_density_data, theme_name='tuf
11931200 print (f" Median: { np .median (local_densities ):.1f} " )
11941201 print (f" Max: { np .max (local_densities ):.0f} " )
11951202 total = len (categories )
1196- for threshold in [100 , 150 , 200 , 250 , 300 ]:
1197- count = np .sum (categories == f'{ threshold } m' )
1198- print (f" { threshold } m: { count :,} ({ count / total * 100 :.1f} %)" )
1199- no_shelter_count = np .sum (categories == 'no_shelter' )
1200- print (f" No shelter: { no_shelter_count :,} ({ no_shelter_count / total * 100 :.1f} %)" )
1203+ for label in ['<100m' , '100-150m' , '150-200m' , '200-250m' , '250-300m' , 'no_shelter' ]:
1204+ count = np .sum (categories == label )
1205+ display_label = 'No shelter' if label == 'no_shelter' else label
1206+ print (f" { display_label } : { count :,} ({ count / total * 100 :.1f} %)" )
1207+
1208+
1209+ def create_distance_to_shelter_line (theme , local_density_data ):
1210+ """Create line graph showing number of buildings vs distance to nearest shelter"""
1211+ if not local_density_data or not local_density_data ['building_coords' ]:
1212+ return
1213+
1214+ building_coords = local_density_data ['building_coords' ]
1215+
1216+ # Load existing shelters
1217+ try :
1218+ with open ('data/shelters.geojson' , 'r' , encoding = 'utf-8' ) as f :
1219+ shelters_data = json .load (f )
1220+ existing_shelters = []
1221+ for feature in shelters_data ['features' ]:
1222+ props = feature ['properties' ]
1223+ status = props .get ('status' , '' ).strip ()
1224+ if status .startswith ('Built' ):
1225+ coords = feature ['geometry' ]['coordinates' ]
1226+ existing_shelters .append ([coords [0 ], coords [1 ]])
1227+ except FileNotFoundError :
1228+ existing_shelters = []
1229+
1230+ if not existing_shelters :
1231+ return
1232+
1233+ # Calculate closest shelter distance for each building
1234+ print (" Calculating closest shelter distances for line graph..." )
1235+ closest_distances_deg = calculate_closest_shelter_distance (building_coords , existing_shelters , max_radius_m = 500 )
1236+ if closest_distances_deg is None :
1237+ return
1238+
1239+ # Convert to meters
1240+ closest_distances_m = np .array (closest_distances_deg ) * 100000
1241+
1242+ # Filter to buildings with shelter within 500m
1243+ has_shelter_mask = closest_distances_m <= 500
1244+ distances_with_shelter = closest_distances_m [has_shelter_mask ]
1245+
1246+ # Create bins for distance ranges
1247+ bins = np .arange (0 , 501 , 10 ) # 10m bins up to 500m
1248+
1249+ # Count buildings in each bin
1250+ counts , bin_edges = np .histogram (distances_with_shelter , bins = bins )
1251+ bin_centers = (bin_edges [:- 1 ] + bin_edges [1 :]) / 2
1252+
1253+ _ , ax = plt .subplots (figsize = (10 , 6 ))
1254+
1255+ # Create line graph
1256+ ax .plot (bin_centers , counts , color = theme ['bar_color' ], linewidth = 2 , marker = 'o' , markersize = 3 )
1257+
1258+ ax .set_xlabel ('Distance to Nearest Shelter (m)' )
1259+ ax .set_ylabel ('Number of Buildings' )
1260+ ax .set_title ('Buildings by Distance to Nearest Shelter' , pad = 15 )
1261+ ax .set_xlim (0 , 500 )
1262+
1263+ setup_tufte_axis (ax )
1264+ ax .grid (True , axis = 'y' , linewidth = 0.5 )
1265+ ax .set_axisbelow (True )
1266+
1267+ plt .tight_layout ()
1268+ plt .savefig (f'output/10_distance_to_shelter_line{ theme ["suffix" ]} .jpg' , dpi = 300 , bbox_inches = 'tight' ,
1269+ facecolor = theme ['background' ], format = 'jpeg' )
1270+ plt .close ()
1271+
1272+ # Print statistics
1273+ print (f" Distance to shelter statistics:" )
1274+ print (f" Buildings with shelter ≤500m: { len (distances_with_shelter ):,} " )
1275+ print (f" Mean distance: { np .mean (distances_with_shelter ):.1f} m" )
1276+ print (f" Median distance: { np .median (distances_with_shelter ):.1f} m" )
1277+
1278+
1279+ def create_local_density_200m (theme , local_density_data , theme_name = 'tufte' ):
1280+ """Create stacked histogram for 200m distance (buildings and shelters within 200m)"""
1281+ if not local_density_data or not local_density_data ['building_coords' ]:
1282+ return
1283+
1284+ building_coords = local_density_data ['building_coords' ]
1285+
1286+ # Load existing shelters
1287+ try :
1288+ with open ('data/shelters.geojson' , 'r' , encoding = 'utf-8' ) as f :
1289+ shelters_data = json .load (f )
1290+ existing_shelters = []
1291+ for feature in shelters_data ['features' ]:
1292+ props = feature ['properties' ]
1293+ status = props .get ('status' , '' ).strip ()
1294+ if status .startswith ('Built' ):
1295+ coords = feature ['geometry' ]['coordinates' ]
1296+ existing_shelters .append ([coords [0 ], coords [1 ]])
1297+ except FileNotFoundError :
1298+ existing_shelters = []
1299+
1300+ if not existing_shelters :
1301+ return
1302+
1303+ # Calculate buildings within 200m
1304+ print (" Calculating buildings within 200m..." )
1305+ local_densities_200m = calculate_local_building_density (building_coords , radius_m = 200 )
1306+ if local_densities_200m is None :
1307+ return
1308+
1309+ local_densities_200m = np .array (local_densities_200m )
1310+
1311+ # Calculate closest shelter distance within 200m
1312+ print (" Calculating closest shelter distances within 200m..." )
1313+ closest_distances_deg = calculate_closest_shelter_distance (building_coords , existing_shelters , max_radius_m = 200 )
1314+ if closest_distances_deg is None :
1315+ return
1316+
1317+ # Categorize buildings
1318+ has_shelter = closest_distances_deg <= (200 / 100000 )
1319+ densities_with_shelter = local_densities_200m [has_shelter ]
1320+ densities_without_shelter = local_densities_200m [~ has_shelter ]
1321+
1322+ _ , ax = plt .subplots (figsize = (10 , 6 ))
1323+
1324+ # Create bins
1325+ max_density = int (np .max (local_densities_200m ))
1326+ bins = np .arange (0 , max_density + 5 , 5 )
1327+
1328+ # Create stacked histogram
1329+ colors_simple = [
1330+ theme ['existing_color' ], # no_shelter (red)
1331+ theme ['optimal_color' ], # has shelter (green)
1332+ ]
1333+
1334+ data_layers = [
1335+ densities_without_shelter ,
1336+ densities_with_shelter ,
1337+ ]
1338+
1339+ labels = ['No shelter' , 'Has shelter (≤200m)' ]
1340+
1341+ ax .hist (data_layers , bins = bins ,
1342+ color = colors_simple , alpha = 0.8 , edgecolor = 'none' , stacked = True , label = labels )
1343+
1344+ ax .set_xlabel ('Buildings within 200m' )
1345+ ax .set_ylabel ('Number of Buildings' )
1346+ ax .set_title ('Buildings within 200m of Each Building' , pad = 15 )
1347+
1348+ # Add legend
1349+ ax .legend (loc = 'upper right' , fontsize = 8 , frameon = False )
1350+
1351+ setup_tufte_axis (ax )
1352+ ax .grid (True , axis = 'y' , linewidth = 0.5 )
1353+ ax .set_axisbelow (True )
1354+
1355+ plt .tight_layout ()
1356+ plt .savefig (f'output/09c_local_density_200m{ theme ["suffix" ]} .jpg' , dpi = 300 , bbox_inches = 'tight' ,
1357+ facecolor = theme ['background' ], format = 'jpeg' )
1358+ plt .close ()
1359+
1360+ # Print statistics
1361+ total = len (has_shelter )
1362+ print (f" Local density statistics (buildings within 200m):" )
1363+ print (f" Mean: { np .mean (local_densities_200m ):.1f} " )
1364+ print (f" Buildings with shelter ≤200m: { np .sum (has_shelter ):,} ({ np .sum (has_shelter )/ total * 100 :.1f} %)" )
1365+ print (f" Buildings without shelter: { np .sum (~ has_shelter ):,} ({ np .sum (~ has_shelter )/ total * 100 :.1f} %)" )
12011366
12021367
12031368def main ():
@@ -1241,13 +1406,16 @@ def main():
12411406 create_accessibility_coverage_progression (theme , radius_data , coverage_radii )
12421407 create_density_scatter (theme , density_data )
12431408 create_local_density_distribution (theme , local_density_data , theme_name )
1409+ create_distance_to_shelter_line (theme , local_density_data )
1410+ create_local_density_200m (theme , local_density_data , theme_name )
12441411
12451412 print ("\n === GENERATION COMPLETE ===" )
12461413 print ("Generated chart files in output/ directory:" )
12471414 for chart in ['01_shelter_types' , '02_data_sources' , '03_coverage_analysis' ,
12481415 '04_buildings_covered' , '05_buildings_per_shelter' ,
12491416 '06_accessibility_coverage_progression' , '07_density_scatter' ,
1250- '09_local_density_distribution' , '09b_local_density_distribution_simple' ]:
1417+ '09_local_density_distribution' , '09b_local_density_distribution_simple' ,
1418+ '09c_local_density_200m' , '10_distance_to_shelter_line' ]:
12511419 print (f" - { chart } _tufte.jpg" )
12521420 print (f" - { chart } _dark.jpg" )
12531421
0 commit comments