|
| 1 | +""" pyplots.ai |
| 2 | +choropleth-basic: Choropleth Map with Regional Coloring |
| 3 | +Library: bokeh 3.8.1 | Python 3.13.11 |
| 4 | +Quality: 91/100 | Created: 2025-12-31 |
| 5 | +""" |
| 6 | + |
| 7 | +import math |
| 8 | + |
| 9 | +from bokeh.io import export_png |
| 10 | +from bokeh.models import ColorBar, ColumnDataSource, LabelSet, LogColorMapper |
| 11 | +from bokeh.palettes import Blues9 |
| 12 | +from bokeh.plotting import figure |
| 13 | + |
| 14 | + |
| 15 | +# Data: US states arranged in a tile grid map layout (all 50 states + DC) |
| 16 | +# Grid positions (col, row) - approximating geographic positions |
| 17 | +regions = { |
| 18 | + "AK": (0, 5), |
| 19 | + "HI": (0, 1), |
| 20 | + "WA": (1, 5), |
| 21 | + "OR": (1, 4), |
| 22 | + "CA": (0, 3), |
| 23 | + "NV": (1, 3), |
| 24 | + "ID": (2, 5), |
| 25 | + "MT": (3, 5), |
| 26 | + "WY": (3, 4), |
| 27 | + "UT": (2, 3), |
| 28 | + "AZ": (2, 2), |
| 29 | + "CO": (3, 3), |
| 30 | + "NM": (3, 2), |
| 31 | + "ND": (4, 5), |
| 32 | + "SD": (4, 4), |
| 33 | + "NE": (4, 3), |
| 34 | + "KS": (4, 2), |
| 35 | + "OK": (4, 1), |
| 36 | + "TX": (3, 1), |
| 37 | + "MN": (5, 5), |
| 38 | + "IA": (5, 4), |
| 39 | + "MO": (5, 3), |
| 40 | + "AR": (5, 2), |
| 41 | + "LA": (5, 1), |
| 42 | + "WI": (6, 5), |
| 43 | + "IL": (6, 4), |
| 44 | + "IN": (7, 4), |
| 45 | + "MI": (7, 5), |
| 46 | + "OH": (8, 4), |
| 47 | + "KY": (7, 3), |
| 48 | + "TN": (6, 3), |
| 49 | + "MS": (6, 2), |
| 50 | + "AL": (7, 2), |
| 51 | + "GA": (8, 2), |
| 52 | + "FL": (8, 1), |
| 53 | + "SC": (9, 2), |
| 54 | + "NC": (9, 3), |
| 55 | + "VA": (9, 4), |
| 56 | + "WV": (8, 3), |
| 57 | + "PA": (10, 4), |
| 58 | + "NY": (10, 5), |
| 59 | + "VT": (11, 5), |
| 60 | + "NH": (11, 4), |
| 61 | + "ME": (12, 5), |
| 62 | + "MA": (11, 3), |
| 63 | + "RI": (12, 3), |
| 64 | + "CT": (11, 2), |
| 65 | + "NJ": (10, 3), |
| 66 | + "DE": (10, 2), |
| 67 | + "MD": (9, 1), |
| 68 | + "DC": (10, 1), |
| 69 | +} |
| 70 | + |
| 71 | +# Population density values (people per sq mile) - realistic ranges |
| 72 | +density_values = { |
| 73 | + "AK": 1, |
| 74 | + "HI": 226, |
| 75 | + "CA": 253, |
| 76 | + "TX": 112, |
| 77 | + "FL": 411, |
| 78 | + "NY": 408, |
| 79 | + "PA": 286, |
| 80 | + "IL": 227, |
| 81 | + "OH": 289, |
| 82 | + "GA": 185, |
| 83 | + "NC": 218, |
| 84 | + "MI": 177, |
| 85 | + "NJ": 1263, |
| 86 | + "VA": 218, |
| 87 | + "WA": 117, |
| 88 | + "AZ": 64, |
| 89 | + "MA": 901, |
| 90 | + "TN": 167, |
| 91 | + "IN": 189, |
| 92 | + "MO": 89, |
| 93 | + "MD": 636, |
| 94 | + "WI": 108, |
| 95 | + "CO": 57, |
| 96 | + "MN": 71, |
| 97 | + "SC": 173, |
| 98 | + "AL": 99, |
| 99 | + "LA": 107, |
| 100 | + "KY": 114, |
| 101 | + "OR": 44, |
| 102 | + "OK": 58, |
| 103 | + "CT": 733, |
| 104 | + "IA": 57, |
| 105 | + "MS": 63, |
| 106 | + "AR": 58, |
| 107 | + "UT": 40, |
| 108 | + "NV": 28, |
| 109 | + "KS": 36, |
| 110 | + "NM": 17, |
| 111 | + "NE": 25, |
| 112 | + "WV": 74, |
| 113 | + "ID": 23, |
| 114 | + "ME": 44, |
| 115 | + "NH": 154, |
| 116 | + "RI": 1061, |
| 117 | + "MT": 8, |
| 118 | + "DE": 508, |
| 119 | + "SD": 12, |
| 120 | + "ND": 11, |
| 121 | + "VT": 68, |
| 122 | + "WY": 6, |
| 123 | + # DC intentionally missing to demonstrate missing data handling |
| 124 | +} |
| 125 | + |
| 126 | +# Prepare data for rectangles |
| 127 | +xs = [] |
| 128 | +ys = [] |
| 129 | +widths = [] |
| 130 | +heights = [] |
| 131 | +colors = [] |
| 132 | +abbrevs = [] |
| 133 | +abbrev_x = [] |
| 134 | +abbrev_y = [] |
| 135 | + |
| 136 | +# Color mapping - use log scale to better differentiate low-density states |
| 137 | +min_val = max(1, min(density_values.values())) # Log scale needs min > 0 |
| 138 | +max_val = max(density_values.values()) |
| 139 | +palette = list(reversed(Blues9)) |
| 140 | +color_mapper = LogColorMapper(palette=palette, low=min_val, high=max_val, nan_color="#d0d0d0") |
| 141 | + |
| 142 | +# Process each region - rect is centered, so adjust coords to center of tile |
| 143 | +for abbrev, (col, row) in regions.items(): |
| 144 | + center_x = col * 1.1 + 0.5 |
| 145 | + center_y = row * 1.1 + 0.5 |
| 146 | + xs.append(center_x) |
| 147 | + ys.append(center_y) |
| 148 | + widths.append(1.0) |
| 149 | + heights.append(1.0) |
| 150 | + abbrev_x.append(center_x) |
| 151 | + abbrev_y.append(center_y) |
| 152 | + abbrevs.append(abbrev) |
| 153 | + |
| 154 | + if abbrev in density_values: |
| 155 | + density = density_values[abbrev] |
| 156 | + # Map to color index using log scale for better low-value differentiation |
| 157 | + log_min = math.log10(min_val) |
| 158 | + log_max = math.log10(max_val) |
| 159 | + log_val = math.log10(max(density, 1)) # Ensure positive for log |
| 160 | + norm = (log_val - log_min) / (log_max - log_min) |
| 161 | + idx = min(int(norm * (len(palette) - 1)), len(palette) - 1) |
| 162 | + colors.append(palette[idx]) |
| 163 | + else: |
| 164 | + # Missing data - gray with pattern indication |
| 165 | + colors.append("#d0d0d0") |
| 166 | + |
| 167 | +# Create data sources |
| 168 | +rect_source = ColumnDataSource(data={"x": xs, "y": ys, "width": widths, "height": heights, "fill_color": colors}) |
| 169 | + |
| 170 | +# Determine text colors based on background (using log scale) |
| 171 | +text_colors = [] |
| 172 | +log_min = math.log10(min_val) |
| 173 | +log_max = math.log10(max_val) |
| 174 | +for abbrev in abbrevs: |
| 175 | + if abbrev not in density_values: |
| 176 | + text_colors.append("#333333") |
| 177 | + else: |
| 178 | + density = density_values[abbrev] |
| 179 | + log_val = math.log10(max(density, 1)) |
| 180 | + norm = (log_val - log_min) / (log_max - log_min) |
| 181 | + text_colors.append("white" if norm > 0.5 else "#306998") |
| 182 | + |
| 183 | +label_source = ColumnDataSource(data={"x": abbrev_x, "y": abbrev_y, "text": abbrevs, "text_color": text_colors}) |
| 184 | + |
| 185 | +# Create figure (4800x2700 for landscape) |
| 186 | +p = figure( |
| 187 | + width=4800, |
| 188 | + height=2700, |
| 189 | + title="choropleth-basic · bokeh · pyplots.ai", |
| 190 | + x_axis_location=None, |
| 191 | + y_axis_location=None, |
| 192 | + tools="", |
| 193 | + toolbar_location=None, |
| 194 | + x_range=(-1, 14), |
| 195 | + y_range=(0, 7), |
| 196 | +) |
| 197 | + |
| 198 | +# Remove grid and axes |
| 199 | +p.grid.grid_line_color = None |
| 200 | +p.outline_line_color = None |
| 201 | +p.xaxis.visible = False |
| 202 | +p.yaxis.visible = False |
| 203 | + |
| 204 | +# Draw rectangles for each state |
| 205 | +p.rect( |
| 206 | + x="x", |
| 207 | + y="y", |
| 208 | + width="width", |
| 209 | + height="height", |
| 210 | + source=rect_source, |
| 211 | + fill_color="fill_color", |
| 212 | + fill_alpha=0.9, |
| 213 | + line_color="white", |
| 214 | + line_width=2, |
| 215 | +) |
| 216 | + |
| 217 | +# Add state abbreviation labels |
| 218 | +labels = LabelSet( |
| 219 | + x="x", |
| 220 | + y="y", |
| 221 | + text="text", |
| 222 | + text_color="text_color", |
| 223 | + source=label_source, |
| 224 | + text_align="center", |
| 225 | + text_baseline="middle", |
| 226 | + text_font_size="18pt", |
| 227 | + text_font_style="bold", |
| 228 | +) |
| 229 | +p.add_layout(labels) |
| 230 | + |
| 231 | +# Title styling |
| 232 | +p.title.text_font_size = "32pt" |
| 233 | +p.title.align = "center" |
| 234 | + |
| 235 | +# Color bar for legend - made larger for prominence |
| 236 | +color_bar = ColorBar( |
| 237 | + color_mapper=color_mapper, |
| 238 | + width=60, |
| 239 | + height=1200, |
| 240 | + location=(0, 0), |
| 241 | + title="Population Density (per sq mile)", |
| 242 | + title_text_font_size="24pt", |
| 243 | + major_label_text_font_size="20pt", |
| 244 | + title_standoff=20, |
| 245 | + padding=30, |
| 246 | + margin=40, |
| 247 | +) |
| 248 | +p.add_layout(color_bar, "right") |
| 249 | + |
| 250 | +# Save output |
| 251 | +export_png(p, filename="plot.png") |
0 commit comments