Skip to content

Commit 490a59f

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

2 files changed

Lines changed: 397 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
""" pyplots.ai
2+
bubble-map-geographic: Bubble Map with Sized Geographic Markers
3+
Library: pygal 3.1.0 | Python 3.13.11
4+
Quality: 90/100 | Created: 2026-01-10
5+
"""
6+
7+
# Fix module name conflict (this file is named pygal.py)
8+
import sys
9+
10+
11+
_cwd = sys.path[0] if sys.path and sys.path[0] else None
12+
if _cwd:
13+
sys.path.remove(_cwd)
14+
15+
import numpy as np # noqa: E402
16+
import pygal # noqa: E402
17+
from pygal.style import Style # noqa: E402
18+
19+
20+
if _cwd:
21+
sys.path.insert(0, _cwd)
22+
23+
24+
# Data - Major world cities with population data (millions)
25+
np.random.seed(42)
26+
27+
cities = {
28+
"Tokyo": (35.68, 139.69, 37.4),
29+
"Delhi": (28.61, 77.21, 32.9),
30+
"Shanghai": (31.23, 121.47, 28.5),
31+
"Sao Paulo": (-23.55, -46.63, 22.4),
32+
"Mexico City": (19.43, -99.13, 21.8),
33+
"Cairo": (30.04, 31.24, 21.3),
34+
"Mumbai": (19.08, 72.88, 20.7),
35+
"Beijing": (39.90, 116.41, 20.5),
36+
"New York": (40.71, -74.01, 18.8),
37+
"Los Angeles": (34.05, -118.24, 12.5),
38+
"Paris": (48.86, 2.35, 11.0),
39+
"London": (51.51, -0.13, 9.5),
40+
"Moscow": (55.76, 37.62, 12.5),
41+
"Istanbul": (41.01, 28.98, 15.4),
42+
"Lagos": (6.52, 3.38, 14.9),
43+
"Buenos Aires": (-34.60, -58.38, 15.4),
44+
"Sydney": (-33.87, 151.21, 5.4),
45+
"Seoul": (37.57, 126.98, 9.8),
46+
"Bangkok": (13.76, 100.50, 10.7),
47+
"Jakarta": (-6.21, 106.85, 10.6),
48+
}
49+
50+
# Extract data
51+
names = list(cities.keys())
52+
lats = [cities[c][0] for c in names]
53+
lons = [cities[c][1] for c in names]
54+
populations = [cities[c][2] for c in names]
55+
56+
# Simplified world coastlines as XY series (longitude, latitude format for pygal XY)
57+
coastlines = [
58+
# North America
59+
[
60+
(-125, 50),
61+
(-141, 60),
62+
(-165, 55),
63+
(-168, 52),
64+
(-148, 60),
65+
(-130, 55),
66+
(-120, 49),
67+
(-95, 49),
68+
(-80, 45),
69+
(-67, 45),
70+
(-75, 35),
71+
(-81, 25),
72+
(-90, 30),
73+
(-97, 26),
74+
(-110, 32),
75+
(-125, 50),
76+
],
77+
# Mexico/Central America
78+
[(-117, 33), (-110, 25), (-97, 20), (-87, 16), (-80, 8), (-90, 22), (-110, 32), (-117, 33)],
79+
# South America
80+
[(-78, 10), (-60, 8), (-35, -6), (-42, -23), (-66, -55), (-72, -30), (-78, 10)],
81+
# Europe/Africa
82+
[(-10, 36), (10, 37), (30, 31), (42, 14), (35, -22), (17, -30), (0, 6), (-17, 14), (-10, 36)],
83+
# Northern Europe
84+
[(-6, 50), (5, 58), (28, 70), (24, 55), (3, 51), (-6, 50)],
85+
# Asia
86+
[(28, 70), (100, 77), (170, 60), (120, 32), (100, 14), (72, 25), (40, 46), (28, 70)],
87+
# India/SE Asia
88+
[(78, 33), (72, 8), (88, 22), (104, 2), (78, 33)],
89+
# Japan
90+
[(130, 32), (145, 44), (130, 32)],
91+
# Australia
92+
[(113, -22), (150, -23), (140, -38), (113, -22)],
93+
]
94+
95+
# Bin cities by population for bubble sizing
96+
small_cities = [] # < 12M
97+
medium_cities = [] # 12-20M
98+
large_cities = [] # 20-30M
99+
mega_cities = [] # > 30M
100+
101+
for i, name in enumerate(names):
102+
lat, lon, pop = lats[i], lons[i], populations[i]
103+
point = {"value": (lon, lat), "label": f"{name}: {pop}M"}
104+
105+
if pop < 12:
106+
small_cities.append(point)
107+
elif pop < 20:
108+
medium_cities.append(point)
109+
elif pop < 30:
110+
large_cities.append(point)
111+
else:
112+
mega_cities.append(point)
113+
114+
# Custom style for 4800x2700 canvas
115+
# Colors: 9 gray for coastlines, then 4 for population categories
116+
custom_style = Style(
117+
background="white",
118+
plot_background="#C8DDF0", # Ocean blue background
119+
foreground="#333333",
120+
foreground_strong="#111111",
121+
foreground_subtle="#666666",
122+
guide_stroke_color="#88888866", # Semi-transparent gray grid lines
123+
guide_stroke_dasharray="5,5", # Dashed grid lines for visibility
124+
colors=(
125+
# Gray for all coastlines (9 series)
126+
"#999999",
127+
"#999999",
128+
"#999999",
129+
"#999999",
130+
"#999999",
131+
"#999999",
132+
"#999999",
133+
"#999999",
134+
"#999999",
135+
# City bubble colors - distinct colors for population sizes
136+
"#306998", # Python Blue - small
137+
"#4a8cc2", # Light blue - medium
138+
"#FFD43B", # Python Yellow - large
139+
"#E24A33", # Red-orange - mega
140+
),
141+
opacity=0.7,
142+
opacity_hover=0.9,
143+
title_font_size=72,
144+
label_font_size=48,
145+
major_label_font_size=40,
146+
legend_font_size=40,
147+
value_font_size=36,
148+
tooltip_font_size=36,
149+
)
150+
151+
# Create XY chart
152+
chart = pygal.XY(
153+
width=4800,
154+
height=2700,
155+
style=custom_style,
156+
title="bubble-map-geographic · pygal · pyplots.ai",
157+
x_title="Longitude (°)",
158+
y_title="Latitude (°)",
159+
show_legend=True,
160+
legend_at_bottom=True,
161+
legend_at_bottom_columns=4,
162+
legend_box_size=30,
163+
stroke=True,
164+
dots_size=3,
165+
show_x_guides=True,
166+
show_y_guides=True,
167+
explicit_size=True,
168+
print_values=False,
169+
xrange=(-180, 180),
170+
range=(-60, 80),
171+
)
172+
173+
# Add coastlines as background (None title = no legend entry in pygal)
174+
for coords in coastlines:
175+
chart.add(None, coords, stroke=True, dots_size=0, show_dots=False, fill=False)
176+
177+
# Add city bubbles by population category (increased sizes for visibility)
178+
chart.add("Pop < 12M", small_cities, stroke=False, dots_size=28)
179+
chart.add("Pop 12-20M", medium_cities, stroke=False, dots_size=42)
180+
chart.add("Pop 20-30M", large_cities, stroke=False, dots_size=58)
181+
chart.add("Pop > 30M", mega_cities, stroke=False, dots_size=78)
182+
183+
# Save outputs (PNG first as primary, then HTML for interactivity)
184+
chart.render_to_png("plot.png")
185+
chart.render_to_file("plot.html")
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
library: pygal
2+
specification_id: bubble-map-geographic
3+
created: '2026-01-10T06:14:04Z'
4+
updated: '2026-01-10T06:22:11Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 20873960009
7+
issue: 3625
8+
python_version: 3.13.11
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/bubble-map-geographic/pygal/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent visual clarity with well-sized text and clear color differentiation
17+
- Effective use of custom coastline polygons to provide geographic context
18+
- Good use of pygal custom Style for controlling fonts and colors at high resolution
19+
- Interactive HTML output with hover tooltips showing city names and populations
20+
- Clean well-structured code following KISS principles
21+
weaknesses:
22+
- Bubble sizes are categorical (4 fixed sizes) rather than continuously scaled proportional
23+
to population values as the spec requires
24+
- Grid lines are somewhat prominent compared to ideal subtle alpha 0.2-0.4
25+
- Does not use pygal World map capability which could provide better geographic
26+
basemap
27+
image_description: 'The plot displays a geographic bubble map with 20 world cities
28+
plotted on an XY coordinate system where X represents Longitude (-180 to 160°)
29+
and Y represents Latitude (-60 to 80°). The background is a light blue (#C8DDF0)
30+
representing oceans, with simplified gray coastline outlines for continents (North
31+
America, South America, Europe, Africa, Asia, Australia). City bubbles are color-coded
32+
by population category: small dark blue (Pop < 12M), medium blue (Pop 12-20M),
33+
yellow (Pop 20-30M), and red-orange (Pop > 30M). Bubble sizes increase proportionally
34+
with population category. The two largest red-orange bubbles represent Tokyo and
35+
Delhi (>30M). The title "bubble-map-geographic · pygal · pyplots.ai" appears at
36+
the top. A legend at the bottom shows the four population categories. Grid lines
37+
are subtle dashed gray lines.'
38+
criteria_checklist:
39+
visual_quality:
40+
score: 36
41+
max: 40
42+
items:
43+
- id: VQ-01
44+
name: Text Legibility
45+
score: 10
46+
max: 10
47+
passed: true
48+
comment: 'All text clearly readable: title at 72pt, axis labels at 48pt, tick
49+
labels at 40pt, legend at 40pt'
50+
- id: VQ-02
51+
name: No Overlap
52+
score: 8
53+
max: 8
54+
passed: true
55+
comment: No overlapping text or elements
56+
- id: VQ-03
57+
name: Element Visibility
58+
score: 6
59+
max: 8
60+
passed: true
61+
comment: Bubbles are well-sized and visible; however size variation within
62+
each category is not shown
63+
- id: VQ-04
64+
name: Color Accessibility
65+
score: 5
66+
max: 5
67+
passed: true
68+
comment: Blue/yellow/red color scheme is colorblind-safe and provides good
69+
contrast
70+
- id: VQ-05
71+
name: Layout Balance
72+
score: 5
73+
max: 5
74+
passed: true
75+
comment: Good use of canvas with balanced margins, plot fills appropriate
76+
area
77+
- id: VQ-06
78+
name: Axis Labels
79+
score: 2
80+
max: 2
81+
passed: true
82+
comment: Longitude (°) and Latitude (°) with units
83+
- id: VQ-07
84+
name: Grid & Legend
85+
score: 0
86+
max: 2
87+
passed: false
88+
comment: Grid lines are quite visible/prominent, not subtle alpha 0.2-0.4
89+
spec_compliance:
90+
score: 23
91+
max: 25
92+
items:
93+
- id: SC-01
94+
name: Plot Type
95+
score: 8
96+
max: 8
97+
passed: true
98+
comment: Correct bubble map implementation using XY scatter with sized markers
99+
- id: SC-02
100+
name: Data Mapping
101+
score: 5
102+
max: 5
103+
passed: true
104+
comment: Longitude on X, Latitude on Y, population controlling size category
105+
- id: SC-03
106+
name: Required Features
107+
score: 3
108+
max: 5
109+
passed: false
110+
comment: 'Has sized bubbles, geographic context, transparency, tooltips. Missing:
111+
continuous size scaling'
112+
- id: SC-04
113+
name: Data Range
114+
score: 3
115+
max: 3
116+
passed: true
117+
comment: All data visible within range
118+
- id: SC-05
119+
name: Legend Accuracy
120+
score: 2
121+
max: 2
122+
passed: true
123+
comment: Legend correctly shows all four population categories
124+
- id: SC-06
125+
name: Title Format
126+
score: 2
127+
max: 2
128+
passed: true
129+
comment: 'Correct format: bubble-map-geographic · pygal · pyplots.ai'
130+
data_quality:
131+
score: 18
132+
max: 20
133+
items:
134+
- id: DQ-01
135+
name: Feature Coverage
136+
score: 6
137+
max: 8
138+
passed: true
139+
comment: Shows range of populations, good geographic spread. Categorical binning
140+
reduces continuous size variation demo
141+
- id: DQ-02
142+
name: Realistic Context
143+
score: 7
144+
max: 7
145+
passed: true
146+
comment: Real-world city population data, accurate locations, plausible values
147+
- id: DQ-03
148+
name: Appropriate Scale
149+
score: 5
150+
max: 5
151+
passed: true
152+
comment: Population values are realistic and well-documented (in millions)
153+
code_quality:
154+
score: 10
155+
max: 10
156+
items:
157+
- id: CQ-01
158+
name: KISS Structure
159+
score: 3
160+
max: 3
161+
passed: true
162+
comment: 'Simple linear flow: imports → data → coastlines → chart → save'
163+
- id: CQ-02
164+
name: Reproducibility
165+
score: 3
166+
max: 3
167+
passed: true
168+
comment: Uses np.random.seed(42)
169+
- id: CQ-03
170+
name: Clean Imports
171+
score: 2
172+
max: 2
173+
passed: true
174+
comment: Only necessary imports
175+
- id: CQ-04
176+
name: No Deprecated API
177+
score: 1
178+
max: 1
179+
passed: true
180+
comment: Uses current pygal API
181+
- id: CQ-05
182+
name: Output Correct
183+
score: 1
184+
max: 1
185+
passed: true
186+
comment: Saves as plot.png and plot.html
187+
library_features:
188+
score: 3
189+
max: 5
190+
items:
191+
- id: LF-01
192+
name: Distinctive Features
193+
score: 3
194+
max: 5
195+
passed: true
196+
comment: Good use of XY chart, custom Style, interactive HTML output. Does
197+
not use pygal World map charts
198+
verdict: APPROVED
199+
impl_tags:
200+
dependencies: []
201+
techniques:
202+
- hover-tooltips
203+
- html-export
204+
- custom-legend
205+
patterns:
206+
- data-generation
207+
- iteration-over-groups
208+
dataprep:
209+
- binning
210+
styling:
211+
- alpha-blending
212+
- grid-styling

0 commit comments

Comments
 (0)