|
1 | 1 | """ pyplots.ai |
2 | 2 | bump-basic: Basic Bump Chart |
3 | | -Library: plotly 6.5.0 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: plotly 6.5.2 | Python 3.14.3 |
| 4 | +Quality: 91/100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import plotly.graph_objects as go |
8 | 8 |
|
9 | 9 |
|
10 | | -# Data - Sports league standings over a season |
11 | | -entities = ["Team Alpha", "Team Beta", "Team Gamma", "Team Delta", "Team Epsilon"] |
12 | | -periods = ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"] |
| 10 | +# Data - Formula 1 driver standings over a season |
| 11 | +drivers = ["Verstappen", "Hamilton", "Norris", "Leclerc", "Piastri", "Sainz"] |
| 12 | +races = ["Bahrain", "Jeddah", "Melbourne", "Suzuka", "Miami", "Imola", "Monaco", "Silverstone"] |
13 | 13 |
|
14 | | -# Rankings for each team across periods (1 = best) |
15 | 14 | rankings = { |
16 | | - "Team Alpha": [3, 2, 1, 1, 2, 1], |
17 | | - "Team Beta": [1, 1, 2, 3, 3, 2], |
18 | | - "Team Gamma": [2, 3, 3, 2, 1, 3], |
19 | | - "Team Delta": [4, 4, 5, 4, 4, 4], |
20 | | - "Team Epsilon": [5, 5, 4, 5, 5, 5], |
| 15 | + "Verstappen": [1, 1, 1, 1, 1, 2, 3, 2], |
| 16 | + "Hamilton": [4, 3, 4, 3, 3, 3, 1, 1], |
| 17 | + "Norris": [5, 5, 3, 4, 2, 1, 2, 3], |
| 18 | + "Leclerc": [2, 2, 2, 2, 4, 4, 4, 4], |
| 19 | + "Piastri": [3, 4, 5, 5, 5, 5, 5, 5], |
| 20 | + "Sainz": [6, 6, 6, 6, 6, 6, 6, 6], |
21 | 21 | } |
22 | 22 |
|
23 | | -# Colors - Python Blue first, then colorblind-safe palette |
24 | | -colors = ["#306998", "#FFD43B", "#2ecc71", "#e74c3c", "#9b59b6"] |
| 23 | +# Colorblind-safe palette — Python Blue first, teal replaces green to avoid red-green issue |
| 24 | +colors = { |
| 25 | + "Verstappen": "#306998", |
| 26 | + "Hamilton": "#e74c3c", |
| 27 | + "Norris": "#17becf", |
| 28 | + "Leclerc": "#f39c12", |
| 29 | + "Piastri": "#9b59b6", |
| 30 | + "Sainz": "#95a5a6", |
| 31 | +} |
| 32 | + |
| 33 | +# Visual hierarchy — emphasize dynamic storylines, mute static ones |
| 34 | +rank_changes = {d: max(r) - min(r) for d, r in rankings.items()} |
| 35 | +line_widths = {d: 5 if rank_changes[d] >= 3 else 3 if rank_changes[d] >= 2 else 2 for d in drivers} |
| 36 | +marker_sizes = {d: 16 if rank_changes[d] >= 3 else 12 if rank_changes[d] >= 2 else 10 for d in drivers} |
| 37 | +opacities = {d: 1.0 if rank_changes[d] >= 3 else 0.8 if rank_changes[d] >= 2 else 0.45 for d in drivers} |
25 | 38 |
|
26 | 39 | # Create figure |
27 | 40 | fig = go.Figure() |
28 | 41 |
|
29 | | -for i, (entity, ranks) in enumerate(rankings.items()): |
| 42 | +for driver in drivers: |
| 43 | + ranks = rankings[driver] |
| 44 | + color = colors[driver] |
30 | 45 | fig.add_trace( |
31 | 46 | go.Scatter( |
32 | | - x=periods, |
| 47 | + x=races, |
33 | 48 | y=ranks, |
34 | 49 | mode="lines+markers", |
35 | | - name=entity, |
36 | | - line={"width": 4, "color": colors[i]}, |
37 | | - marker={"size": 16, "color": colors[i]}, |
| 50 | + name=driver, |
| 51 | + line={"width": line_widths[driver], "color": color}, |
| 52 | + marker={"size": marker_sizes[driver], "color": color, "line": {"width": 2, "color": "white"}}, |
| 53 | + opacity=opacities[driver], |
| 54 | + showlegend=False, |
| 55 | + hovertemplate="<b>%{text}</b><br>%{x}: P%{y}<extra></extra>", |
| 56 | + text=[driver] * len(races), |
38 | 57 | ) |
39 | 58 | ) |
| 59 | + # End-of-line label |
| 60 | + fig.add_annotation( |
| 61 | + x=races[-1], |
| 62 | + y=ranks[-1], |
| 63 | + text=f" <b>{driver}</b>" if rank_changes[driver] >= 3 else f" {driver}", |
| 64 | + showarrow=False, |
| 65 | + xanchor="left", |
| 66 | + font={"size": 16, "color": color}, |
| 67 | + opacity=opacities[driver], |
| 68 | + ) |
40 | 69 |
|
41 | 70 | # Layout with inverted Y-axis (rank 1 at top) |
42 | 71 | fig.update_layout( |
43 | | - title={"text": "bump-basic · plotly · pyplots.ai", "font": {"size": 28}}, |
44 | | - xaxis={"title": {"text": "Period", "font": {"size": 22}}, "tickfont": {"size": 18}}, |
| 72 | + title={"text": "bump-basic · plotly · pyplots.ai", "font": {"size": 28}, "x": 0.02, "xanchor": "left"}, |
| 73 | + xaxis={"title": {"text": "Race", "font": {"size": 22}}, "tickfont": {"size": 18}, "showgrid": False}, |
45 | 74 | yaxis={ |
46 | | - "title": {"text": "Rank", "font": {"size": 22}}, |
| 75 | + "title": {"text": "Championship Position", "font": {"size": 22}}, |
47 | 76 | "tickfont": {"size": 18}, |
48 | | - "autorange": "reversed", # Invert so rank 1 is at top |
| 77 | + "autorange": "reversed", |
49 | 78 | "tickmode": "linear", |
50 | 79 | "tick0": 1, |
51 | 80 | "dtick": 1, |
| 81 | + "gridcolor": "rgba(0,0,0,0.06)", |
| 82 | + "gridwidth": 1, |
| 83 | + "showgrid": True, |
| 84 | + "zeroline": False, |
52 | 85 | }, |
53 | | - legend={"font": {"size": 18}, "x": 1.02, "y": 1, "xanchor": "left"}, |
54 | 86 | template="plotly_white", |
55 | | - margin={"r": 150}, # Extra margin for legend |
| 87 | + margin={"r": 130, "t": 80, "l": 80, "b": 70}, |
| 88 | + plot_bgcolor="rgba(0,0,0,0)", |
| 89 | + hoverlabel={"font_size": 16}, |
56 | 90 | ) |
57 | 91 |
|
58 | 92 | # Save as PNG (4800x2700 px) |
|
0 commit comments