|
1 | 1 | """ pyplots.ai |
2 | 2 | bump-basic: Basic Bump Chart |
3 | | -Library: matplotlib 3.10.8 | Python 3.13.11 |
4 | | -Quality: 92/100 | Created: 2025-12-23 |
| 3 | +Library: matplotlib 3.10.8 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import matplotlib.patheffects as pe |
7 | 8 | import matplotlib.pyplot as plt |
8 | 9 | import numpy as np |
9 | 10 |
|
10 | 11 |
|
11 | | -# Data - Sports league standings over a season |
12 | | -entities = ["Team Alpha", "Team Beta", "Team Gamma", "Team Delta", "Team Epsilon"] |
13 | | -periods = ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"] |
| 12 | +# Data - Formula 1 driver standings over an 8-race season |
| 13 | +drivers = ["Verstappen", "Hamilton", "Norris", "Leclerc", "Sainz", "Piastri", "Russell"] |
| 14 | +races = ["Bahrain", "Jeddah", "Melbourne", "Suzuka", "Shanghai", "Miami", "Imola", "Monaco"] |
14 | 15 |
|
15 | | -# Rankings for each team across periods (1 = best) |
| 16 | +# Rankings per driver across races (1 = championship leader) |
16 | 17 | rankings = { |
17 | | - "Team Alpha": [3, 2, 1, 1, 2, 1], |
18 | | - "Team Beta": [1, 1, 2, 3, 3, 2], |
19 | | - "Team Gamma": [2, 3, 3, 2, 1, 3], |
20 | | - "Team Delta": [4, 4, 5, 4, 4, 4], |
21 | | - "Team Epsilon": [5, 5, 4, 5, 5, 5], |
| 18 | + "Verstappen": [1, 1, 1, 2, 3, 3, 2, 1], |
| 19 | + "Hamilton": [4, 3, 2, 1, 1, 2, 1, 2], |
| 20 | + "Norris": [5, 5, 4, 3, 2, 1, 3, 3], |
| 21 | + "Leclerc": [2, 2, 3, 4, 5, 5, 4, 4], |
| 22 | + "Sainz": [3, 4, 5, 5, 4, 4, 5, 5], |
| 23 | + "Piastri": [6, 6, 7, 7, 6, 6, 6, 7], |
| 24 | + "Russell": [7, 7, 6, 6, 7, 7, 7, 6], |
22 | 25 | } |
23 | 26 |
|
24 | | -# Colors - Python Blue first, then colorblind-safe palette |
25 | | -colors = ["#306998", "#FFD43B", "#2ecc71", "#e74c3c", "#9b59b6"] |
| 27 | +# Colorblind-safe palette — distinct hues, no similar oranges |
| 28 | +colors = { |
| 29 | + "Verstappen": "#306998", |
| 30 | + "Hamilton": "#9467bd", |
| 31 | + "Norris": "#17becf", |
| 32 | + "Leclerc": "#d62728", |
| 33 | + "Sainz": "#e8963e", |
| 34 | + "Piastri": "#8c564b", |
| 35 | + "Russell": "#7f7f7f", |
| 36 | +} |
| 37 | + |
| 38 | +# Top-3 finishers get visual emphasis for storytelling hierarchy |
| 39 | +top_drivers = {"Verstappen", "Hamilton", "Norris"} |
26 | 40 |
|
27 | | -# Create plot (4800x2700 px) |
| 41 | +# Plot |
28 | 42 | fig, ax = plt.subplots(figsize=(16, 9)) |
| 43 | +x = np.arange(len(races)) |
| 44 | + |
| 45 | +for driver, ranks in rankings.items(): |
| 46 | + is_top = driver in top_drivers |
| 47 | + lw = 4.0 if is_top else 2.5 |
| 48 | + ms = 16 if is_top else 10 |
| 49 | + zo = 4 if is_top else 3 |
| 50 | + alpha = 1.0 if is_top else 0.55 |
29 | 51 |
|
30 | | -x = np.arange(len(periods)) |
| 52 | + ax.plot( |
| 53 | + x, |
| 54 | + ranks, |
| 55 | + marker="o", |
| 56 | + markersize=ms, |
| 57 | + linewidth=lw, |
| 58 | + color=colors[driver], |
| 59 | + zorder=zo, |
| 60 | + alpha=alpha, |
| 61 | + path_effects=[pe.Stroke(linewidth=lw + 2, foreground="white"), pe.Normal()], |
| 62 | + ) |
| 63 | + # End-of-line labels (replaces legend, more direct) |
| 64 | + ax.text( |
| 65 | + x[-1] + 0.15, |
| 66 | + ranks[-1], |
| 67 | + driver, |
| 68 | + fontsize=16, |
| 69 | + fontweight="bold", |
| 70 | + color=colors[driver], |
| 71 | + va="center", |
| 72 | + alpha=1.0 if is_top else 0.8, |
| 73 | + path_effects=[pe.withStroke(linewidth=3, foreground="white")], |
| 74 | + ) |
31 | 75 |
|
32 | | -for i, (entity, ranks) in enumerate(rankings.items()): |
33 | | - ax.plot(x, ranks, marker="o", markersize=15, linewidth=3, color=colors[i], label=entity) |
| 76 | +# Annotate key lead changes for data storytelling |
| 77 | +ax.annotate( |
| 78 | + "Hamilton\ntakes the lead", |
| 79 | + xy=(3, 1), |
| 80 | + xytext=(1.5, -0.6), |
| 81 | + fontsize=12, |
| 82 | + fontweight="bold", |
| 83 | + color=colors["Hamilton"], |
| 84 | + ha="center", |
| 85 | + va="bottom", |
| 86 | + path_effects=[pe.withStroke(linewidth=2, foreground="white")], |
| 87 | + arrowprops={"arrowstyle": "->", "color": colors["Hamilton"], "lw": 1.5, "connectionstyle": "arc3,rad=-0.15"}, |
| 88 | +) |
| 89 | + |
| 90 | +ax.annotate( |
| 91 | + "Norris\npeaks at P1", |
| 92 | + xy=(5, 1), |
| 93 | + xytext=(5.8, -0.6), |
| 94 | + fontsize=12, |
| 95 | + fontweight="bold", |
| 96 | + color=colors["Norris"], |
| 97 | + ha="center", |
| 98 | + va="bottom", |
| 99 | + path_effects=[pe.withStroke(linewidth=2, foreground="white")], |
| 100 | + arrowprops={"arrowstyle": "->", "color": colors["Norris"], "lw": 1.5, "connectionstyle": "arc3,rad=0.15"}, |
| 101 | +) |
34 | 102 |
|
35 | 103 | # Invert Y-axis so rank 1 is at top |
| 104 | +ax.set_ylim(-1.2, len(drivers) + 0.5) |
36 | 105 | ax.invert_yaxis() |
37 | 106 |
|
38 | | -# Labels and styling |
39 | | -ax.set_xlabel("Period", fontsize=20) |
40 | | -ax.set_ylabel("Rank", fontsize=20) |
41 | | -ax.set_title("bump-basic · matplotlib · pyplots.ai", fontsize=24) |
| 107 | +# Style |
| 108 | +ax.set_xlabel("Grand Prix", fontsize=20) |
| 109 | +ax.set_ylabel("Championship Position", fontsize=20) |
| 110 | +ax.set_title("bump-basic \u00b7 matplotlib \u00b7 pyplots.ai", fontsize=24, fontweight="medium") |
42 | 111 |
|
43 | 112 | ax.set_xticks(x) |
44 | | -ax.set_xticklabels(periods) |
45 | | -ax.set_yticks(range(1, len(entities) + 1)) |
| 113 | +ax.set_xticklabels(races, rotation=25, ha="right") |
| 114 | +ax.set_yticks(range(1, len(drivers) + 1)) |
46 | 115 | ax.tick_params(axis="both", labelsize=16) |
47 | 116 |
|
48 | | -ax.grid(True, alpha=0.3, linestyle="--") |
49 | | -ax.legend(fontsize=16, loc="upper left", bbox_to_anchor=(1.02, 1)) |
| 117 | +ax.yaxis.grid(True, alpha=0.2, linewidth=0.8) |
| 118 | +ax.spines["top"].set_visible(False) |
| 119 | +ax.spines["right"].set_visible(False) |
| 120 | + |
| 121 | +ax.set_xlim(-0.3, len(races) - 1 + 1.5) |
50 | 122 |
|
51 | 123 | plt.tight_layout() |
52 | 124 | plt.savefig("plot.png", dpi=300, bbox_inches="tight") |
0 commit comments