{"spec_id":"errorbar-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nerrorbar-basic: Basic Error Bar Plot\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-06-30\n\"\"\"\n\nimport importlib\nimport os\nimport sys\n\n\n# Drop script directory from sys.path so the `altair` package resolves, not this file\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\nalt = importlib.import_module(\"altair\")\nnp = importlib.import_module(\"numpy\")\npd = importlib.import_module(\"pandas\")\nPILImage = importlib.import_module(\"PIL.Image\")\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nBRAND = \"#009E73\"  # Imprint palette position 1\nACCENT = \"#BD8233\"  # Imprint ochre — highlights peak-response group\n\n# Data\nnp.random.seed(42)\ncategories = [\"Control\", \"Treatment A\", \"Treatment B\", \"Treatment C\", \"Treatment D\", \"Treatment E\"]\ny_values = [25.3, 38.7, 42.1, 35.8, 48.2, 31.5]\n\n# Asymmetric errors: Treatment C shows wide lower bound; Treatment D peaks highest\nasymmetric_lower = [2.1, 3.5, 2.8, 6.5, 4.8, 2.5]\nasymmetric_upper = [2.1, 3.5, 2.8, 2.8, 2.2, 2.5]\n\ndf = pd.DataFrame(\n    {\n        \"category\": categories,\n        \"value\": y_values,\n        \"error_lower\": [y - el for y, el in zip(y_values, asymmetric_lower, strict=True)],\n        \"error_upper\": [y + eu for y, eu in zip(y_values, asymmetric_upper, strict=True)],\n    }\n)\n\n# Plot\ny_scale = alt.Scale(domain=[15, 55], nice=False)\ny_title = \"Response Value (units)\"\n\nbase = alt.Chart(df).encode(x=alt.X(\"category:N\", title=\"Experimental Group\", sort=categories))\n\n# Condition-based encoding: Treatment D (peak response) accented in ochre via alt.condition()\nhighlight = alt.datum.category == \"Treatment D\"\n\nerror_bars = base.mark_rule(strokeWidth=3).encode(\n    y=alt.Y(\"error_lower:Q\", title=y_title, scale=y_scale),\n    y2=\"error_upper:Q\",\n    color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),\n)\n\ncaps_top = base.mark_tick(thickness=3, size=22).encode(\n    y=alt.Y(\"error_upper:Q\", title=y_title, scale=y_scale),\n    color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),\n)\n\ncaps_bottom = base.mark_tick(thickness=3, size=22).encode(\n    y=alt.Y(\"error_lower:Q\", title=y_title, scale=y_scale),\n    color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),\n)\n\npoints = base.mark_circle().encode(\n    y=alt.Y(\"value:Q\", title=y_title, scale=y_scale),\n    color=alt.condition(highlight, alt.value(ACCENT), alt.value(BRAND)),\n    size=alt.condition(highlight, alt.value(480), alt.value(280)),\n    tooltip=[\n        alt.Tooltip(\"category:N\", title=\"Group\"),\n        alt.Tooltip(\"value:Q\", title=\"Mean\", format=\".2f\"),\n        alt.Tooltip(\"error_lower:Q\", title=\"Lower bound\", format=\".2f\"),\n        alt.Tooltip(\"error_upper:Q\", title=\"Upper bound\", format=\".2f\"),\n    ],\n)\n\n# Annotation surfacing the \"Peak response\" label for Treatment D\npeak_df = df[df[\"category\"] == \"Treatment D\"]\nannotation = (\n    alt.Chart(peak_df)\n    .mark_text(dy=-28, color=ACCENT, fontSize=10, fontStyle=\"italic\", text=\"Peak response\")\n    .encode(x=alt.X(\"category:N\", sort=categories), y=alt.Y(\"error_upper:Q\", scale=y_scale))\n)\n\nchart = (\n    alt.layer(error_bars, caps_bottom, caps_top, points, annotation)\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"errorbar-basic · python · altair · anyplot.ai\", fontSize=16, color=INK, anchor=\"start\", offset=20\n        ),\n    )\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        labelFontSize=11,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        domainOpacity=0,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.13,\n        labelAngle=0,\n    )\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad to exact 3200×1800 target — only expand, never crop (AR-09 guard)\nTW, TH = 3200, 1800\n_img = PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = PILImage.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}