|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | bubble-map-geographic: Bubble Map with Sized Geographic Markers |
3 | | -Library: plotly 6.5.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2026-01-10 |
| 3 | +Library: plotly 6.7.0 | Python 3.13.13 |
| 4 | +Quality: 86/100 | Updated: 2026-05-18 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import numpy as np |
8 | 10 | import pandas as pd |
9 | 11 | import plotly.graph_objects as go |
10 | 12 |
|
11 | 13 |
|
| 14 | +# Theme tokens |
| 15 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 16 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 17 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 18 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 19 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 20 | + |
| 21 | +LAND_COLOR = "#E8E4D8" if THEME == "light" else "#2E2E28" |
| 22 | +OCEAN_COLOR = "#D0E4F0" if THEME == "light" else "#1A2535" |
| 23 | +COAST_COLOR = "#B0A890" if THEME == "light" else "#4A4A40" |
| 24 | +COUNTRY_COLOR = "#C8C4B4" if THEME == "light" else "#3A3A34" |
| 25 | + |
| 26 | +# Okabe-Ito palette for regions (canonical order, first = #009E73) |
| 27 | +REGION_COLORS = { |
| 28 | + "Asia": "#009E73", |
| 29 | + "Europe": "#D55E00", |
| 30 | + "North America": "#0072B2", |
| 31 | + "South America": "#CC79A7", |
| 32 | + "Africa": "#E69F00", |
| 33 | + "Oceania": "#56B4E9", |
| 34 | +} |
| 35 | +REGION_ORDER = ["Asia", "Europe", "North America", "South America", "Africa", "Oceania"] |
| 36 | + |
12 | 37 | # Data: Major world cities with population (in millions) |
13 | 38 | np.random.seed(42) |
14 | 39 |
|
|
47 | 72 |
|
48 | 73 | df = pd.DataFrame(cities) |
49 | 74 |
|
50 | | -# Scale bubble sizes: area proportional to population |
51 | | -# Using sqrt to make area proportional (since marker size is diameter) |
52 | | -min_size = 15 |
53 | | -max_size = 70 |
54 | | -df["marker_size"] = min_size + (max_size - min_size) * np.sqrt( |
55 | | - (df["pop"] - df["pop"].min()) / (df["pop"].max() - df["pop"].min()) |
56 | | -) |
57 | | - |
58 | | -# Color palette for regions (colorblind-safe) |
59 | | -region_colors = { |
60 | | - "Asia": "#306998", # Python Blue |
61 | | - "Europe": "#FFD43B", # Python Yellow |
62 | | - "North America": "#2ca02c", # Green |
63 | | - "South America": "#ff7f0e", # Orange |
64 | | - "Africa": "#9467bd", # Purple |
65 | | - "Oceania": "#17becf", # Cyan |
66 | | -} |
| 75 | +# Bubble sizing: scale area proportional to population (sqrt of normalized value) |
| 76 | +min_size, max_size = 15, 70 |
| 77 | +pop_min, pop_max = df["pop"].min(), df["pop"].max() |
| 78 | +df["marker_size"] = min_size + (max_size - min_size) * np.sqrt((df["pop"] - pop_min) / (pop_max - pop_min)) |
67 | 79 |
|
68 | | -# Create figure with geo map |
| 80 | +# Plot |
69 | 81 | fig = go.Figure() |
70 | 82 |
|
71 | | -# Add traces for each region to create proper legend |
72 | | -for region in df["region"].unique(): |
73 | | - region_df = df[df["region"] == region] |
| 83 | +for region in REGION_ORDER: |
| 84 | + rdf = df[df["region"] == region] |
| 85 | + if rdf.empty: |
| 86 | + continue |
74 | 87 | fig.add_trace( |
75 | 88 | go.Scattergeo( |
76 | | - lon=region_df["lon"], |
77 | | - lat=region_df["lat"], |
78 | | - text=region_df.apply(lambda x: f"{x['city']}<br>Population: {x['pop']:.1f}M", axis=1), |
| 89 | + lon=rdf["lon"], |
| 90 | + lat=rdf["lat"], |
| 91 | + text=rdf.apply(lambda r: f"{r['city']}<br>Population: {r['pop']:.1f}M", axis=1), |
79 | 92 | marker={ |
80 | | - "size": region_df["marker_size"], |
81 | | - "color": region_colors[region], |
| 93 | + "size": rdf["marker_size"], |
| 94 | + "color": REGION_COLORS[region], |
82 | 95 | "opacity": 0.65, |
83 | 96 | "line": {"width": 1.5, "color": "white"}, |
84 | 97 | "sizemode": "diameter", |
|
88 | 101 | ) |
89 | 102 | ) |
90 | 103 |
|
91 | | -# Update layout for 4800x2700 |
| 104 | +# Style |
92 | 105 | fig.update_layout( |
| 106 | + paper_bgcolor=PAGE_BG, |
| 107 | + plot_bgcolor=PAGE_BG, |
93 | 108 | title={ |
94 | | - "text": "World City Populations · bubble-map-geographic · plotly · pyplots.ai", |
95 | | - "font": {"size": 32, "color": "#333333"}, |
| 109 | + "text": "World City Populations · bubble-map-geographic · python · plotly · anyplot.ai", |
| 110 | + "font": {"size": 28, "color": INK}, |
96 | 111 | "x": 0.5, |
97 | 112 | "xanchor": "center", |
98 | 113 | }, |
99 | 114 | geo={ |
100 | 115 | "showland": True, |
101 | | - "landcolor": "rgb(243, 243, 243)", |
| 116 | + "landcolor": LAND_COLOR, |
102 | 117 | "showocean": True, |
103 | | - "oceancolor": "rgb(230, 240, 250)", |
| 118 | + "oceancolor": OCEAN_COLOR, |
104 | 119 | "showcoastlines": True, |
105 | | - "coastlinecolor": "rgb(150, 150, 150)", |
| 120 | + "coastlinecolor": COAST_COLOR, |
106 | 121 | "coastlinewidth": 1, |
107 | 122 | "showframe": True, |
108 | | - "framecolor": "rgb(100, 100, 100)", |
| 123 | + "framecolor": INK_SOFT, |
109 | 124 | "framewidth": 1, |
110 | 125 | "showcountries": True, |
111 | | - "countrycolor": "rgb(200, 200, 200)", |
| 126 | + "countrycolor": COUNTRY_COLOR, |
112 | 127 | "countrywidth": 0.5, |
113 | 128 | "projection_type": "natural earth", |
114 | 129 | "lataxis": {"range": [-60, 75]}, |
115 | 130 | "lonaxis": {"range": [-140, 180]}, |
| 131 | + "bgcolor": PAGE_BG, |
116 | 132 | }, |
117 | 133 | legend={ |
118 | | - "title": {"text": "Region", "font": {"size": 20}}, |
119 | | - "font": {"size": 18}, |
| 134 | + "title": {"text": "Region", "font": {"size": 20, "color": INK}}, |
| 135 | + "font": {"size": 18, "color": INK_SOFT}, |
120 | 136 | "itemsizing": "constant", |
121 | 137 | "x": 0.02, |
122 | | - "y": 0.35, |
123 | | - "bgcolor": "rgba(255,255,255,0.85)", |
124 | | - "bordercolor": "rgba(0,0,0,0.2)", |
| 138 | + "y": 0.40, |
| 139 | + "xanchor": "left", |
| 140 | + "yanchor": "bottom", |
| 141 | + "bgcolor": ELEVATED_BG, |
| 142 | + "bordercolor": INK_SOFT, |
125 | 143 | "borderwidth": 1, |
126 | 144 | }, |
127 | 145 | margin={"l": 20, "r": 20, "t": 80, "b": 20}, |
128 | | - template="plotly_white", |
129 | 146 | ) |
130 | 147 |
|
131 | | -# Add size legend annotation |
| 148 | +# Visual size legend: circle shapes in paper coordinates |
| 149 | +FIG_W, FIG_H = 1600, 900 |
| 150 | +ref_pops = [35, 20, 5] |
| 151 | +ref_sizes_px = [min_size + (max_size - min_size) * np.sqrt((p - pop_min) / (pop_max - pop_min)) for p in ref_pops] |
| 152 | + |
| 153 | +fig.add_shape( |
| 154 | + type="rect", |
| 155 | + xref="paper", |
| 156 | + yref="paper", |
| 157 | + x0=0.005, |
| 158 | + y0=0.005, |
| 159 | + x1=0.195, |
| 160 | + y1=0.270, |
| 161 | + fillcolor=ELEVATED_BG, |
| 162 | + line={"color": INK_SOFT, "width": 1}, |
| 163 | + opacity=0.92, |
| 164 | +) |
| 165 | + |
132 | 166 | fig.add_annotation( |
133 | | - text="Bubble size = Population (millions)", |
| 167 | + text="<b>Population scale</b>", |
134 | 168 | xref="paper", |
135 | 169 | yref="paper", |
136 | | - x=0.02, |
137 | | - y=0.15, |
| 170 | + x=0.100, |
| 171 | + y=0.252, |
138 | 172 | showarrow=False, |
139 | | - font={"size": 16, "color": "#555555"}, |
140 | | - align="left", |
| 173 | + font={"size": 15, "color": INK}, |
| 174 | + align="center", |
| 175 | + xanchor="center", |
| 176 | + yanchor="top", |
141 | 177 | ) |
142 | 178 |
|
143 | | -# Example size indicators |
144 | | -size_examples = [5, 20, 35] |
145 | | -for i, pop_val in enumerate(size_examples): |
146 | | - size = min_size + (max_size - min_size) * np.sqrt((pop_val - df["pop"].min()) / (df["pop"].max() - df["pop"].min())) |
| 179 | +cx = 0.062 |
| 180 | +y_centers = [0.075, 0.160, 0.220] |
| 181 | +labels = ["35M", "20M", "5M"] |
| 182 | + |
| 183 | +for size_px, y_c, label in zip(ref_sizes_px, y_centers, labels, strict=False): |
| 184 | + rx = (size_px / 2) / FIG_W |
| 185 | + ry = (size_px / 2) / FIG_H |
| 186 | + fig.add_shape( |
| 187 | + type="circle", |
| 188 | + xref="paper", |
| 189 | + yref="paper", |
| 190 | + x0=cx - rx, |
| 191 | + y0=y_c - ry, |
| 192 | + x1=cx + rx, |
| 193 | + y1=y_c + ry, |
| 194 | + fillcolor=INK_SOFT, |
| 195 | + line={"color": PAGE_BG, "width": 1}, |
| 196 | + opacity=0.55, |
| 197 | + ) |
147 | 198 | fig.add_annotation( |
148 | | - text=f" {pop_val}M", |
| 199 | + text=label, |
149 | 200 | xref="paper", |
150 | 201 | yref="paper", |
151 | | - x=0.065, |
152 | | - y=0.09 - i * 0.028, |
| 202 | + x=cx + rx + 0.012, |
| 203 | + y=y_c, |
153 | 204 | showarrow=False, |
154 | | - font={"size": 14, "color": "#666666"}, |
| 205 | + font={"size": 15, "color": INK_SOFT}, |
155 | 206 | align="left", |
| 207 | + xanchor="left", |
| 208 | + yanchor="middle", |
156 | 209 | ) |
157 | 210 |
|
158 | | -# Save as PNG (4800x2700) |
159 | | -fig.write_image("plot.png", width=1600, height=900, scale=3) |
160 | | - |
161 | | -# Save as HTML for interactivity |
162 | | -fig.write_html("plot.html", include_plotlyjs="cdn") |
| 211 | +# Save |
| 212 | +fig.write_image(f"plot-{THEME}.png", width=FIG_W, height=FIG_H, scale=3) |
| 213 | +fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") |
0 commit comments