{"spec_id":"raincloud-basic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nraincloud-basic: Basic Raincloud Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-26\n\"\"\"\n\nimport os\nimport sys\n\n\n# Avoid shadowing the installed altair package with this file's directory\nsys.path[:] = [p for p in sys.path if os.path.realpath(p or \".\") != os.path.dirname(os.path.realpath(__file__))]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\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 — first three positions for three abstract categories\nCOLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\nSEMANTIC_RED = \"#AE3030\"  # palette position 5 — median highlight\n\n# Data: Reaction times (ms) across three experimental conditions\nnp.random.seed(42)\ncontrol = np.random.normal(450, 60, 80)\ntreatment_a = np.random.normal(380, 50, 80)\ntreatment_b = np.concatenate([np.random.normal(340, 25, 50), np.random.normal(460, 35, 30)])\n\ncondition_order = [\"Control\", \"Treatment A\", \"Treatment B\"]\ndata = pd.DataFrame(\n    {\n        \"condition\": [\"Control\"] * 80 + [\"Treatment A\"] * 80 + [\"Treatment B\"] * 80,\n        \"reaction_time\": np.concatenate([control, treatment_a, treatment_b]),\n    }\n)\n\n# Map conditions to numeric y baselines (1.5 spacing between rows)\ncondition_map = {c: i * 1.5 for i, c in enumerate(condition_order)}\ndata[\"condition_num\"] = data[\"condition\"].map(condition_map)\n\n# Rain positions — jittered BELOW each baseline\ndata[\"jitter_pos\"] = data[\"condition_num\"] + np.random.uniform(-0.22, -0.08, len(data))\n\n# Box plot statistics per condition\nbox_rows = []\nfor cond in condition_order:\n    vals = data.loc[data[\"condition\"] == cond, \"reaction_time\"]\n    q1, med, q3 = vals.quantile([0.25, 0.5, 0.75])\n    iqr = q3 - q1\n    box_rows.append(\n        {\n            \"condition\": cond,\n            \"condition_num\": condition_map[cond],\n            \"q1\": q1,\n            \"median\": med,\n            \"q3\": q3,\n            \"lower_w\": max(q1 - 1.5 * iqr, vals.min()),\n            \"upper_w\": min(q3 + 1.5 * iqr, vals.max()),\n        }\n    )\nbox_df = pd.DataFrame(box_rows)\n\n# Scales\nx_min, x_max = data[\"reaction_time\"].min(), data[\"reaction_time\"].max()\nx_pad = (x_max - x_min) * 0.06\nx_domain = [round(x_min - x_pad, -1), round(x_max + x_pad, -1)]\nx_scale = alt.Scale(domain=x_domain)\ny_domain = [-0.5, 4.3]\ny_scale = alt.Scale(domain=y_domain)\ncolor_scale = alt.Scale(domain=condition_order, range=COLORS)\n\n# Native y-axis labels for the three condition baselines\ny_axis = alt.Axis(\n    values=[0, 1.5, 3.0],\n    labelExpr=(\n        \"datum.value === 0 ? 'Control' : datum.value === 1.5 ? 'Treatment A' : datum.value === 3 ? 'Treatment B' : ''\"\n    ),\n    title=None,\n    labelFontSize=12,\n    labelFontWeight=\"bold\",\n    labelColor=INK,\n    labelPadding=8,\n    domain=False,\n    ticks=False,\n    grid=False,\n)\n\n# Half-violin \"cloud\" — extends ABOVE the baseline\nviolin = (\n    alt.Chart(data)\n    .transform_density(\n        \"reaction_time\", as_=[\"reaction_time\", \"density\"], groupby=[\"condition\", \"condition_num\"], extent=x_domain\n    )\n    .transform_calculate(violin_pos=\"datum.condition_num + 0.04 + datum.density * 100\")\n    .mark_area(orient=\"vertical\", opacity=0.55, interpolate=\"monotone\")\n    .encode(\n        x=alt.X(\"reaction_time:Q\", title=\"Reaction Time (ms)\", scale=x_scale),\n        y=alt.Y(\"condition_num:Q\", axis=y_axis, scale=y_scale),\n        y2=\"violin_pos:Q\",\n        color=alt.Color(\"condition:N\", scale=color_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"condition:N\", title=\"Condition\"),\n            alt.Tooltip(\"reaction_time:Q\", title=\"Reaction Time (ms)\", format=\".0f\"),\n        ],\n    )\n)\n\n# IQR box — elevated fill with INK outline distinguishes from cloud\nbox_iqr = (\n    alt.Chart(box_df)\n    .mark_bar(height=14, stroke=INK, strokeWidth=1.5, cornerRadius=2, fill=ELEVATED_BG, fillOpacity=0.95)\n    .encode(x=alt.X(\"q1:Q\", scale=x_scale), x2=\"q3:Q\", y=alt.Y(\"condition_num:Q\", scale=y_scale))\n)\n\n# Median tick — semantic red\nbox_median = (\n    alt.Chart(box_df)\n    .mark_tick(thickness=3, color=SEMANTIC_RED, orient=\"vertical\", size=14)\n    .encode(x=alt.X(\"median:Q\", scale=x_scale), y=alt.Y(\"condition_num:Q\", scale=y_scale))\n)\n\n# Whisker rules\nbox_whiskers = (\n    alt.Chart(box_df)\n    .mark_rule(strokeWidth=1.2, color=INK_SOFT)\n    .encode(x=alt.X(\"lower_w:Q\", scale=x_scale), x2=\"upper_w:Q\", y=alt.Y(\"condition_num:Q\", scale=y_scale))\n)\n\n# Rain — jittered strip BELOW the baseline\nstrip = (\n    alt.Chart(data)\n    .mark_circle(size=14, opacity=0.55)\n    .encode(\n        x=alt.X(\"reaction_time:Q\", scale=x_scale),\n        y=alt.Y(\"jitter_pos:Q\", scale=y_scale),\n        color=alt.Color(\"condition:N\", scale=color_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"condition:N\", title=\"Condition\"),\n            alt.Tooltip(\"reaction_time:Q\", title=\"Reaction Time (ms)\", format=\".1f\"),\n        ],\n    )\n)\n\n# Bimodality annotation for Treatment B (data storytelling)\nannotation_df = pd.DataFrame([{\"x\": 340, \"y\": 3.95, \"text\": \"Peak 1\"}, {\"x\": 460, \"y\": 3.95, \"text\": \"Peak 2\"}])\nbimodal_labels = (\n    alt.Chart(annotation_df)\n    .mark_text(fontSize=10, fontStyle=\"italic\", color=INK, fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=alt.Y(\"y:Q\", scale=y_scale), text=\"text:N\")\n)\n\narrow_df = pd.DataFrame([{\"x\": 355, \"y\": 3.95, \"x2\": 445, \"y2\": 3.95}])\nbimodal_arrow = (\n    alt.Chart(arrow_df)\n    .mark_rule(strokeDash=[4, 3], color=INK_SOFT, strokeWidth=1.2)\n    .encode(x=\"x:Q\", y=alt.Y(\"y:Q\", scale=y_scale), x2=\"x2:Q\", y2=\"y2:Q\")\n)\n\nnote_df = pd.DataFrame([{\"x\": 400, \"y\": 4.20, \"text\": \"Bimodal distribution\"}])\nbimodal_note = (\n    alt.Chart(note_df)\n    .mark_text(fontSize=10, color=INK, fontStyle=\"italic\", fontWeight=\"bold\")\n    .encode(x=\"x:Q\", y=alt.Y(\"y:Q\", scale=y_scale), text=\"text:N\")\n)\n\n# Median value labels above each box\nmedian_labels = (\n    alt.Chart(box_df)\n    .mark_text(fontSize=10, color=SEMANTIC_RED, fontWeight=\"bold\", dy=-14)\n    .encode(\n        x=alt.X(\"median:Q\", scale=x_scale),\n        y=alt.Y(\"condition_num:Q\", scale=y_scale),\n        text=alt.Text(\"median:Q\", format=\".0f\"),\n    )\n)\n\nchart = (\n    alt.layer(\n        violin, box_whiskers, box_iqr, box_median, median_labels, strip, bimodal_labels, bimodal_arrow, bimodal_note\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"raincloud-basic · python · altair · anyplot.ai\",\n            fontSize=16,\n            fontWeight=\"bold\",\n            anchor=\"middle\",\n            offset=10,\n            color=INK,\n        ),\n    )\n    .configure(padding={\"left\": 15, \"right\": 15, \"top\": 5, \"bottom\": 15})\n    .configure_view(fill=PAGE_BG, stroke=None)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleFontWeight=\"bold\",\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.15,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_title(color=INK)\n    .interactive()\n)\n\n# Save PNG, then pad to canonical 3200×1800 with PAGE_BG\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\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 (interactive)\nchart.save(f\"plot-{THEME}.html\")\n"}