|
1 | 1 | """ pyplots.ai |
2 | 2 | bump-basic: Basic Bump Chart |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.8.2 | Python 3.14.3 |
| 4 | +Quality: 90/100 | Updated: 2026-02-22 |
5 | 5 | """ |
6 | 6 |
|
7 | | -from bokeh.io import export_png, output_file, save |
8 | | -from bokeh.models import ColumnDataSource, Legend |
9 | | -from bokeh.palettes import Category10 |
| 7 | +from bokeh.io import export_png |
| 8 | +from bokeh.models import ColumnDataSource, CustomJSTickFormatter, FixedTicker, Label |
10 | 9 | from bokeh.plotting import figure |
11 | 10 |
|
12 | 11 |
|
13 | | -# Data - Sports league standings over a season |
14 | | -entities = ["Team Alpha", "Team Beta", "Team Gamma", "Team Delta", "Team Epsilon"] |
15 | | -periods = ["Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"] |
| 12 | +# Data - Formula 1 constructor standings over a 6-race stretch |
| 13 | +entities = ["Red Bull Racing", "McLaren", "Ferrari", "Mercedes", "Aston Martin"] |
| 14 | +periods = ["Race 1", "Race 2", "Race 3", "Race 4", "Race 5", "Race 6"] |
16 | 15 |
|
17 | | -# Rankings for each team across weeks (1 = best) |
| 16 | +# Rankings for each team across races (1 = best) |
18 | 17 | rankings = { |
19 | | - "Team Alpha": [3, 2, 1, 1, 2, 1], |
20 | | - "Team Beta": [1, 1, 2, 3, 3, 4], |
21 | | - "Team Gamma": [5, 4, 4, 2, 1, 2], |
22 | | - "Team Delta": [2, 3, 3, 4, 4, 3], |
23 | | - "Team Epsilon": [4, 5, 5, 5, 5, 5], |
| 18 | + "Red Bull Racing": [3, 2, 1, 1, 2, 1], |
| 19 | + "McLaren": [1, 1, 2, 3, 3, 4], |
| 20 | + "Ferrari": [5, 4, 4, 2, 1, 2], |
| 21 | + "Mercedes": [2, 3, 3, 4, 4, 3], |
| 22 | + "Aston Martin": [4, 5, 5, 5, 5, 5], |
24 | 23 | } |
25 | 24 |
|
26 | | -# Colors for each team |
27 | | -colors = Category10[5] |
| 25 | +# Cohesive palette starting with Python Blue — colorblind-safe |
| 26 | +colors = ["#306998", "#E6894A", "#D44D5C", "#5BA67D", "#8B6DB0"] |
| 27 | + |
| 28 | +# Emphasis: highlight the two teams with dramatic rank changes |
| 29 | +# Ferrari rises from 5th to 1st; McLaren falls from 1st to 4th |
| 30 | +highlight = {"Ferrari", "Red Bull Racing", "McLaren"} |
28 | 31 |
|
29 | 32 | # Create figure with inverted y-axis (rank 1 at top) |
30 | 33 | p = figure( |
31 | 34 | width=4800, |
32 | 35 | height=2700, |
33 | 36 | title="bump-basic · bokeh · pyplots.ai", |
34 | 37 | x_range=periods, |
35 | | - y_range=(5.5, 0.5), # Inverted: rank 1 at top |
36 | | - x_axis_label="Week", |
37 | | - y_axis_label="Rank Position", |
| 38 | + y_range=(5.8, 0.4), |
| 39 | + x_axis_label="Constructor Standings by Race", |
| 40 | + y_axis_label="Championship Position", |
| 41 | + toolbar_location=None, |
38 | 42 | ) |
39 | 43 |
|
40 | | -# Plot lines and markers for each entity |
41 | | -legend_items = [] |
| 44 | +# Plot lines and markers for each entity with visual hierarchy |
42 | 45 | for i, (entity, ranks) in enumerate(rankings.items()): |
43 | | - source = ColumnDataSource(data={"x": periods, "y": ranks}) |
44 | | - |
45 | | - # Draw connecting lines |
46 | | - line = p.line(x="x", y="y", source=source, line_width=4, line_color=colors[i], line_alpha=0.8) |
47 | | - |
48 | | - # Draw dot markers at each period |
49 | | - scatter = p.scatter(x="x", y="y", source=source, size=20, color=colors[i], alpha=0.9) |
50 | | - |
51 | | - legend_items.append((entity, [line, scatter])) |
52 | | - |
53 | | -# Add legend outside the plot |
54 | | -legend = Legend(items=legend_items, location="center") |
55 | | -legend.label_text_font_size = "18pt" |
56 | | -legend.spacing = 10 |
57 | | -p.add_layout(legend, "right") |
58 | | - |
59 | | -# Style title |
60 | | -p.title.text_font_size = "28pt" |
61 | | - |
62 | | -# Style axes |
63 | | -p.xaxis.axis_label_text_font_size = "22pt" |
64 | | -p.yaxis.axis_label_text_font_size = "22pt" |
65 | | -p.xaxis.major_label_text_font_size = "18pt" |
66 | | -p.yaxis.major_label_text_font_size = "18pt" |
67 | | - |
68 | | -# Grid styling |
69 | | -p.xgrid.grid_line_alpha = 0.3 |
70 | | -p.ygrid.grid_line_alpha = 0.3 |
71 | | -p.xgrid.grid_line_dash = "dashed" |
72 | | -p.ygrid.grid_line_dash = "dashed" |
| 46 | + source = ColumnDataSource(data={"x": periods, "y": ranks, "team": [entity] * len(periods)}) |
| 47 | + |
| 48 | + is_highlight = entity in highlight |
| 49 | + lw = 10 if is_highlight else 5 |
| 50 | + alpha_line = 0.95 if is_highlight else 0.55 |
| 51 | + alpha_marker = 1.0 if is_highlight else 0.6 |
| 52 | + marker_size = 38 if is_highlight else 22 |
| 53 | + |
| 54 | + line = p.line(x="x", y="y", source=source, line_width=lw, line_color=colors[i], line_alpha=alpha_line) |
| 55 | + scatter = p.scatter(x="x", y="y", source=source, size=marker_size, color=colors[i], alpha=alpha_marker) |
| 56 | + |
| 57 | + # End-of-line labels using Bokeh's Label annotation |
| 58 | + label = Label( |
| 59 | + x=5, |
| 60 | + y=ranks[-1], |
| 61 | + text=entity, |
| 62 | + text_font_size="20pt", |
| 63 | + text_color=colors[i], |
| 64 | + text_alpha=alpha_line, |
| 65 | + text_font_style="bold" if is_highlight else "normal", |
| 66 | + x_offset=18, |
| 67 | + y_offset=-8, |
| 68 | + ) |
| 69 | + p.add_layout(label) |
| 70 | + |
| 71 | +# Title styling |
| 72 | +p.title.text_font_size = "32pt" |
| 73 | +p.title.text_font_style = "bold" |
| 74 | +p.title.text_color = "#2c3e50" |
| 75 | + |
| 76 | +# Axis styling |
| 77 | +p.xaxis.axis_label_text_font_size = "24pt" |
| 78 | +p.yaxis.axis_label_text_font_size = "24pt" |
| 79 | +p.xaxis.major_label_text_font_size = "20pt" |
| 80 | +p.yaxis.major_label_text_font_size = "20pt" |
| 81 | +p.xaxis.axis_label_text_color = "#555555" |
| 82 | +p.yaxis.axis_label_text_color = "#555555" |
| 83 | +p.xaxis.major_label_text_color = "#444444" |
| 84 | +p.yaxis.major_label_text_color = "#444444" |
| 85 | + |
| 86 | +# Remove spines for clean look |
| 87 | +p.xaxis.axis_line_color = None |
| 88 | +p.yaxis.axis_line_color = None |
| 89 | +p.xaxis.major_tick_line_color = None |
| 90 | +p.yaxis.major_tick_line_color = None |
| 91 | +p.xaxis.minor_tick_line_color = None |
| 92 | +p.yaxis.minor_tick_line_color = None |
| 93 | + |
| 94 | +# Grid styling - subtle dashed lines |
| 95 | +p.xgrid.grid_line_alpha = 0.15 |
| 96 | +p.ygrid.grid_line_alpha = 0.25 |
| 97 | +p.ygrid.grid_line_dash = [4, 4] |
| 98 | + |
| 99 | +# Y-axis: FixedTicker at rank positions with CustomJSTickFormatter for ordinals |
| 100 | +p.yaxis.ticker = FixedTicker(ticks=[1, 2, 3, 4, 5]) |
| 101 | +p.yaxis.formatter = CustomJSTickFormatter( |
| 102 | + code=""" |
| 103 | + const suffixes = {1: 'st', 2: 'nd', 3: 'rd', 4: 'th', 5: 'th'}; |
| 104 | + return tick + (suffixes[tick] || 'th'); |
| 105 | +""" |
| 106 | +) |
73 | 107 |
|
74 | | -# Background styling |
75 | | -p.background_fill_color = "#fafafa" |
| 108 | +# Background |
| 109 | +p.background_fill_color = "#f8f9fa" |
76 | 110 | p.border_fill_color = "white" |
| 111 | +p.outline_line_color = None |
| 112 | + |
| 113 | +# Generous padding for balanced layout |
| 114 | +p.min_border_left = 100 |
| 115 | +p.min_border_right = 300 |
| 116 | +p.min_border_top = 80 |
| 117 | +p.min_border_bottom = 80 |
77 | 118 |
|
78 | 119 | # Save as PNG |
79 | 120 | export_png(p, filename="plot.png") |
80 | | - |
81 | | -# Save as HTML (interactive version) |
82 | | -output_file("plot.html") |
83 | | -save(p) |
|
0 commit comments