Skip to content

Commit 66b37ca

Browse files
Merge branch 'main' into implementation/errorbar-basic/ggplot2
2 parents f29c530 + 7013281 commit 66b37ca

10 files changed

Lines changed: 762 additions & 547 deletions

File tree

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
errorbar-basic: Basic Error Bar Plot
3-
Library: altair 6.1.0 | Python 3.14.4
4-
Quality: 88/100 | Updated: 2026-04-25
3+
Library: altair 6.2.2 | Python 3.13.14
4+
Quality: 94/100 | Updated: 2026-06-30
55
"""
66

77
import importlib
@@ -14,22 +14,23 @@
1414
alt = importlib.import_module("altair")
1515
np = importlib.import_module("numpy")
1616
pd = importlib.import_module("pandas")
17-
17+
PILImage = importlib.import_module("PIL.Image")
1818

1919
# Theme tokens
2020
THEME = os.getenv("ANYPLOT_THEME", "light")
2121
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
2222
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
2323
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
2424
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
25-
BRAND = "#009E73" # Okabe-Ito position 1
25+
BRAND = "#009E73" # Imprint palette position 1
26+
ACCENT = "#BD8233" # Imprint ochre — highlights peak-response group
2627

2728
# Data
2829
np.random.seed(42)
2930
categories = ["Control", "Treatment A", "Treatment B", "Treatment C", "Treatment D", "Treatment E"]
3031
y_values = [25.3, 38.7, 42.1, 35.8, 48.2, 31.5]
3132

32-
# Asymmetric errors: Treatment C and D show notably different lower/upper bounds
33+
# Asymmetric errors: Treatment C shows wide lower bound; Treatment D peaks highest
3334
asymmetric_lower = [2.1, 3.5, 2.8, 6.5, 4.8, 2.5]
3435
asymmetric_upper = [2.1, 3.5, 2.8, 2.8, 2.2, 2.5]
3536

@@ -48,19 +49,29 @@
4849

4950
base = alt.Chart(df).encode(x=alt.X("category:N", title="Experimental Group", sort=categories))
5051

51-
error_bars = base.mark_rule(strokeWidth=3, color=BRAND).encode(
52-
y=alt.Y("error_lower:Q", title=y_title, scale=y_scale), y2="error_upper:Q"
52+
# Condition-based encoding: Treatment D (peak response) accented in ochre via alt.condition()
53+
highlight = alt.datum.category == "Treatment D"
54+
55+
error_bars = base.mark_rule(strokeWidth=3).encode(
56+
y=alt.Y("error_lower:Q", title=y_title, scale=y_scale),
57+
y2="error_upper:Q",
58+
color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),
5359
)
5460

55-
caps_top = base.mark_tick(thickness=3, size=22, color=BRAND).encode(
56-
y=alt.Y("error_upper:Q", title=y_title, scale=y_scale)
61+
caps_top = base.mark_tick(thickness=3, size=22).encode(
62+
y=alt.Y("error_upper:Q", title=y_title, scale=y_scale),
63+
color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),
5764
)
58-
caps_bottom = base.mark_tick(thickness=3, size=22, color=BRAND).encode(
59-
y=alt.Y("error_lower:Q", title=y_title, scale=y_scale)
65+
66+
caps_bottom = base.mark_tick(thickness=3, size=22).encode(
67+
y=alt.Y("error_lower:Q", title=y_title, scale=y_scale),
68+
color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),
6069
)
6170

62-
points = base.mark_circle(size=320, color=BRAND).encode(
71+
points = base.mark_circle().encode(
6372
y=alt.Y("value:Q", title=y_title, scale=y_scale),
73+
color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),
74+
size=alt.condition(highlight, alt.value(480), alt.value(280)),
6475
tooltip=[
6576
alt.Tooltip("category:N", title="Group"),
6677
alt.Tooltip("value:Q", title="Mean", format=".2f"),
@@ -69,28 +80,54 @@
6980
],
7081
)
7182

83+
# Annotation surfacing the "Peak response" label for Treatment D
84+
peak_df = df[df["category"] == "Treatment D"]
85+
annotation = (
86+
alt.Chart(peak_df)
87+
.mark_text(dy=-28, color=ACCENT, fontSize=10, fontStyle="italic", text="Peak response")
88+
.encode(x=alt.X("category:N", sort=categories), y=alt.Y("error_upper:Q", scale=y_scale))
89+
)
90+
7291
chart = (
73-
alt.layer(error_bars, caps_bottom, caps_top, points)
92+
alt.layer(error_bars, caps_bottom, caps_top, points, annotation)
7493
.properties(
75-
width=1600,
76-
height=900,
94+
width=620,
95+
height=320,
7796
background=PAGE_BG,
78-
title=alt.Title("errorbar-basic · altair · anyplot.ai", fontSize=28, color=INK, anchor="start", offset=20),
97+
title=alt.Title(
98+
"errorbar-basic · python · altair · anyplot.ai", fontSize=16, color=INK, anchor="start", offset=20
99+
),
79100
)
80101
.configure_view(fill=PAGE_BG, stroke=None)
81102
.configure_axis(
82-
labelFontSize=18,
83-
titleFontSize=22,
103+
labelFontSize=11,
104+
titleFontSize=12,
84105
labelColor=INK_SOFT,
85106
titleColor=INK,
86107
domainColor=INK_SOFT,
108+
domainOpacity=0,
87109
tickColor=INK_SOFT,
88110
gridColor=INK,
89-
gridOpacity=0.10,
111+
gridOpacity=0.13,
90112
labelAngle=0,
91113
)
92114
.configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
93115
)
94116

95-
chart.save(f"plot-{THEME}.png", scale_factor=3.0)
117+
chart.save(f"plot-{THEME}.png", scale_factor=4.0)
118+
119+
# Pad to exact 3200×1800 target — only expand, never crop (AR-09 guard)
120+
TW, TH = 3200, 1800
121+
_img = PILImage.open(f"plot-{THEME}.png").convert("RGB")
122+
_w, _h = _img.size
123+
if _w > TW or _h > TH:
124+
raise SystemExit(
125+
f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
126+
f"Shrink chart .properties(width=, height=) values and re-render."
127+
)
128+
if _w < TW or _h < TH:
129+
_canvas = PILImage.new("RGB", (TW, TH), PAGE_BG)
130+
_canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
131+
_canvas.save(f"plot-{THEME}.png")
132+
96133
chart.save(f"plot-{THEME}.html")

plots/errorbar-basic/implementations/python/bokeh.py

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
""" anyplot.ai
22
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
55
"""
66

77
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]
816

917
import numpy as np
10-
from bokeh.io import export_png, output_file, save
18+
from bokeh.io import output_file, save
1119
from bokeh.models import ColumnDataSource, Label, TeeHead, Whisker
1220
from bokeh.plotting import figure
21+
from selenium import webdriver
22+
from selenium.webdriver.chrome.options import Options
1323

1424

1525
# Theme tokens
@@ -20,7 +30,7 @@
2030
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
2131
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
2232

23-
# Okabe-Ito categorical palette (positions 1-6)
33+
# Imprint categorical palette (positions 1-6)
2434
IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD"]
2535

2636
# Data — experimental measurements with associated uncertainties
@@ -40,16 +50,22 @@
4050
data={"categories": categories, "means": means, "upper": upper, "lower": lower, "colors": colors}
4151
)
4252

43-
# Plot
53+
# Canvas: 3200×1800 (landscape) — hard rule, no deviation
54+
W, H = 3200, 1800
55+
4456
p = figure(
45-
width=4800,
46-
height=2700,
57+
width=W,
58+
height=H,
4759
x_range=categories,
48-
title="errorbar-basic · bokeh · anyplot.ai",
60+
title="errorbar-basic · python · bokeh · anyplot.ai",
4961
x_axis_label="Experimental Group",
5062
y_axis_label="Response Value (units)",
5163
toolbar_location=None,
5264
tools="",
65+
min_border_bottom=160,
66+
min_border_left=180,
67+
min_border_top=110,
68+
min_border_right=50,
5369
)
5470

5571
# Error bars (Whisker with TeeHead caps) — one whisker per group so each can take its own color
@@ -93,23 +109,23 @@
93109

94110
# Title
95111
p.title.text_color = INK
96-
p.title.text_font_size = "36pt"
112+
p.title.text_font_size = "50pt"
97113
p.title.text_font_style = "normal"
98114
p.title.align = "left"
99115

100116
# Axis labels
101117
p.xaxis.axis_label_text_color = INK
102118
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"
105121
p.xaxis.axis_label_text_font_style = "normal"
106122
p.yaxis.axis_label_text_font_style = "normal"
107123

108124
# Tick labels
109125
p.xaxis.major_label_text_color = INK_SOFT
110126
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"
113129

114130
# Axis lines and ticks
115131
p.xaxis.axis_line_color = INK_SOFT
@@ -121,7 +137,7 @@
121137

122138
# Subtle y-grid only
123139
p.ygrid.grid_line_color = INK
124-
p.ygrid.grid_line_alpha = 0.10
140+
p.ygrid.grid_line_alpha = 0.15
125141
p.xgrid.grid_line_color = None
126142

127143
# Y-range trimmed to data — eliminates dead space below
@@ -131,7 +147,27 @@
131147
p.y_range.start = max(0.0, y_min - y_pad)
132148
p.y_range.end = y_max + y_pad
133149

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")
137152
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()

plots/errorbar-basic/implementations/python/letsplot.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
""" anyplot.ai
22
errorbar-basic: Basic Error Bar Plot
3-
Library: letsplot 4.9.0 | Python 3.14.4
4-
Quality: 91/100 | Updated: 2026-04-25
3+
Library: letsplot 4.11.0 | Python 3.13.14
4+
Quality: 89/100 | Updated: 2026-06-30
55
"""
66

77
import os
@@ -32,11 +32,13 @@
3232
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
3333
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
3434
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
35+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
36+
RULE = "#1A1A1726" if THEME == "light" else "#F0EFE826" # 15% opacity — subtle grid
3537

36-
BRAND = "#009E73" # Okabe-Ito position 1
37-
FOCAL = "#C475FD" # Okabe-Ito position 2 — emphasises group with largest spread
38+
BRAND = "#009E73" # Imprint palette position 1
39+
FOCAL = "#C475FD" # Imprint palette position 2 — highlights group with largest spread
3840

39-
# Data — experimental measurements with uncertainty
41+
# Data — clinical measurements comparing control vs treatment groups
4042
data = pd.DataFrame(
4143
{
4244
"experiment": ["Control", "Treatment A", "Treatment B", "Treatment C", "Treatment D"],
@@ -47,24 +49,29 @@
4749
data["ymin"] = data["mean_value"] - data["error"]
4850
data["ymax"] = data["mean_value"] + data["error"]
4951

50-
# Highlight the group with the largest error spread to give the chart a focal point
5152
focal_idx = data["error"].idxmax()
5253
data["highlight"] = ["focal" if i == focal_idx else "base" for i in data.index]
5354

5455
color_map = {"base": BRAND, "focal": FOCAL}
5556

57+
title = "errorbar-basic · python · letsplot · anyplot.ai"
58+
subtitle = (
59+
"Treatment D (highlighted) has the widest error margin (±7.1 mg/dL), indicating higher measurement variability"
60+
)
61+
5662
anyplot_theme = theme(
5763
plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
5864
panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
5965
panel_border=element_blank(),
6066
panel_grid_major_x=element_blank(),
6167
panel_grid_minor=element_blank(),
62-
panel_grid_major_y=element_line(color=INK_SOFT, size=0.3),
68+
panel_grid_major_y=element_line(color=RULE, size=0.3),
6369
axis_line=element_line(color=INK_SOFT),
6470
axis_ticks=element_line(color=INK_SOFT),
65-
axis_title=element_text(color=INK, size=20),
66-
axis_text=element_text(color=INK_SOFT, size=16),
67-
plot_title=element_text(color=INK, size=24),
71+
axis_title=element_text(color=INK, size=12),
72+
axis_text=element_text(color=INK_SOFT, size=10),
73+
plot_title=element_text(color=INK, size=16),
74+
plot_subtitle=element_text(color=INK_SOFT, size=11),
6875
legend_position="none",
6976
)
7077

@@ -73,14 +80,14 @@
7380
+ geom_errorbar(aes(ymin="ymin", ymax="ymax"), width=0.3, size=1.5)
7481
+ geom_point(size=6)
7582
+ scale_color_manual(values=color_map)
76-
+ labs(x="Experimental Group", y="Measured Value (mg/dL)", title="errorbar-basic · letsplot · anyplot.ai")
83+
+ labs(x="Experimental Group", y="Measured Value (mg/dL)", title=title, subtitle=subtitle)
7784
+ theme_minimal()
7885
+ anyplot_theme
79-
+ ggsize(1600, 900)
86+
+ ggsize(800, 450)
8087
)
8188

82-
# PNG (scale 3x for 4800 x 2700 px)
83-
ggsave(plot, f"plot-{THEME}.png", scale=3, path=".")
89+
# PNG (scale=4 → 3200 × 1800 px)
90+
ggsave(plot, f"plot-{THEME}.png", scale=4, path=".")
8491

8592
# HTML (interactive)
8693
ggsave(plot, f"plot-{THEME}.html", path=".")

0 commit comments

Comments
 (0)