Skip to content

Commit ed4f501

Browse files
feat(plotnine): implement map-tile-background (#7757)
## Implementation: `map-tile-background` - python/plotnine Implements the **python/plotnine** version of `map-tile-background`. **File:** `plots/map-tile-background/implementations/python/plotnine.py` **Parent Issue:** #3756 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26520398115)* --------- 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 7c6355e commit ed4f501

2 files changed

Lines changed: 228 additions & 187 deletions

File tree

plots/map-tile-background/implementations/python/plotnine.py

Lines changed: 79 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
map-tile-background: Map with Tile Background
3-
Library: plotnine 0.15.2 | Python 3.13.11
4-
Quality: 90/100 | Created: 2026-01-20
3+
Library: plotnine 0.15.4 | Python 3.13.13
4+
Quality: 82/100 | Updated: 2026-05-27
55
"""
66

7+
import os
8+
import sys
9+
710
import numpy as np
811
import pandas as pd
9-
from plotnine import (
12+
13+
14+
# Work around naming conflict with plotnine.py script and plotnine package
15+
script_dir = os.path.dirname(os.path.abspath(__file__))
16+
if script_dir in sys.path:
17+
sys.path.remove(script_dir)
18+
if "" in sys.path:
19+
sys.path.remove("")
20+
if "." in sys.path:
21+
sys.path.remove(".")
22+
23+
from plotnine import ( # noqa: E402
1024
aes,
1125
annotate,
1226
coord_fixed,
@@ -27,7 +41,15 @@
2741
)
2842

2943

30-
# Seed for reproducibility
44+
THEME = os.getenv("ANYPLOT_THEME", "light")
45+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
46+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
47+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
48+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
49+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
50+
51+
ANYPLOT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
52+
3153
np.random.seed(42)
3254

3355
# San Francisco Bay Area landmarks with visitor counts (thousands per year)
@@ -106,26 +128,25 @@
106128
df = pd.DataFrame(landmarks_data)
107129

108130
# Simulated tile-style background using grid rectangles
109-
# This creates a visual effect similar to map tiles
110131
lon_min, lon_max = -122.52, -122.36
111132
lat_min, lat_max = 37.755, 37.84
112133

113-
# Create grid cells that simulate map tiles (10x8 grid)
114134
n_tiles_x = 10
115135
n_tiles_y = 8
116136
tile_width = (lon_max - lon_min) / n_tiles_x
117137
tile_height = (lat_max - lat_min) / n_tiles_y
118138

119-
# Generate tile background with terrain-like coloring
139+
# Theme-adaptive terrain colors
140+
water_color = "#B8D4E8" if THEME == "light" else "#1D2E3A"
141+
land_color = "#E8E4D8" if THEME == "light" else "#2B2820"
142+
coast_color = "#8A7A6B" if THEME == "light" else "#7A7268"
143+
120144
tiles = []
121-
tile_id = 0
122145
for i in range(n_tiles_x):
123146
for j in range(n_tiles_y):
124147
x_center = lon_min + tile_width * (i + 0.5)
125148
y_center = lat_min + tile_height * (j + 0.5)
126149

127-
# Determine terrain type based on position (simulating land/water)
128-
# Water on east side (bay) and portions of north (golden gate strait)
129150
is_water = (
130151
(x_center > -122.39 and y_center < 37.79)
131152
or (x_center > -122.44 and y_center > 37.825)
@@ -139,14 +160,12 @@
139160
"ymin": lat_min + tile_height * j,
140161
"ymax": lat_min + tile_height * (j + 1),
141162
"terrain": "water" if is_water else "land",
142-
"tile_id": tile_id,
143163
}
144164
)
145-
tile_id += 1
146165

147166
df_tiles = pd.DataFrame(tiles)
148167

149-
# Coastline polygon (San Francisco peninsula outline for visible area)
168+
# Coastline polygon (San Francisco peninsula outline)
150169
coast_coords = [
151170
(-122.52, 37.755),
152171
(-122.48, 37.755),
@@ -165,94 +184,79 @@
165184
coastline = [{"region": "sf", "order": i, "lon": c[0], "lat": c[1]} for i, c in enumerate(coast_coords)]
166185
df_coast = pd.DataFrame(coastline)
167186

168-
# Category color palette (colorblind-safe)
169-
category_colors = {
170-
"Cultural": "#9467BD",
171-
"Historic": "#8C564B",
172-
"Landmark": "#306998",
173-
"Museum": "#2CA02C",
174-
"Shopping": "#FFD43B",
175-
"Sports": "#D62728",
176-
"Tourism": "#17BECF",
177-
"Transport": "#FF7F0E",
178-
}
179-
180-
# Prepare label data with individual position adjustments to avoid overlap
181-
# Select top attractions by visitor count
182-
top_attractions = df[df["visitors"] >= 10000].copy()
187+
# anyplot palette assigned alphabetically by category
188+
categories_sorted = sorted(df["category"].unique())
189+
category_colors = {cat: ANYPLOT_PALETTE[i] for i, cat in enumerate(categories_sorted)}
183190

184-
# Create label positions with nudge offsets to prevent overlap
185-
# Labels positioned with significant separation to avoid any touching
191+
# Labels for top 3 most-visited landmarks (well-separated geographically)
192+
top3 = df.nlargest(3, "visitors") # Union Square, Fisherman's Wharf, Golden Gate Bridge
186193
label_positions = {
187-
"Golden Gate Bridge": {"nudge_x": 0.01, "nudge_y": 0.016},
188-
"Fisherman's Wharf": {"nudge_x": -0.055, "nudge_y": 0.02},
189-
"Pier 39": {"nudge_x": 0.045, "nudge_y": -0.025},
190-
"Union Square": {"nudge_x": 0.025, "nudge_y": 0.015},
194+
"Union Square": {"nudge_x": 0.025, "nudge_y": 0.012},
195+
"Fisherman's Wharf": {"nudge_x": -0.035, "nudge_y": 0.014},
196+
"Golden Gate Bridge": {"nudge_x": 0.015, "nudge_y": 0.014},
191197
}
192198

193-
# Build label dataframe with adjusted positions
194199
label_records = []
195-
for _, row in top_attractions.iterrows():
200+
for _, row in top3.iterrows():
196201
pos = label_positions.get(row["name"], {"nudge_x": 0, "nudge_y": 0.012})
197202
label_records.append({"name": row["name"], "lon": row["lon"] + pos["nudge_x"], "lat": row["lat"] + pos["nudge_y"]})
198203
label_df = pd.DataFrame(label_records)
199204

200-
# Create the map visualization
205+
title = "map-tile-background · python · plotnine · anyplot.ai"
206+
201207
plot = (
202208
ggplot()
203-
# Layer 1: Tile background rectangles
204209
+ geom_rect(
205210
aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax", fill="terrain"),
206211
data=df_tiles,
207-
color="#AAAAAA",
208-
size=0.15,
209-
alpha=0.8,
212+
color=INK_MUTED,
213+
size=0.05,
214+
alpha=0.85,
210215
)
211-
+ scale_fill_manual(values={"water": "#B8D4E8", "land": "#E8E4D8"}, guide=None)
212-
# Layer 2: Coastline outline for geographic context
213-
+ geom_polygon(
214-
aes(x="lon", y="lat", group="region"), data=df_coast, fill="none", color="#444444", size=1.2, alpha=1.0
215-
)
216-
# Layer 3: Data points with size encoding for visitor counts
217-
+ geom_point(aes(x="lon", y="lat", color="category", size="visitors"), data=df, alpha=0.85, stroke=1.2)
218-
+ scale_size_continuous(range=(5, 20), name="Visitors\n(thousands/yr)")
216+
+ scale_fill_manual(values={"water": water_color, "land": land_color}, guide=None)
217+
+ geom_polygon(aes(x="lon", y="lat", group="region"), data=df_coast, fill="none", color=coast_color, size=0.8)
218+
+ geom_point(aes(x="lon", y="lat", color="category", size="visitors"), data=df, alpha=0.88, stroke=0.4)
219+
+ scale_size_continuous(range=(2, 10), name="Visitors\n(K/yr)")
219220
+ scale_color_manual(values=category_colors, name="Category")
220-
# Layer 4: Labels for top landmarks (positions pre-adjusted in label_df)
221221
+ geom_label(
222-
aes(x="lon", y="lat", label="name"), data=label_df, size=9, alpha=0.9, fill="white", label_padding=0.25
222+
aes(x="lon", y="lat", label="name"),
223+
data=label_df,
224+
size=3.5,
225+
alpha=0.92,
226+
fill=ELEVATED_BG,
227+
color=INK,
228+
label_padding=0.2,
223229
)
224-
# Attribution for simulated tile background (spec requirement)
225230
+ annotate(
226231
"text",
227-
x=lon_max - 0.01,
228-
y=lat_min + 0.005,
229-
label="Simulated tiles | Data: SF landmarks",
230-
size=7,
232+
x=lon_max - 0.003,
233+
y=lat_min + 0.003,
234+
label="Simulated tiles · SF landmarks",
235+
size=2.5,
231236
ha="right",
232237
va="bottom",
233-
color="#666666",
234-
alpha=0.8,
238+
color=INK_MUTED,
235239
)
236-
# Coordinate system with proper aspect ratio for geographic accuracy
237-
+ coord_fixed(ratio=1.3, xlim=(lon_min, lon_max), ylim=(lat_min, lat_max))
238-
+ labs(title="SF Landmarks · map-tile-background · plotnine · pyplots.ai", x="Longitude (°)", y="Latitude (°)")
240+
+ coord_fixed(ratio=1.06, xlim=(lon_min, lon_max), ylim=(lat_min, lat_max))
241+
+ labs(title=title, x="Longitude (°)", y="Latitude (°)")
239242
+ theme_minimal()
240243
+ theme(
241-
figure_size=(16, 9),
242-
plot_title=element_text(size=20, weight="bold", ha="center"),
243-
axis_title=element_text(size=18),
244-
axis_text=element_text(size=14),
245-
legend_title=element_text(size=14),
246-
legend_text=element_text(size=11),
247-
legend_position="right",
248-
legend_box_spacing=0.1,
249-
legend_key_size=16,
244+
figure_size=(8, 4.5),
245+
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
246+
panel_background=element_rect(fill=PAGE_BG),
250247
panel_grid_major=element_blank(),
251248
panel_grid_minor=element_blank(),
252-
panel_background=element_rect(fill="none"),
253-
plot_margin=0.02,
249+
panel_border=element_rect(color=INK_SOFT, fill=None),
250+
plot_title=element_text(size=12, weight="bold", ha="center", color=INK),
251+
axis_title=element_text(size=10, color=INK),
252+
axis_text=element_text(size=8, color=INK_SOFT),
253+
legend_title=element_text(size=9, color=INK),
254+
legend_text=element_text(size=8, color=INK_SOFT),
255+
legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
256+
legend_position="right",
257+
legend_key_size=10,
258+
plot_margin=0.01,
254259
)
255260
)
256261

257-
# Save at 300 DPI for 4800x2700 px output
258-
plot.save("plot.png", dpi=300, verbose=False)
262+
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

0 commit comments

Comments
 (0)