Skip to content

Commit 1b798fa

Browse files
Merge branch 'main' into implementation/errorbar-basic/bokeh
2 parents a2c53bc + 9f78c6a commit 1b798fa

6 files changed

Lines changed: 490 additions & 316 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")
Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,114 @@
11
""" anyplot.ai
22
errorbar-basic: Basic Error Bar Plot
3-
Library: matplotlib 3.10.9 | Python 3.14.4
4-
Quality: 86/100 | Updated: 2026-04-25
3+
Library: matplotlib 3.11.0 | Python 3.13.14
4+
Quality: 91/100 | Updated: 2026-06-30
55
"""
66

77
import os
88

99
import matplotlib.pyplot as plt
1010
import numpy as np
11+
from matplotlib.lines import Line2D
1112

1213

1314
# Theme tokens
1415
THEME = os.getenv("ANYPLOT_THEME", "light")
1516
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
17+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
1618
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
1719
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
18-
BRAND = "#009E73" # Okabe-Ito position 1
20+
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
21+
BRAND = "#009E73" # Imprint palette position 1 — in-spec machines
22+
IMPRINT_RED = "#AE3030" # Imprint palette position 5 — out-of-spec machines
1923

20-
# Data - experimental measurements with associated uncertainties
21-
np.random.seed(42)
22-
categories = ["Control", "Treatment A", "Treatment B", "Treatment C", "Treatment D", "Treatment E"]
23-
x = np.arange(len(categories))
24-
y = np.array([25.3, 38.7, 42.1, 35.8, 48.2, 31.5])
24+
# Data: fill weight calibration across 6 production machines (target: 500 g, USL: 505 g)
25+
# Machines 1-3 are well-calibrated (symmetric errors); machines 4-6 show process drift (asymmetric)
26+
machines = ["Machine 1", "Machine 2", "Machine 3", "Machine 4", "Machine 5", "Machine 6"]
27+
x = np.arange(len(machines))
28+
weights = np.array([498.2, 499.8, 502.3, 507.5, 511.8, 518.4])
29+
TARGET = 500.0
30+
UPPER_SPEC = 505.0
2531

26-
# Asymmetric errors: Treatment C and D have notably different lower/upper bounds
27-
asymmetric_lower = np.array([2.1, 3.5, 2.8, 6.5, 4.8, 2.5])
28-
asymmetric_upper = np.array([2.1, 3.5, 2.8, 2.8, 2.2, 2.5])
32+
# Symmetric errors for well-calibrated machines; asymmetric for drifting machines
33+
errors_lower = np.array([1.5, 1.2, 2.1, 2.8, 3.2, 4.1])
34+
errors_upper = np.array([1.5, 1.2, 2.1, 4.5, 6.8, 8.3])
35+
36+
# Color by compliance: in-spec = brand green, out-of-spec = Imprint red
37+
point_colors = [BRAND if w <= UPPER_SPEC else IMPRINT_RED for w in weights]
2938

3039
# Plot
31-
fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG)
40+
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
3241
ax.set_facecolor(PAGE_BG)
3342

34-
ax.errorbar(
35-
x,
36-
y,
37-
yerr=[asymmetric_lower, asymmetric_upper],
38-
fmt="o",
39-
markersize=15,
40-
color=BRAND,
41-
ecolor=BRAND,
42-
elinewidth=3,
43-
capsize=10,
44-
capthick=3,
45-
markeredgecolor=PAGE_BG,
46-
markeredgewidth=1.2,
47-
alpha=0.95,
48-
)
43+
# Tolerance band and reference lines drawn first (below data)
44+
ax.axhspan(TARGET - 5, UPPER_SPEC, color=BRAND, alpha=0.07, zorder=0)
45+
ax.axhline(TARGET, color=INK_SOFT, linewidth=1.2, linestyle="--", alpha=0.6, zorder=1)
46+
ax.axhline(UPPER_SPEC, color=IMPRINT_RED, linewidth=1.2, linestyle=":", alpha=0.7, zorder=1)
47+
48+
# One errorbar call per point to support per-machine color coding
49+
for xi, yi, el, eu, col in zip(x, weights, errors_lower, errors_upper, point_colors, strict=True):
50+
ax.errorbar(
51+
xi,
52+
yi,
53+
yerr=[[el], [eu]],
54+
fmt="o",
55+
markersize=8,
56+
color=col,
57+
ecolor=col,
58+
elinewidth=2.5,
59+
capsize=7,
60+
capthick=2.5,
61+
markeredgecolor=PAGE_BG,
62+
markeredgewidth=0.8,
63+
alpha=0.95,
64+
)
4965

5066
# Style
51-
ax.set_xlabel("Experimental Group", fontsize=20, color=INK)
52-
ax.set_ylabel("Response Value (units)", fontsize=20, color=INK)
53-
ax.set_title("errorbar-basic · matplotlib · anyplot.ai", fontsize=24, fontweight="medium", color=INK)
67+
title = "errorbar-basic · python · matplotlib · anyplot.ai"
68+
ax.set_title(title, fontsize=12, fontweight="medium", color=INK)
69+
ax.set_xlabel("Production Machine", fontsize=10, color=INK)
70+
ax.set_ylabel("Fill Weight (g)", fontsize=10, color=INK)
5471
ax.set_xticks(x)
55-
ax.set_xticklabels(categories)
56-
ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT)
72+
ax.set_xticklabels(machines)
73+
ax.tick_params(axis="both", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)
5774
ax.spines["top"].set_visible(False)
5875
ax.spines["right"].set_visible(False)
5976
for s in ("left", "bottom"):
6077
ax.spines[s].set_color(INK_SOFT)
61-
ax.yaxis.grid(True, alpha=0.10, linewidth=0.8, color=INK)
78+
ax.yaxis.grid(True, alpha=0.12, linewidth=0.8, color=INK)
6279
ax.set_axisbelow(True)
63-
ax.set_ylim(0, max(y + asymmetric_upper) * 1.15)
80+
ax.set_ylim(490, 532)
81+
82+
# Legend — in-spec/out-of-spec groups plus reference lines
83+
legend_elements = [
84+
Line2D(
85+
[0],
86+
[0],
87+
marker="o",
88+
color="none",
89+
markerfacecolor=BRAND,
90+
markeredgecolor=PAGE_BG,
91+
markersize=8,
92+
label="In-spec",
93+
),
94+
Line2D(
95+
[0],
96+
[0],
97+
marker="o",
98+
color="none",
99+
markerfacecolor=IMPRINT_RED,
100+
markeredgecolor=PAGE_BG,
101+
markersize=8,
102+
label="Out-of-spec",
103+
),
104+
Line2D([0], [0], color=INK_SOFT, linewidth=1.2, linestyle="--", label="Target (500 g)"),
105+
Line2D([0], [0], color=IMPRINT_RED, linewidth=1.2, linestyle=":", label="Upper spec (505 g)"),
106+
]
107+
leg = ax.legend(handles=legend_elements, fontsize=8, loc="upper left")
108+
leg.get_frame().set_facecolor(ELEVATED_BG)
109+
leg.get_frame().set_edgecolor(INK_SOFT)
110+
plt.setp(leg.get_texts(), color=INK_SOFT)
64111

112+
# Save
65113
plt.tight_layout()
66-
plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG)
114+
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)

0 commit comments

Comments
 (0)