{"spec_id":"band-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nband-basic: Basic Band Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-29\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\")\nImage = 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\"\n\n# Imprint palette\nBRAND = \"#009E73\"  # Imprint palette position 1 — ALWAYS first series\nIMPRINT_BLUE = \"#4467A3\"  # Imprint palette position 3 — band fills\nIMPRINT_RED = \"#AE3030\"  # Imprint palette position 5 — semantic callout anchor\n\n# Data - oscilloscope voltage measurement with growing uncertainty\nnp.random.seed(42)\nx = np.linspace(0, 10, 100)\ny_center = 2 * np.sin(x) + 0.5 * x  # Sinusoidal signal with linear drift\n\n# Confidence band widens over time (realistic sensor drift uncertainty)\nuncertainty = 0.5 + 0.15 * x\ny_lower = y_center - 1.96 * uncertainty\ny_upper = y_center + 1.96 * uncertainty\ny_inner_lower = y_center - 0.674 * uncertainty  # 50% CI inner band\ny_inner_upper = y_center + 0.674 * uncertainty\n\ndf = pd.DataFrame(\n    {\n        \"x\": x,\n        \"y_center\": y_center,\n        \"y_lower\": y_lower,\n        \"y_upper\": y_upper,\n        \"y_inner_lower\": y_inner_lower,\n        \"y_inner_upper\": y_inner_upper,\n    }\n)\n\n# Annotation data — callout at x≈9.0 s where uncertainty is wide and clearly visible\nann_row = df.iloc[90]\nann_df = pd.DataFrame(\n    {\n        \"x\": [ann_row[\"x\"]],\n        \"y_upper\": [ann_row[\"y_upper\"]],\n        \"y_lower\": [ann_row[\"y_lower\"]],\n        \"y_mid\": [(ann_row[\"y_upper\"] + ann_row[\"y_lower\"]) / 2],\n        \"label\": [f\"95% CI: ±{(ann_row['y_upper'] - ann_row['y_lower']) / 2:.1f} mV\"],\n    }\n)\n\n# Nearest-point selection for interactive HTML export\nnearest = alt.selection_point(nearest=True, on=\"pointerover\", fields=[\"x\"], empty=False)\n\n# 95% confidence band (outer) — Imprint blue at low opacity for depth\nband_outer = (\n    alt.Chart(df)\n    .mark_area(opacity=0.15, color=IMPRINT_BLUE, interpolate=\"monotone\")\n    .encode(\n        x=alt.X(\"x:Q\", title=\"Time (s)\"), y=alt.Y(\"y_lower:Q\", title=\"Oscilloscope Signal (mV)\"), y2=alt.Y2(\"y_upper:Q\")\n    )\n)\n\n# 50% confidence band (inner) — Imprint blue at higher opacity for layered contrast\nband_inner = (\n    alt.Chart(df)\n    .mark_area(opacity=0.30, color=IMPRINT_BLUE, interpolate=\"monotone\")\n    .encode(x=\"x:Q\", y=\"y_inner_lower:Q\", y2=\"y_inner_upper:Q\")\n)\n\n# Central trend line — Imprint brand green (first/primary series)\nline = alt.Chart(df).mark_line(strokeWidth=2.5, color=BRAND, interpolate=\"monotone\").encode(x=\"x:Q\", y=\"y_center:Q\")\n\n# Annotation: dashed vertical bracket showing uncertainty span\nann_rule = (\n    alt.Chart(ann_df)\n    .mark_rule(color=IMPRINT_RED, strokeWidth=1.5, strokeDash=[6, 3])\n    .encode(x=\"x:Q\", y=\"y_lower:Q\", y2=\"y_upper:Q\")\n)\n\nann_text = (\n    alt.Chart(ann_df)\n    .mark_text(align=\"left\", dx=10, fontSize=12, fontWeight=\"bold\", color=IMPRINT_RED)\n    .encode(x=\"x:Q\", y=\"y_mid:Q\", text=\"label:N\")\n)\n\n# Interactive tooltip points — visible on hover in HTML, hidden in static PNG\ntooltip_points = (\n    alt.Chart(df)\n    .mark_point(color=BRAND, size=80)\n    .encode(\n        x=\"x:Q\",\n        y=\"y_center:Q\",\n        opacity=alt.condition(nearest, alt.value(1), alt.value(0)),\n        tooltip=[\n            alt.Tooltip(\"x:Q\", title=\"Time (s)\", format=\".1f\"),\n            alt.Tooltip(\"y_center:Q\", title=\"Signal (mV)\", format=\".2f\"),\n            alt.Tooltip(\"y_lower:Q\", title=\"95% CI Lower\", format=\".2f\"),\n            alt.Tooltip(\"y_upper:Q\", title=\"95% CI Upper\", format=\".2f\"),\n        ],\n    )\n    .add_params(nearest)\n)\n\n# Vertical guide rule — visible on hover in HTML only\nguide_rule = (\n    alt.Chart(df)\n    .mark_rule(color=INK_SOFT, strokeDash=[4, 4])\n    .encode(x=\"x:Q\", opacity=alt.condition(nearest, alt.value(0.5), alt.value(0)))\n)\n\ntitle = \"band-basic · python · altair · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_fs = max(11, round(16 * ratio))\n\n# Combine layers with theme-adaptive chrome\nchart = (\n    (band_outer + band_inner + line + ann_rule + ann_text + tooltip_points + guide_rule)\n    .properties(width=620, height=320, background=PAGE_BG, title=alt.Title(title, fontSize=title_fs, color=INK))\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.12,\n    )\n    .configure_axisX(grid=False)\n)\n\n# Save PNG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n# Pad PNG to exact 3200×1800 target (vl-convert may land slightly short)\nTW, TH = 3200, 1800\n_img = Image.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 = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save HTML\nchart.save(f\"plot-{THEME}.html\")\n"}