|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | 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 |
5 | 5 | """ |
6 | 6 |
|
| 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 | + |
7 | 16 | import numpy as np |
8 | 17 | import plotly.graph_objects as go |
9 | 18 |
|
10 | 19 |
|
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 |
12 | 35 | np.random.seed(42) |
13 | 36 |
|
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 | +) |
85 | 107 |
|
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) |
88 | 109 | 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 | + |
89 | 121 |
|
90 | | -# Create the map figure with tile background |
91 | 122 | fig = go.Figure() |
92 | 123 |
|
93 | | -# Add scattermap trace for data points on tile background (new API replacing scattermapbox) |
| 124 | +# Main trace (top center labels) — carries colorbar |
94 | 125 | fig.add_trace( |
95 | 126 | go.Scattermap( |
96 | | - lat=landmarks["lat"], |
97 | | - lon=landmarks["lon"], |
| 127 | + lat=_select(lats, top_idx), |
| 128 | + lon=_select(lons, top_idx), |
98 | 129 | 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), |
109 | 149 | 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>"), |
115 | 174 | ) |
116 | 175 | ) |
117 | 176 |
|
118 | | -# Update layout with OpenStreetMap tile background (new 'map' API replacing 'mapbox') |
119 | 177 | 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}, |
129 | 194 | 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 | + ], |
130 | 211 | ) |
131 | 212 |
|
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