Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 55 additions & 31 deletions plots/errorbar-basic/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
""" anyplot.ai
errorbar-basic: Basic Error Bar Plot
Library: pygal 3.1.0 | Python 3.14.4
Quality: 88/100 | Updated: 2026-04-25
Library: pygal 3.1.3 | Python 3.13.14
Quality: 84/100 | Updated: 2026-06-30
"""

import os
import sys


# Remove script's own directory from sys.path so 'import pygal' resolves the
# installed package rather than this file.
sys.path[:] = [p for p in sys.path if os.path.abspath(p or ".") != os.path.dirname(os.path.abspath(__file__))]

import pygal
from pygal.style import Style
Expand All @@ -14,54 +20,57 @@
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

BRAND = "#009E73" # Okabe-Ito position 1 — primary data color
IMPRINT_PALETTE = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314")
BRAND = IMPRINT_PALETTE[0] # #009E73 — Imprint palette position 1, always first series
ANYPLOT_AMBER = "#DDCC77" # warning / caution annotation (outside categorical pool, per pygal.md)

# Data — dose-response study: vehicle control + escalating doses (mg/kg)
categories = ["Vehicle", "1 mg/kg", "3 mg/kg", "10 mg/kg", "30 mg/kg", "100 mg/kg"]
means = [25.3, 28.4, 33.1, 38.7, 47.5, 42.0]
# Asymmetric errors: 10 and 30 mg/kg show notable lower-tail variability
# Asymmetric errors: 10 mg/kg shows notable lower-tail variability
err_lower = [2.1, 2.5, 3.0, 6.2, 4.8, 3.4]
err_upper = [2.1, 2.5, 3.0, 2.8, 2.2, 3.4]

n = len(categories)
baseline = means[0] # Vehicle/control mean — visual reference for treatment effects

# Style — first series (baseline reference) uses muted ink, all data series use brand green
# Colors: series ordering determines color assignment
# 1: Mean ± error → BRAND; 2: asymmetry callout → ANYPLOT_AMBER; 3: baseline → INK_MUTED; 4+: error bars → BRAND
colors_tuple = (BRAND, ANYPLOT_AMBER, INK_MUTED) + (BRAND,) * 62

custom_style = Style(
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
colors=(INK_MUTED,) + (BRAND,) * 64,
title_font_size=56,
label_font_size=40,
major_label_font_size=40,
legend_font_size=36,
value_font_size=28,
tooltip_font_size=32,
stroke_width=6,
colors=colors_tuple,
title_font_size=66,
label_font_size=56,
major_label_font_size=44,
legend_font_size=44,
value_font_size=36,
tooltip_font_size=36,
stroke_width=4,
opacity=1.0,
opacity_hover=0.85,
)

# Y-axis zoomed to the data band with breathing room above and below
# Y-axis range with breathing room
data_min = min(m - e for m, e in zip(means, err_lower, strict=True))
data_max = max(m + e for m, e in zip(means, err_upper, strict=True))
pad = (data_max - data_min) * 0.15
y_min = max(0.0, data_min - pad)
y_max = data_max + pad

# x positions shifted to 1..n so pygal's implicit x=0 line stays off-canvas
chart = pygal.XY(
style=custom_style,
width=4800,
height=2700,
width=3200,
height=1800,
dots_size=24,
title="errorbar-basic · pygal · anyplot.ai",
title="errorbar-basic · python · pygal · anyplot.ai",
x_title="Dose",
y_title="Response Value (units)",
show_legend=True,
Expand All @@ -77,8 +86,32 @@

chart.x_labels = [{"label": categories[i], "value": i + 1} for i in range(n)]
chart.x_labels_major = [{"label": categories[i], "value": i + 1} for i in range(n)]
chart.y_labels = [20, 25, 30, 35, 40, 45, 50]

# Mean points — first series (BRAND #009E73) so brand green is assigned as palette position 1
mean_points = [
{"value": (i + 1, means[i]), "label": f"{categories[i]}: {means[i]:.1f} (−{err_lower[i]:.1f}/+{err_upper[i]:.1f})"}
for i in range(n)
]
chart.add("Mean ± error", mean_points, stroke=False, dots_size=28)

# Asymmetric variability callout — second series (ANYPLOT_AMBER warning color)
# Amber marker overlaid at 10 mg/kg creates a visual focal point for the chart's key insight:
# lower-tail variability (6.2) substantially exceeds upper (2.8), indicating non-Gaussian spread
asym_idx = 3 # 10 mg/kg
chart.add(
"↓ Asymmetric at 10 mg/kg (−6.2 / +2.8)",
[
{
"value": (asym_idx + 1, means[asym_idx]),
"label": "10 mg/kg: lower-tail variability (6.2) ≫ upper (2.8) — floor-effect compression near peak response",
}
],
stroke=False,
dots_size=38,
)

# Vehicle baseline reference line — visual hierarchy anchor at control mean
# Vehicle baseline reference line — third series (INK_MUTED) for control comparison
chart.add(
"Vehicle baseline",
[(0.5, baseline), (n + 0.5, baseline)],
Expand All @@ -87,21 +120,12 @@
stroke_style={"width": 3, "dasharray": "18, 14"},
)

cap_width = 0.16 # Width of error bar caps in x-axis units

# Mean points with rich tooltip labels (pygal interactive feature)
mean_points = [
{"value": (i + 1, means[i]), "label": f"{categories[i]}: {means[i]:.1f} (-{err_lower[i]:.1f}/+{err_upper[i]:.1f})"}
for i in range(n)
]
chart.add("Mean ± error", mean_points, stroke=False, dots_size=28)

# Error bars: separate series per segment for clean line breaks (no legend entry)
# Error bars: vertical stem + upper and lower caps per data point
cap_width = 0.16
for i in range(n):
x = i + 1
low = means[i] - err_lower[i]
high = means[i] + err_upper[i]

chart.add(None, [(x, low), (x, high)], stroke=True, show_dots=False)
chart.add(None, [(x - cap_width, low), (x + cap_width, low)], stroke=True, show_dots=False)
chart.add(None, [(x - cap_width, high), (x + cap_width, high)], stroke=True, show_dots=False)
Expand Down
Loading
Loading