Skip to content

Commit a970bdc

Browse files
feat(plotly): implement bubble-map-geographic (#7290)
## Implementation: `bubble-map-geographic` - python/plotly Implements the **python/plotly** version of `bubble-map-geographic`. **File:** `plots/bubble-map-geographic/implementations/python/plotly.py` **Parent Issue:** #3625 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26064039573)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent 6e1ebfc commit a970bdc

2 files changed

Lines changed: 269 additions & 183 deletions

File tree

Lines changed: 113 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
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
55
"""
66

7+
import os
8+
79
import numpy as np
810
import pandas as pd
911
import plotly.graph_objects as go
1012

1113

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+
1237
# Data: Major world cities with population (in millions)
1338
np.random.seed(42)
1439

@@ -47,38 +72,26 @@
4772

4873
df = pd.DataFrame(cities)
4974

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))
6779

68-
# Create figure with geo map
80+
# Plot
6981
fig = go.Figure()
7082

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
7487
fig.add_trace(
7588
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),
7992
marker={
80-
"size": region_df["marker_size"],
81-
"color": region_colors[region],
93+
"size": rdf["marker_size"],
94+
"color": REGION_COLORS[region],
8295
"opacity": 0.65,
8396
"line": {"width": 1.5, "color": "white"},
8497
"sizemode": "diameter",
@@ -88,75 +101,113 @@
88101
)
89102
)
90103

91-
# Update layout for 4800x2700
104+
# Style
92105
fig.update_layout(
106+
paper_bgcolor=PAGE_BG,
107+
plot_bgcolor=PAGE_BG,
93108
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},
96111
"x": 0.5,
97112
"xanchor": "center",
98113
},
99114
geo={
100115
"showland": True,
101-
"landcolor": "rgb(243, 243, 243)",
116+
"landcolor": LAND_COLOR,
102117
"showocean": True,
103-
"oceancolor": "rgb(230, 240, 250)",
118+
"oceancolor": OCEAN_COLOR,
104119
"showcoastlines": True,
105-
"coastlinecolor": "rgb(150, 150, 150)",
120+
"coastlinecolor": COAST_COLOR,
106121
"coastlinewidth": 1,
107122
"showframe": True,
108-
"framecolor": "rgb(100, 100, 100)",
123+
"framecolor": INK_SOFT,
109124
"framewidth": 1,
110125
"showcountries": True,
111-
"countrycolor": "rgb(200, 200, 200)",
126+
"countrycolor": COUNTRY_COLOR,
112127
"countrywidth": 0.5,
113128
"projection_type": "natural earth",
114129
"lataxis": {"range": [-60, 75]},
115130
"lonaxis": {"range": [-140, 180]},
131+
"bgcolor": PAGE_BG,
116132
},
117133
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},
120136
"itemsizing": "constant",
121137
"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,
125143
"borderwidth": 1,
126144
},
127145
margin={"l": 20, "r": 20, "t": 80, "b": 20},
128-
template="plotly_white",
129146
)
130147

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+
132166
fig.add_annotation(
133-
text="Bubble size = Population (millions)",
167+
text="<b>Population scale</b>",
134168
xref="paper",
135169
yref="paper",
136-
x=0.02,
137-
y=0.15,
170+
x=0.100,
171+
y=0.252,
138172
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",
141177
)
142178

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+
)
147198
fig.add_annotation(
148-
text=f" {pop_val}M",
199+
text=label,
149200
xref="paper",
150201
yref="paper",
151-
x=0.065,
152-
y=0.09 - i * 0.028,
202+
x=cx + rx + 0.012,
203+
y=y_c,
153204
showarrow=False,
154-
font={"size": 14, "color": "#666666"},
205+
font={"size": 15, "color": INK_SOFT},
155206
align="left",
207+
xanchor="left",
208+
yanchor="middle",
156209
)
157210

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

Comments
 (0)