Skip to content

Commit ee764f4

Browse files
github-actions[bot]claudeMarkusNeusinger
authored
feat(plotly): implement map-tile-background (#7755)
## Implementation: `map-tile-background` - python/plotly Implements the **python/plotly** version of `map-tile-background`. **File:** `plots/map-tile-background/implementations/python/plotly.py` **Parent Issue:** #3756 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/anyplot/actions/runs/26520049241)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com>
1 parent ed4f501 commit ee764f4

3 files changed

Lines changed: 374 additions & 231 deletions

File tree

Lines changed: 189 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,136 +1,214 @@
1-
""" pyplots.ai
1+
""" anyplot.ai
22
map-tile-background: Map with Tile Background
3-
Library: plotly 6.5.2 | Python 3.13.11
4-
Quality: 91/100 | Created: 2026-01-20
3+
Library: plotly 6.7.0 | Python 3.13.13
4+
Quality: 87/100 | Updated: 2026-05-27
55
"""
66

7+
import os
8+
import sys
9+
10+
11+
# Remove this script's directory from sys.path so "plotly.py" doesn't shadow
12+
# the installed plotly package when Python prepends the script dir.
13+
_script_dir = os.path.dirname(os.path.abspath(__file__))
14+
sys.path = [p for p in sys.path if os.path.abspath(p) != _script_dir]
15+
716
import numpy as np
817
import plotly.graph_objects as go
918

1019

11-
# Data - European City Landmarks
20+
THEME = os.getenv("ANYPLOT_THEME", "light")
21+
22+
# Theme-adaptive chrome
23+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
24+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
25+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
26+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
27+
28+
# Anyplot sequential colorscale: brand green → blue (single-polarity continuous)
29+
imprint_seq = [[0.0, "#009E73"], [1.0, "#4467A3"]]
30+
31+
# Theme-adaptive map tile style
32+
map_style = "open-street-map" if THEME == "light" else "carto-darkmatter"
33+
34+
# Data — European City Landmarks with annual visitor counts
1235
np.random.seed(42)
1336

14-
# Major European city landmarks with simulated visitor counts
15-
landmarks = {
16-
"name": [
17-
"Eiffel Tower",
18-
"Colosseum",
19-
"Sagrada Familia",
20-
"Big Ben",
21-
"Brandenburg Gate",
22-
"Anne Frank House",
23-
"Acropolis",
24-
"Charles Bridge",
25-
"St. Stephen's Basilica",
26-
"Royal Palace",
27-
"Manneken Pis",
28-
"Tivoli Gardens",
29-
"Schonbrunn Palace",
30-
"Old Town Square",
31-
"Rialto Bridge",
32-
],
33-
"lat": [
34-
48.8584,
35-
41.8902,
36-
41.4036,
37-
51.5007,
38-
52.5163,
39-
52.3752,
40-
37.9715,
41-
50.0865,
42-
47.5008,
43-
59.3268,
44-
50.8450,
45-
55.6736,
46-
48.1845,
47-
50.0870,
48-
45.4380,
49-
],
50-
"lon": [
51-
2.2945,
52-
12.4922,
53-
2.1744,
54-
-0.1246,
55-
13.3777,
56-
4.8840,
57-
23.7267,
58-
14.4114,
59-
19.0538,
60-
18.0717,
61-
4.3499,
62-
12.5681,
63-
16.3119,
64-
14.4208,
65-
12.3358,
66-
],
67-
"visitors": [
68-
7000000,
69-
7400000,
70-
4500000,
71-
2000000,
72-
3000000,
73-
1300000,
74-
3000000,
75-
5000000,
76-
1000000,
77-
1500000,
78-
500000,
79-
4000000,
80-
4000000,
81-
6000000,
82-
5000000,
83-
],
84-
}
37+
names = [
38+
"Eiffel Tower",
39+
"Colosseum",
40+
"Sagrada Familia",
41+
"Big Ben",
42+
"Brandenburg Gate",
43+
"Anne Frank House",
44+
"Acropolis",
45+
"Charles Bridge",
46+
"St. Stephen's Basilica",
47+
"Royal Palace",
48+
"Manneken Pis",
49+
"Tivoli Gardens",
50+
"Schönbrunn Palace",
51+
"Dubrovnik Old Walls",
52+
"Rialto Bridge",
53+
]
54+
lats = [
55+
48.8584, # Eiffel Tower
56+
41.8902, # Colosseum
57+
41.4036, # Sagrada Familia
58+
51.5007, # Big Ben
59+
52.5163, # Brandenburg Gate
60+
52.3752, # Anne Frank House
61+
37.9715, # Acropolis
62+
50.0865, # Charles Bridge
63+
47.5008, # St. Stephen's Basilica
64+
59.3268, # Royal Palace
65+
50.8450, # Manneken Pis
66+
55.6736, # Tivoli Gardens
67+
48.1845, # Schönbrunn Palace
68+
42.6411, # Dubrovnik Old Walls
69+
45.4380, # Rialto Bridge
70+
]
71+
lons = [
72+
2.2945, # Eiffel Tower
73+
12.4922, # Colosseum
74+
2.1744, # Sagrada Familia
75+
-0.1246, # Big Ben
76+
13.3777, # Brandenburg Gate
77+
4.8840, # Anne Frank House
78+
23.7267, # Acropolis
79+
14.4114, # Charles Bridge
80+
19.0538, # St. Stephen's Basilica
81+
18.0717, # Royal Palace
82+
4.3499, # Manneken Pis
83+
12.5681, # Tivoli Gardens
84+
16.3119, # Schönbrunn Palace
85+
18.1085, # Dubrovnik Old Walls
86+
12.3358, # Rialto Bridge
87+
]
88+
visitors = np.array(
89+
[
90+
7_000_000, # Eiffel Tower
91+
7_400_000, # Colosseum
92+
4_500_000, # Sagrada Familia
93+
2_000_000, # Big Ben
94+
3_000_000, # Brandenburg Gate
95+
1_300_000, # Anne Frank House
96+
3_000_000, # Acropolis
97+
5_000_000, # Charles Bridge
98+
1_000_000, # St. Stephen's Basilica
99+
1_500_000, # Royal Palace
100+
500_000, # Manneken Pis
101+
4_000_000, # Tivoli Gardens
102+
4_000_000, # Schönbrunn Palace
103+
1_200_000, # Dubrovnik Old Walls
104+
5_000_000, # Rialto Bridge
105+
]
106+
)
85107

86-
# Convert visitors to marker sizes (scaled for visibility)
87-
visitors = np.array(landmarks["visitors"])
108+
# Scale visitor counts to marker diameters (15–50 px range)
88109
sizes = 15 + (visitors - visitors.min()) / (visitors.max() - visitors.min()) * 35
110+
vmin, vmax = int(visitors.min()), int(visitors.max())
111+
112+
# Split into two groups to alternate textposition for the crowded NW Europe cluster:
113+
# "bottom center" for Big Ben, St. Stephen's, Schönbrunn, Manneken Pis, Colosseum, Rialto
114+
bottom_idx = {3, 8, 12, 10, 1, 14}
115+
top_idx = set(range(len(names))) - bottom_idx
116+
117+
118+
def _select(seq, indices):
119+
return [seq[i] for i in sorted(indices)]
120+
89121

90-
# Create the map figure with tile background
91122
fig = go.Figure()
92123

93-
# Add scattermap trace for data points on tile background (new API replacing scattermapbox)
124+
# Main trace (top center labels) — carries colorbar
94125
fig.add_trace(
95126
go.Scattermap(
96-
lat=landmarks["lat"],
97-
lon=landmarks["lon"],
127+
lat=_select(lats, top_idx),
128+
lon=_select(lons, top_idx),
98129
mode="markers+text",
99-
marker=dict(
100-
size=sizes,
101-
color=visitors,
102-
colorscale=[[0, "#306998"], [0.5, "#4a8bc2"], [1, "#FFD43B"]],
103-
colorbar=dict(
104-
title=dict(text="Annual Visitors", font=dict(size=18)), tickfont=dict(size=14), thickness=20, len=0.7
105-
),
106-
opacity=0.85,
107-
),
108-
text=landmarks["name"],
130+
marker={
131+
"size": _select(list(sizes), top_idx),
132+
"color": _select(list(visitors), top_idx),
133+
"colorscale": imprint_seq,
134+
"cmin": vmin,
135+
"cmax": vmax,
136+
"colorbar": {
137+
"title": {"text": "Annual Visitors", "font": {"size": 12, "color": INK}},
138+
"tickfont": {"size": 10, "color": INK_SOFT},
139+
"bgcolor": ELEVATED_BG,
140+
"bordercolor": INK_SOFT,
141+
"borderwidth": 1,
142+
"thickness": 18,
143+
"len": 0.65,
144+
"tickformat": ",.0f",
145+
},
146+
"opacity": 0.85,
147+
},
148+
text=_select(names, top_idx),
109149
textposition="top center",
110-
textfont=dict(size=12, color="black"),
111-
hovertemplate="<b>%{text}</b><br>"
112-
+ "Lat: %{lat:.4f}<br>"
113-
+ "Lon: %{lon:.4f}<br>"
114-
+ "Visitors: %{marker.color:,.0f}<extra></extra>",
150+
textfont={"size": 11, "color": INK},
151+
hovertemplate=("<b>%{text}</b><br>Visitors: %{marker.color:,.0f}<extra></extra>"),
152+
)
153+
)
154+
155+
# Secondary trace (bottom center labels) — no colorbar, same scale
156+
fig.add_trace(
157+
go.Scattermap(
158+
lat=_select(lats, bottom_idx),
159+
lon=_select(lons, bottom_idx),
160+
mode="markers+text",
161+
marker={
162+
"size": _select(list(sizes), bottom_idx),
163+
"color": _select(list(visitors), bottom_idx),
164+
"colorscale": imprint_seq,
165+
"cmin": vmin,
166+
"cmax": vmax,
167+
"showscale": False,
168+
"opacity": 0.85,
169+
},
170+
text=_select(names, bottom_idx),
171+
textposition="bottom center",
172+
textfont={"size": 11, "color": INK},
173+
hovertemplate=("<b>%{text}</b><br>Visitors: %{marker.color:,.0f}<extra></extra>"),
115174
)
116175
)
117176

118-
# Update layout with OpenStreetMap tile background (new 'map' API replacing 'mapbox')
119177
fig.update_layout(
120-
title=dict(
121-
text="European Landmarks Visitor Data<br>"
122-
"<span style='font-size:20px'>map-tile-background \u00b7 plotly \u00b7 pyplots.ai</span>",
123-
font=dict(size=28),
124-
x=0.5,
125-
xanchor="center",
126-
),
127-
map=dict(style="open-street-map", center=dict(lat=48.5, lon=10.0), zoom=3.5),
128-
margin=dict(l=20, r=20, t=100, b=20),
178+
autosize=False,
179+
paper_bgcolor=PAGE_BG,
180+
font={"color": INK},
181+
title={
182+
"text": (
183+
"European Landmarks by Visitor Count<br>"
184+
"<span style='font-size:12px'>"
185+
"map-tile-background · python · plotly · anyplot.ai"
186+
"</span>"
187+
),
188+
"font": {"size": 16, "color": INK},
189+
"x": 0.5,
190+
"xanchor": "center",
191+
},
192+
map={"style": map_style, "center": {"lat": 48.5, "lon": 10.0}, "zoom": 3.5},
193+
margin={"l": 20, "r": 20, "t": 80, "b": 20},
129194
showlegend=False,
195+
annotations=[
196+
{
197+
"text": "<b>★ Top attraction</b><br>Colosseum · 7.4M visitors/yr",
198+
"xref": "paper",
199+
"yref": "paper",
200+
"x": 0.68,
201+
"y": 0.22,
202+
"showarrow": False,
203+
"font": {"size": 11, "color": "#009E73"},
204+
"bgcolor": ELEVATED_BG,
205+
"bordercolor": "#009E73",
206+
"borderwidth": 1,
207+
"borderpad": 5,
208+
"align": "left",
209+
}
210+
],
130211
)
131212

132-
# Save as PNG (4800x2700 at scale 3)
133-
fig.write_image("plot.png", width=1600, height=900, scale=3)
134-
135-
# Save interactive HTML version
136-
fig.write_html("plot.html", include_plotlyjs="cdn")
213+
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
214+
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

0 commit comments

Comments
 (0)