|
1 | 1 | """ anyplot.ai |
2 | 2 | errorbar-basic: Basic Error Bar Plot |
3 | | -Library: bokeh 3.9.0 | Python 3.14.4 |
4 | | -Quality: 88/100 | Updated: 2026-04-25 |
| 3 | +Library: bokeh 3.9.1 | Python 3.13.14 |
| 4 | +Quality: 92/100 | Updated: 2026-06-30 |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import os |
| 8 | +import sys |
| 9 | +import time |
| 10 | +from pathlib import Path |
| 11 | + |
| 12 | + |
| 13 | +# Remove script dir from sys.path so 'bokeh.py' doesn't shadow the installed bokeh package |
| 14 | +_here = os.path.dirname(os.path.abspath(__file__)) |
| 15 | +sys.path = [p for p in sys.path if p != _here] |
8 | 16 |
|
9 | 17 | import numpy as np |
10 | | -from bokeh.io import export_png, output_file, save |
| 18 | +from bokeh.io import output_file, save |
11 | 19 | from bokeh.models import ColumnDataSource, Label, TeeHead, Whisker |
12 | 20 | from bokeh.plotting import figure |
| 21 | +from selenium import webdriver |
| 22 | +from selenium.webdriver.chrome.options import Options |
13 | 23 |
|
14 | 24 |
|
15 | 25 | # Theme tokens |
|
20 | 30 | INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" |
21 | 31 | INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" |
22 | 32 |
|
23 | | -# Okabe-Ito categorical palette (positions 1-6) |
| 33 | +# Imprint categorical palette (positions 1-6) |
24 | 34 | IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"] |
25 | 35 |
|
26 | 36 | # Data — experimental measurements with associated uncertainties |
|
40 | 50 | data={"categories": categories, "means": means, "upper": upper, "lower": lower, "colors": colors} |
41 | 51 | ) |
42 | 52 |
|
43 | | -# Plot |
| 53 | +# Canvas: 3200×1800 (landscape) — hard rule, no deviation |
| 54 | +W, H = 3200, 1800 |
| 55 | + |
44 | 56 | p = figure( |
45 | | - width=4800, |
46 | | - height=2700, |
| 57 | + width=W, |
| 58 | + height=H, |
47 | 59 | x_range=categories, |
48 | | - title="errorbar-basic · bokeh · anyplot.ai", |
| 60 | + title="errorbar-basic · python · bokeh · anyplot.ai", |
49 | 61 | x_axis_label="Experimental Group", |
50 | 62 | y_axis_label="Response Value (units)", |
51 | 63 | toolbar_location=None, |
52 | 64 | tools="", |
| 65 | + min_border_bottom=160, |
| 66 | + min_border_left=180, |
| 67 | + min_border_top=110, |
| 68 | + min_border_right=50, |
53 | 69 | ) |
54 | 70 |
|
55 | 71 | # Error bars (Whisker with TeeHead caps) — one whisker per group so each can take its own color |
|
93 | 109 |
|
94 | 110 | # Title |
95 | 111 | p.title.text_color = INK |
96 | | -p.title.text_font_size = "36pt" |
| 112 | +p.title.text_font_size = "50pt" |
97 | 113 | p.title.text_font_style = "normal" |
98 | 114 | p.title.align = "left" |
99 | 115 |
|
100 | 116 | # Axis labels |
101 | 117 | p.xaxis.axis_label_text_color = INK |
102 | 118 | p.yaxis.axis_label_text_color = INK |
103 | | -p.xaxis.axis_label_text_font_size = "32pt" |
104 | | -p.yaxis.axis_label_text_font_size = "32pt" |
| 119 | +p.xaxis.axis_label_text_font_size = "42pt" |
| 120 | +p.yaxis.axis_label_text_font_size = "42pt" |
105 | 121 | p.xaxis.axis_label_text_font_style = "normal" |
106 | 122 | p.yaxis.axis_label_text_font_style = "normal" |
107 | 123 |
|
108 | 124 | # Tick labels |
109 | 125 | p.xaxis.major_label_text_color = INK_SOFT |
110 | 126 | p.yaxis.major_label_text_color = INK_SOFT |
111 | | -p.xaxis.major_label_text_font_size = "24pt" |
112 | | -p.yaxis.major_label_text_font_size = "24pt" |
| 127 | +p.xaxis.major_label_text_font_size = "34pt" |
| 128 | +p.yaxis.major_label_text_font_size = "34pt" |
113 | 129 |
|
114 | 130 | # Axis lines and ticks |
115 | 131 | p.xaxis.axis_line_color = INK_SOFT |
|
121 | 137 |
|
122 | 138 | # Subtle y-grid only |
123 | 139 | p.ygrid.grid_line_color = INK |
124 | | -p.ygrid.grid_line_alpha = 0.10 |
| 140 | +p.ygrid.grid_line_alpha = 0.15 |
125 | 141 | p.xgrid.grid_line_color = None |
126 | 142 |
|
127 | 143 | # Y-range trimmed to data — eliminates dead space below |
|
131 | 147 | p.y_range.start = max(0.0, y_min - y_pad) |
132 | 148 | p.y_range.end = y_max + y_pad |
133 | 149 |
|
134 | | -# Save |
135 | | -export_png(p, filename=f"plot-{THEME}.png") |
136 | | -output_file(f"plot-{THEME}.html", title="errorbar-basic · bokeh · anyplot.ai") |
| 150 | +# Save HTML (required interactive artifact) |
| 151 | +output_file(f"plot-{THEME}.html", title="errorbar-basic · python · bokeh · anyplot.ai") |
137 | 152 | save(p) |
| 153 | + |
| 154 | +# Screenshot with headless Chrome via Selenium — export_png uses chromedriver snap shim which fails |
| 155 | +opts = Options() |
| 156 | +for arg in ( |
| 157 | + "--headless=new", |
| 158 | + "--no-sandbox", |
| 159 | + "--disable-dev-shm-usage", |
| 160 | + "--disable-gpu", |
| 161 | + f"--window-size={W},{H}", |
| 162 | + "--hide-scrollbars", |
| 163 | +): |
| 164 | + opts.add_argument(arg) |
| 165 | +driver = webdriver.Chrome(options=opts) |
| 166 | +# Use CDP to set exact viewport dimensions (--window-size alone can be 100-150px short due to browser chrome) |
| 167 | +driver.execute_cdp_cmd( |
| 168 | + "Emulation.setDeviceMetricsOverride", {"width": W, "height": H, "deviceScaleFactor": 1, "mobile": False} |
| 169 | +) |
| 170 | +driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}") |
| 171 | +time.sleep(3) |
| 172 | +driver.save_screenshot(f"plot-{THEME}.png") |
| 173 | +driver.quit() |
0 commit comments