{"spec_id":"raincloud-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nraincloud-basic: Basic Raincloud Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-05-26\n\"\"\"\n\nimport base64\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nBOX_FILL = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nGRID_ALPHA = 0.15 if THEME == \"light\" else 0.20\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data — reaction times (ms) across four experimental conditions\nnp.random.seed(42)\ncategories = [\"Control\", \"Treatment A\", \"Treatment B\", \"Treatment C\"]\nn_points = [80, 75, 85, 70]\ndata = {\n    \"Control\": np.random.normal(450, 80, n_points[0]),\n    \"Treatment A\": np.random.normal(380, 60, n_points[1]),\n    \"Treatment B\": np.concatenate(\n        [np.random.normal(350, 40, n_points[2] // 2), np.random.normal(460, 45, n_points[2] - n_points[2] // 2)]\n    ),\n    \"Treatment C\": np.random.normal(320, 50, n_points[3]),\n}\n\nW, H = 3200, 1800\n\np = figure(\n    width=W,\n    height=H,\n    title=\"raincloud-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Reaction Time (ms)\",\n    y_axis_label=\"Treatment Group\",\n    y_range=(-0.55, len(categories) - 0.30),\n    x_range=(150, 650),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=220,\n    min_border_top=110,\n    min_border_right=80,\n    tools=\"\",\n)\n\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = None\np.xgrid.grid_line_alpha = GRID_ALPHA\n\np.yaxis.ticker = list(range(len(categories)))\np.yaxis.major_label_overrides = dict(enumerate(categories))\n\nfor idx, (cat, values) in enumerate(data.items()):\n    color = IMPRINT_PALETTE[idx]\n    y_base = idx\n\n    # KDE — Silverman's rule\n    n = len(values)\n    std = np.std(values)\n    bw = 1.06 * std * n ** (-1 / 5)\n    x_min, x_max = values.min() - 25, values.max() + 25\n    x_kde = np.linspace(x_min, x_max, 256)\n    y_kde = np.zeros_like(x_kde)\n    for point in values:\n        y_kde += np.exp(-0.5 * ((x_kde - point) / bw) ** 2) / (bw * np.sqrt(2 * np.pi))\n    y_kde /= n\n    y_kde_scaled = y_kde / y_kde.max() * 0.40\n\n    # Cloud — half-violin above the baseline\n    violin_x = np.concatenate([x_kde, x_kde[::-1]])\n    violin_y = np.concatenate([y_base + y_kde_scaled, np.full(len(x_kde), y_base)])\n    p.patch(x=violin_x, y=violin_y, fill_color=color, fill_alpha=0.55, line_color=color, line_width=2)\n\n    # Box plot — sits on the baseline\n    q1, q2, q3 = np.percentile(values, [25, 50, 75])\n    iqr = q3 - q1\n    whisker_low = max(values.min(), q1 - 1.5 * iqr)\n    whisker_high = min(values.max(), q3 + 1.5 * iqr)\n    box_h = 0.11\n\n    p.line(x=[whisker_low, q1], y=[y_base, y_base], line_color=INK, line_width=3)\n    p.line(x=[q3, whisker_high], y=[y_base, y_base], line_color=INK, line_width=3)\n    p.line(x=[whisker_low, whisker_low], y=[y_base - box_h / 2, y_base + box_h / 2], line_color=INK, line_width=3)\n    p.line(x=[whisker_high, whisker_high], y=[y_base - box_h / 2, y_base + box_h / 2], line_color=INK, line_width=3)\n    p.patch(\n        x=[q1, q3, q3, q1],\n        y=[y_base - box_h, y_base - box_h, y_base + box_h, y_base + box_h],\n        fill_color=BOX_FILL,\n        fill_alpha=0.95,\n        line_color=INK,\n        line_width=3,\n    )\n    p.line(x=[q2, q2], y=[y_base - box_h, y_base + box_h], line_color=color, line_width=6)\n\n    # Rain — jittered points below the baseline\n    jitter = np.random.uniform(-0.40, -0.08, len(values))\n    mean_val = float(np.mean(values))\n    std_val = float(np.std(values))\n    source_points = ColumnDataSource(\n        data={\n            \"x\": values,\n            \"y\": y_base + jitter,\n            \"category\": [cat] * len(values),\n            \"mean\": [f\"{mean_val:.1f}\"] * len(values),\n            \"median\": [f\"{q2:.1f}\"] * len(values),\n            \"std\": [f\"{std_val:.1f}\"] * len(values),\n            \"n\": [str(n)] * len(values),\n        }\n    )\n    scatter_glyph = p.scatter(\n        x=\"x\",\n        y=\"y\",\n        source=source_points,\n        size=12,\n        fill_color=color,\n        fill_alpha=0.65,\n        line_color=PAGE_BG,\n        line_width=1.0,\n    )\n\n    hover = HoverTool(\n        renderers=[scatter_glyph],\n        tooltips=[\n            (\"Group\", \"@category\"),\n            (\"Value\", \"@x{0.1f} ms\"),\n            (\"Mean\", \"@mean ms\"),\n            (\"Median\", \"@median ms\"),\n            (\"Std\", \"@std ms\"),\n            (\"n\", \"@n\"),\n        ],\n        point_policy=\"follow_mouse\",\n    )\n    p.add_tools(hover)\n\n# Annotate Treatment B's bimodality — the most interesting feature of the synthetic data\np.add_layout(\n    Label(\n        x=475,\n        y=2.35,\n        text=\"bimodal distribution\",\n        text_color=INK_SOFT,\n        text_font_size=\"28pt\",\n        text_font_style=\"italic\",\n        text_align=\"left\",\n        text_baseline=\"middle\",\n    )\n)\n\n# Save HTML (catalog artifact) + PNG via headless Chrome\noutput_file(f\"plot-{THEME}.html\", title=\"raincloud-basic · python · bokeh · anyplot.ai\")\nsave(p)\n\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H + 200}\",\n    \"--hide-scrollbars\",\n    \"--force-device-scale-factor=1\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\n\nshot = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\",\n    {\"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}, \"captureBeyondViewport\": True},\n)\nPath(f\"plot-{THEME}.png\").write_bytes(base64.b64decode(shot[\"data\"]))\ndriver.quit()\n"}