Skip to content

Commit 27c8601

Browse files
feat(plotly): implement bubble-map-geographic (#3634)
## Implementation: `bubble-map-geographic` - plotly Implements the **plotly** version of `bubble-map-geographic`. **File:** `plots/bubble-map-geographic/implementations/plotly.py` **Parent Issue:** #3625 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/20873955145)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 3dbd924 commit 27c8601

2 files changed

Lines changed: 380 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
""" pyplots.ai
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
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
import plotly.graph_objects as go
10+
11+
12+
# Data: Major world cities with population (in millions)
13+
np.random.seed(42)
14+
15+
cities = [
16+
{"city": "Tokyo", "lat": 35.6762, "lon": 139.6503, "pop": 37.4, "region": "Asia"},
17+
{"city": "Delhi", "lat": 28.7041, "lon": 77.1025, "pop": 31.2, "region": "Asia"},
18+
{"city": "Shanghai", "lat": 31.2304, "lon": 121.4737, "pop": 27.8, "region": "Asia"},
19+
{"city": "São Paulo", "lat": -23.5505, "lon": -46.6333, "pop": 22.4, "region": "South America"},
20+
{"city": "Mexico City", "lat": 19.4326, "lon": -99.1332, "pop": 21.9, "region": "North America"},
21+
{"city": "Cairo", "lat": 30.0444, "lon": 31.2357, "pop": 21.3, "region": "Africa"},
22+
{"city": "Mumbai", "lat": 19.0760, "lon": 72.8777, "pop": 20.7, "region": "Asia"},
23+
{"city": "Beijing", "lat": 39.9042, "lon": 116.4074, "pop": 20.5, "region": "Asia"},
24+
{"city": "Dhaka", "lat": 23.8103, "lon": 90.4125, "pop": 22.5, "region": "Asia"},
25+
{"city": "Osaka", "lat": 34.6937, "lon": 135.5023, "pop": 19.2, "region": "Asia"},
26+
{"city": "New York", "lat": 40.7128, "lon": -74.0060, "pop": 18.8, "region": "North America"},
27+
{"city": "Karachi", "lat": 24.8607, "lon": 67.0011, "pop": 16.5, "region": "Asia"},
28+
{"city": "Buenos Aires", "lat": -34.6037, "lon": -58.3816, "pop": 15.2, "region": "South America"},
29+
{"city": "Istanbul", "lat": 41.0082, "lon": 28.9784, "pop": 15.4, "region": "Europe"},
30+
{"city": "Lagos", "lat": 6.5244, "lon": 3.3792, "pop": 14.9, "region": "Africa"},
31+
{"city": "Manila", "lat": 14.5995, "lon": 120.9842, "pop": 14.4, "region": "Asia"},
32+
{"city": "Rio de Janeiro", "lat": -22.9068, "lon": -43.1729, "pop": 13.5, "region": "South America"},
33+
{"city": "Los Angeles", "lat": 34.0522, "lon": -118.2437, "pop": 12.5, "region": "North America"},
34+
{"city": "Moscow", "lat": 55.7558, "lon": 37.6173, "pop": 12.5, "region": "Europe"},
35+
{"city": "Paris", "lat": 48.8566, "lon": 2.3522, "pop": 11.1, "region": "Europe"},
36+
{"city": "London", "lat": 51.5074, "lon": -0.1278, "pop": 9.5, "region": "Europe"},
37+
{"city": "Lima", "lat": -12.0464, "lon": -77.0428, "pop": 10.9, "region": "South America"},
38+
{"city": "Bangkok", "lat": 13.7563, "lon": 100.5018, "pop": 10.7, "region": "Asia"},
39+
{"city": "Jakarta", "lat": -6.2088, "lon": 106.8456, "pop": 10.6, "region": "Asia"},
40+
{"city": "Seoul", "lat": 37.5665, "lon": 126.9780, "pop": 9.9, "region": "Asia"},
41+
{"city": "Sydney", "lat": -33.8688, "lon": 151.2093, "pop": 5.4, "region": "Oceania"},
42+
{"city": "Melbourne", "lat": -37.8136, "lon": 144.9631, "pop": 5.0, "region": "Oceania"},
43+
{"city": "Toronto", "lat": 43.6532, "lon": -79.3832, "pop": 6.3, "region": "North America"},
44+
{"city": "Chicago", "lat": 41.8781, "lon": -87.6298, "pop": 8.9, "region": "North America"},
45+
{"city": "Singapore", "lat": 1.3521, "lon": 103.8198, "pop": 5.9, "region": "Asia"},
46+
]
47+
48+
df = pd.DataFrame(cities)
49+
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+
}
67+
68+
# Create figure with geo map
69+
fig = go.Figure()
70+
71+
# Add traces for each region to create proper legend
72+
for region in df["region"].unique():
73+
region_df = df[df["region"] == region]
74+
fig.add_trace(
75+
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),
79+
marker={
80+
"size": region_df["marker_size"],
81+
"color": region_colors[region],
82+
"opacity": 0.65,
83+
"line": {"width": 1.5, "color": "white"},
84+
"sizemode": "diameter",
85+
},
86+
name=region,
87+
hovertemplate="%{text}<extra></extra>",
88+
)
89+
)
90+
91+
# Update layout for 4800x2700
92+
fig.update_layout(
93+
title={
94+
"text": "World City Populations · bubble-map-geographic · plotly · pyplots.ai",
95+
"font": {"size": 32, "color": "#333333"},
96+
"x": 0.5,
97+
"xanchor": "center",
98+
},
99+
geo={
100+
"showland": True,
101+
"landcolor": "rgb(243, 243, 243)",
102+
"showocean": True,
103+
"oceancolor": "rgb(230, 240, 250)",
104+
"showcoastlines": True,
105+
"coastlinecolor": "rgb(150, 150, 150)",
106+
"coastlinewidth": 1,
107+
"showframe": True,
108+
"framecolor": "rgb(100, 100, 100)",
109+
"framewidth": 1,
110+
"showcountries": True,
111+
"countrycolor": "rgb(200, 200, 200)",
112+
"countrywidth": 0.5,
113+
"projection_type": "natural earth",
114+
"lataxis": {"range": [-60, 75]},
115+
"lonaxis": {"range": [-140, 180]},
116+
},
117+
legend={
118+
"title": {"text": "Region", "font": {"size": 20}},
119+
"font": {"size": 18},
120+
"itemsizing": "constant",
121+
"x": 0.02,
122+
"y": 0.35,
123+
"bgcolor": "rgba(255,255,255,0.85)",
124+
"bordercolor": "rgba(0,0,0,0.2)",
125+
"borderwidth": 1,
126+
},
127+
margin={"l": 20, "r": 20, "t": 80, "b": 20},
128+
template="plotly_white",
129+
)
130+
131+
# Add size legend annotation
132+
fig.add_annotation(
133+
text="Bubble size = Population (millions)",
134+
xref="paper",
135+
yref="paper",
136+
x=0.02,
137+
y=0.15,
138+
showarrow=False,
139+
font={"size": 16, "color": "#555555"},
140+
align="left",
141+
)
142+
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()))
147+
fig.add_annotation(
148+
text=f" {pop_val}M",
149+
xref="paper",
150+
yref="paper",
151+
x=0.065,
152+
y=0.09 - i * 0.028,
153+
showarrow=False,
154+
font={"size": 14, "color": "#666666"},
155+
align="left",
156+
)
157+
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")
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
library: plotly
2+
specification_id: bubble-map-geographic
3+
created: '2026-01-10T06:09:27Z'
4+
updated: '2026-01-10T06:13:11Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20873955145
7+
issue: 3625
8+
python_version: 3.13.11
9+
library_version: 6.5.1
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/plotly/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/plotly/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/plotly/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Excellent geographic visualization using Plotly's native Scattergeo with Natural
17+
Earth projection
18+
- Proper area scaling using sqrt transform for perceptually accurate bubble sizes
19+
- Well-designed colorblind-safe palette for 6 distinct regions
20+
- Comprehensive hover tooltips showing city name and population
21+
- Clean code structure with clear data preparation
22+
- Both PNG and interactive HTML outputs generated
23+
weaknesses:
24+
- Size legend shows text labels (5M, 20M, 35M) but lacks corresponding visual bubble
25+
indicators to demonstrate the actual sizes
26+
- Could leverage more Plotly-specific features like linked hover highlighting or
27+
custom zoom controls
28+
image_description: 'The plot displays a world map using the Natural Earth projection
29+
with a light gray landmass and pale blue ocean. Thirty major world cities are
30+
represented as bubbles sized proportionally to their population (in millions).
31+
The bubbles are color-coded by region using a 6-color palette: blue for Asia,
32+
orange for South America, green for North America, purple for Africa, yellow for
33+
Europe, and cyan for Oceania. The largest bubbles (Tokyo, Delhi, Shanghai) are
34+
clearly visible in Asia. A region legend is positioned in the lower-left corner
35+
with a white semi-transparent background. Below the legend, a size legend annotation
36+
explains "Bubble size = Population (millions)" with example sizes for 5M, 20M,
37+
and 35M. The title at the top follows the required format. Country boundaries
38+
are shown as subtle gray lines, and the map uses appropriate latitude/longitude
39+
constraints to show all populated continents.'
40+
criteria_checklist:
41+
visual_quality:
42+
score: 36
43+
max: 40
44+
items:
45+
- id: VQ-01
46+
name: Text Legibility
47+
score: 9
48+
max: 10
49+
passed: true
50+
comment: Title at 32pt is excellent, legend text at 18-20pt is good. Size
51+
legend text at 14-16pt is slightly small but readable.
52+
- id: VQ-02
53+
name: No Overlap
54+
score: 8
55+
max: 8
56+
passed: true
57+
comment: No overlapping text elements. Bubbles intentionally overlap in dense
58+
areas but handled well with transparency.
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 7
62+
max: 8
63+
passed: true
64+
comment: Marker sizes scale well from small to large cities. Some smaller
65+
cities could be slightly more visible.
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 5
69+
max: 5
70+
passed: true
71+
comment: Six-color palette is colorblind-safe with good distinction between
72+
all colors.
73+
- id: VQ-05
74+
name: Layout Balance
75+
score: 5
76+
max: 5
77+
passed: true
78+
comment: Map fills canvas well, balanced margins, legend positioned appropriately.
79+
- id: VQ-06
80+
name: Axis Labels
81+
score: 2
82+
max: 2
83+
passed: true
84+
comment: N/A for geographic maps - no traditional axis labels needed.
85+
- id: VQ-07
86+
name: Grid & Legend
87+
score: 2
88+
max: 2
89+
passed: true
90+
comment: Region legend well-formatted with semi-transparent background. Size
91+
legend provides helpful context.
92+
spec_compliance:
93+
score: 25
94+
max: 25
95+
items:
96+
- id: SC-01
97+
name: Plot Type
98+
score: 8
99+
max: 8
100+
passed: true
101+
comment: Correctly implements geographic bubble map with Scattergeo.
102+
- id: SC-02
103+
name: Data Mapping
104+
score: 5
105+
max: 5
106+
passed: true
107+
comment: Latitude/longitude correctly mapped, population correctly encoded
108+
as bubble size.
109+
- id: SC-03
110+
name: Required Features
111+
score: 5
112+
max: 5
113+
passed: true
114+
comment: 'All spec requirements met: size legend, transparency, geographic
115+
context, hover tooltips.'
116+
- id: SC-04
117+
name: Data Range
118+
score: 3
119+
max: 3
120+
passed: true
121+
comment: Map shows all data points with appropriate lat/lon range.
122+
- id: SC-05
123+
name: Legend Accuracy
124+
score: 2
125+
max: 2
126+
passed: true
127+
comment: Region legend is accurate; size legend explains the scaling.
128+
- id: SC-06
129+
name: Title Format
130+
score: 2
131+
max: 2
132+
passed: true
133+
comment: Uses correct format with spec-id, library, and pyplots.ai.
134+
data_quality:
135+
score: 18
136+
max: 20
137+
items:
138+
- id: DQ-01
139+
name: Feature Coverage
140+
score: 7
141+
max: 8
142+
passed: true
143+
comment: Shows good range of populations across 6 regions. Could include more
144+
small cities.
145+
- id: DQ-02
146+
name: Realistic Context
147+
score: 7
148+
max: 7
149+
passed: true
150+
comment: Real-world city population data with accurate coordinates.
151+
- id: DQ-03
152+
name: Appropriate Scale
153+
score: 4
154+
max: 5
155+
passed: true
156+
comment: Population values are realistic and current.
157+
code_quality:
158+
score: 9
159+
max: 10
160+
items:
161+
- id: CQ-01
162+
name: KISS Structure
163+
score: 3
164+
max: 3
165+
passed: true
166+
comment: 'Clean linear structure: imports, data, size calculation, figure,
167+
save.'
168+
- id: CQ-02
169+
name: Reproducibility
170+
score: 3
171+
max: 3
172+
passed: true
173+
comment: Uses np.random.seed(42).
174+
- id: CQ-03
175+
name: Clean Imports
176+
score: 2
177+
max: 2
178+
passed: true
179+
comment: Only numpy, pandas, and plotly.graph_objects imported.
180+
- id: CQ-04
181+
name: No Deprecated API
182+
score: 1
183+
max: 1
184+
passed: true
185+
comment: Uses current Plotly API.
186+
- id: CQ-05
187+
name: Output Correct
188+
score: 0
189+
max: 1
190+
passed: false
191+
comment: Size legend text labels lack corresponding visual bubble indicators.
192+
library_features:
193+
score: 3
194+
max: 5
195+
items:
196+
- id: LF-01
197+
name: Distinctive Features
198+
score: 3
199+
max: 5
200+
passed: true
201+
comment: Uses Scattergeo and generates interactive HTML with hover tooltips.
202+
Could leverage more Plotly features.
203+
verdict: APPROVED
204+
impl_tags:
205+
dependencies: []
206+
techniques:
207+
- annotations
208+
- hover-tooltips
209+
- html-export
210+
- iteration-over-groups
211+
patterns:
212+
- data-generation
213+
- iteration-over-groups
214+
dataprep:
215+
- normalization
216+
styling:
217+
- alpha-blending
218+
- edge-highlighting

0 commit comments

Comments
 (0)