|
1 | | -""" pyplots.ai |
| 1 | +""" anyplot.ai |
2 | 2 | ecdf-basic: Basic ECDF Plot |
3 | | -Library: bokeh 3.8.1 | Python 3.13.11 |
4 | | -Quality: 91/100 | Created: 2025-12-23 |
| 3 | +Library: bokeh 3.9.0 | Python 3.14.4 |
| 4 | +Quality: 87/100 | Updated: 2026-04-24 |
5 | 5 | """ |
6 | 6 |
|
| 7 | +import os |
| 8 | + |
7 | 9 | import numpy as np |
8 | | -from bokeh.io import export_png, save |
9 | | -from bokeh.models import ColumnDataSource |
| 10 | +from bokeh.io import export_png, output_file, save |
| 11 | +from bokeh.models import ColumnDataSource, HoverTool, Label, Span |
10 | 12 | from bokeh.plotting import figure |
11 | | -from bokeh.resources import CDN |
12 | 13 |
|
13 | 14 |
|
14 | | -# Data |
15 | | -np.random.seed(42) |
16 | | -values = np.random.randn(200) * 15 + 50 # Normal distribution: mean=50, std=15 |
| 15 | +# Theme tokens |
| 16 | +THEME = os.getenv("ANYPLOT_THEME", "light") |
| 17 | +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" |
| 18 | +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" |
| 19 | +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" |
| 20 | +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
| 21 | +BRAND = "#009E73" # Okabe-Ito position 1 — always first series |
17 | 22 |
|
18 | | -# Compute ECDF |
19 | | -sorted_values = np.sort(values) |
20 | | -ecdf_y = np.arange(1, len(sorted_values) + 1) / len(sorted_values) |
| 23 | +# Data: marathon finish times (minutes) for 300 recreational runners |
| 24 | +np.random.seed(42) |
| 25 | +n_runners = 300 |
| 26 | +finish_times = np.random.normal(loc=240, scale=32, size=n_runners) |
21 | 27 |
|
22 | | -# Create step function data |
23 | | -# For proper ECDF step visualization, duplicate points for horizontal/vertical steps |
24 | | -x_step = np.repeat(sorted_values, 2)[1:] |
25 | | -y_step = np.repeat(ecdf_y, 2)[:-1] |
| 28 | +# ECDF |
| 29 | +sorted_times = np.sort(finish_times) |
| 30 | +cumulative = np.arange(1, n_runners + 1) / n_runners |
26 | 31 |
|
27 | | -# Add starting point at first value with y=0 |
28 | | -x_step = np.concatenate([[sorted_values[0]], x_step]) |
29 | | -y_step = np.concatenate([[0], y_step]) |
| 32 | +# Key percentiles for storytelling |
| 33 | +q25, q50, q75 = np.percentile(sorted_times, [25, 50, 75]) |
30 | 34 |
|
31 | | -source = ColumnDataSource(data={"x": x_step, "y": y_step}) |
| 35 | +source = ColumnDataSource(data={"x": sorted_times, "y": cumulative}) |
32 | 36 |
|
33 | 37 | # Plot |
34 | 38 | p = figure( |
35 | 39 | width=4800, |
36 | 40 | height=2700, |
37 | | - title="ecdf-basic · bokeh · pyplots.ai", |
38 | | - x_axis_label="Value", |
39 | | - y_axis_label="Cumulative Proportion", |
40 | | - y_range=(0, 1.05), |
| 41 | + title="Marathon Finish Times · ecdf-basic · bokeh · anyplot.ai", |
| 42 | + x_axis_label="Finish Time (minutes)", |
| 43 | + y_axis_label="Cumulative Proportion of Runners", |
| 44 | + y_range=(0, 1.02), |
| 45 | + background_fill_color=PAGE_BG, |
| 46 | + border_fill_color=PAGE_BG, |
| 47 | + toolbar_location=None, |
41 | 48 | ) |
42 | 49 |
|
43 | | -# Draw step line |
44 | | -p.line(x="x", y="y", source=source, line_width=4, line_color="#306998", alpha=0.9) |
| 50 | +# ECDF step line (step-after matches the 1/n jump at each data point) |
| 51 | +step_renderer = p.step(x="x", y="y", source=source, line_width=4, line_color=BRAND, mode="after") |
| 52 | + |
| 53 | +# Percentile reference lines (25th, 50th, 75th) |
| 54 | +for q_val, q_lbl in [(q25, "25th"), (q50, "50th (median)"), (q75, "75th")]: |
| 55 | + p.add_layout( |
| 56 | + Span(location=q_val, dimension="height", line_color=INK_SOFT, line_dash="dashed", line_width=2, line_alpha=0.55) |
| 57 | + ) |
| 58 | + p.add_layout( |
| 59 | + Label( |
| 60 | + x=q_val, |
| 61 | + y=0.03, |
| 62 | + text=f"{q_lbl}: {q_val:.0f} min", |
| 63 | + text_font_size="20pt", |
| 64 | + text_color=INK_SOFT, |
| 65 | + text_font_style="italic", |
| 66 | + x_offset=14, |
| 67 | + ) |
| 68 | + ) |
45 | 69 |
|
46 | | -# Style |
| 70 | +# Hover tool — bokeh's interactive strength |
| 71 | +p.add_tools( |
| 72 | + HoverTool( |
| 73 | + renderers=[step_renderer], tooltips=[("Finish Time", "@x{0.0} min"), ("Cumulative", "@y{0.0%}")], mode="vline" |
| 74 | + ) |
| 75 | +) |
| 76 | + |
| 77 | +# Typography |
47 | 78 | p.title.text_font_size = "28pt" |
| 79 | +p.title.text_color = INK |
| 80 | +p.title.text_font_style = "bold" |
48 | 81 | p.xaxis.axis_label_text_font_size = "22pt" |
49 | 82 | p.yaxis.axis_label_text_font_size = "22pt" |
50 | 83 | p.xaxis.major_label_text_font_size = "18pt" |
51 | 84 | p.yaxis.major_label_text_font_size = "18pt" |
52 | 85 |
|
53 | | -# Grid styling |
54 | | -p.grid.grid_line_alpha = 0.3 |
55 | | -p.grid.grid_line_dash = "dashed" |
| 86 | +# Chrome colors |
| 87 | +p.xaxis.axis_label_text_color = INK |
| 88 | +p.yaxis.axis_label_text_color = INK |
| 89 | +p.xaxis.major_label_text_color = INK_SOFT |
| 90 | +p.yaxis.major_label_text_color = INK_SOFT |
| 91 | +p.xaxis.axis_line_color = INK_SOFT |
| 92 | +p.yaxis.axis_line_color = INK_SOFT |
| 93 | +p.xaxis.major_tick_line_color = INK_SOFT |
| 94 | +p.yaxis.major_tick_line_color = INK_SOFT |
| 95 | +p.xaxis.minor_tick_line_color = None |
| 96 | +p.yaxis.minor_tick_line_color = None |
| 97 | + |
| 98 | +# Grid: y-only, subtle solid lines; remove box outline |
| 99 | +p.outline_line_color = None |
| 100 | +p.xgrid.grid_line_color = None |
| 101 | +p.ygrid.grid_line_color = INK |
| 102 | +p.ygrid.grid_line_alpha = 0.10 |
56 | 103 |
|
57 | 104 | # Save |
58 | | -export_png(p, filename="plot.png") |
59 | | -save(p, filename="plot.html", resources=CDN, title="ecdf-basic · bokeh · pyplots.ai") |
| 105 | +export_png(p, filename=f"plot-{THEME}.png") |
| 106 | +output_file(f"plot-{THEME}.html", title="Marathon Finish Times · ecdf-basic · bokeh · anyplot.ai") |
| 107 | +save(p) |
0 commit comments