{"spec_id":"box-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nbox-basic: Basic Box Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-05-28\n\"\"\"\n\nimport io\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\nfrom PIL import Image\n\n\n# Remove this script's own directory from sys.path so the installed\n# bokeh package is found instead of this file (also named bokeh.py).\n_own_dir = os.path.dirname(os.path.realpath(__file__))\nsys.path = [p for p in sys.path if os.path.realpath(p or \".\") != _own_dir]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, LabelSet, Whisker\nfrom bokeh.plotting import figure\nfrom bokeh.transform import factor_cmap\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — test scores across 4 classes with varying distributions\nnp.random.seed(42)\ncategories = [\"Class A\", \"Class B\", \"Class C\", \"Class D\"]\n\nscores = {\n    \"Class A\": np.random.normal(75, 10, 100),\n    \"Class B\": np.concatenate([np.random.normal(85, 5, 90), np.array([65, 68, 70])]),\n    \"Class C\": np.clip(np.random.normal(68, 14, 100), 30, None),\n    \"Class D\": np.concatenate([np.random.normal(78, 8, 95), np.array([100, 102, 50, 52])]),\n}\n\n# Box plot statistics\nbox_data = {\"cat\": [], \"q1\": [], \"q2\": [], \"q3\": [], \"upper\": [], \"lower\": [], \"iqr\": []}\noutlier_x = []\noutlier_y = []\n\nfor cat in categories:\n    values = np.array(scores[cat])\n    q1 = np.percentile(values, 25)\n    q2 = np.percentile(values, 50)\n    q3 = np.percentile(values, 75)\n    iqr = q3 - q1\n    upper_fence = q3 + 1.5 * iqr\n    lower_fence = q1 - 1.5 * iqr\n    upper_whisker = values[values <= upper_fence].max()\n    lower_whisker = values[values >= lower_fence].min()\n\n    box_data[\"cat\"].append(cat)\n    box_data[\"q1\"].append(round(q1, 1))\n    box_data[\"q2\"].append(round(q2, 1))\n    box_data[\"q3\"].append(round(q3, 1))\n    box_data[\"upper\"].append(round(upper_whisker, 1))\n    box_data[\"lower\"].append(round(lower_whisker, 1))\n    box_data[\"iqr\"].append(round(iqr, 1))\n\n    outliers = values[(values < lower_fence) | (values > upper_fence)]\n    for o in outliers:\n        outlier_x.append(cat)\n        outlier_y.append(round(o, 1))\n\nsource = ColumnDataSource(data=box_data)\n\n# Figure\ntitle = \"box-basic · python · bokeh · anyplot.ai\"\nW, H = 3200, 1800\np = figure(\n    x_range=categories,\n    width=W,\n    height=H,\n    title=title,\n    x_axis_label=\"Class\",\n    y_axis_label=\"Test Score (points)\",\n    toolbar_location=None,\n    tools=\"\",\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Boxes (q1–q3)\ncmap = factor_cmap(\"cat\", palette=IMPRINT_PALETTE[:4], factors=categories)\nbox_width = 0.7\n\np.vbar(\n    x=\"cat\",\n    top=\"q3\",\n    bottom=\"q2\",\n    source=source,\n    width=box_width,\n    fill_color=cmap,\n    line_color=INK_SOFT,\n    line_width=2,\n    fill_alpha=0.85,\n)\np.vbar(\n    x=\"cat\",\n    top=\"q2\",\n    bottom=\"q1\",\n    source=source,\n    width=box_width,\n    fill_color=cmap,\n    line_color=INK_SOFT,\n    line_width=2,\n    fill_alpha=0.85,\n)\n\n# Median line\nmedian_source = ColumnDataSource(data={\"x\": box_data[\"cat\"], \"y\": box_data[\"q2\"]})\np.rect(x=\"x\", y=\"y\", width=box_width, height=0.3, source=median_source, fill_color=INK, line_color=INK)\n\n# Whiskers\nwhisker = Whisker(\n    base=\"cat\", upper=\"upper\", lower=\"lower\", source=source, level=\"annotation\", line_width=3, line_color=INK_SOFT\n)\nwhisker.upper_head.size = 40\nwhisker.lower_head.size = 40\nwhisker.upper_head.line_width = 3\nwhisker.lower_head.line_width = 3\np.add_layout(whisker)\n\n# Outliers\nif outlier_x:\n    outlier_source = ColumnDataSource(data={\"x\": outlier_x, \"y\": outlier_y})\n    p.scatter(\n        x=\"x\",\n        y=\"y\",\n        source=outlier_source,\n        size=18,\n        fill_color=PAGE_BG,\n        line_color=INK_SOFT,\n        line_width=2.5,\n        marker=\"circle\",\n        alpha=0.9,\n    )\n\n# Annotations — highlight tightest and widest spread\nclass_b_iqr = box_data[\"iqr\"][1]\nclass_c_iqr = box_data[\"iqr\"][2]\n\nannotation_source = ColumnDataSource(\n    data={\n        \"x\": [\"Class B\", \"Class C\"],\n        \"y\": [box_data[\"upper\"][1] + 6, box_data[\"upper\"][2] + 6],\n        \"text\": [f\"Tightest spread (IQR = {class_b_iqr})\", f\"Widest spread (IQR = {class_c_iqr})\"],\n    }\n)\nlabels = LabelSet(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    text_color=INK_SOFT,\n    source=annotation_source,\n    text_font_size=\"30pt\",\n    text_font_style=\"italic\",\n    text_align=\"center\",\n)\np.add_layout(labels)\n\n# Styling\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"normal\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\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 = None\np.yaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\np.x_range.range_padding = 0.12\n\n# Grid\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_width = 1\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — window is H+200 tall so bokeh canvas fills\n# exactly W×H; PIL crops to the target rect before saving.\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.set_window_size(W, H + 200)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nraw = driver.get_screenshot_as_png()\ndriver.quit()\nImage.open(io.BytesIO(raw)).crop((0, 0, W, H)).save(f\"plot-{THEME}.png\")\n"}