{"spec_id":"boxen-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nboxen-basic: Basic Boxen Plot (Letter-Value Plot)\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 96/100 | Updated: 2026-05-17\n\"\"\"\n\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, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme configuration\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# Data - Server response times by endpoint (large datasets ideal for boxen)\nnp.random.seed(42)\n\n# Generate 5000 points per category with different distributions\ncategories = [\"API Auth\", \"API Users\", \"API Orders\", \"API Search\"]\ndata = {\n    \"API Auth\": np.concatenate(\n        [np.random.exponential(50, 4000) + 20, np.random.normal(200, 30, 800), np.random.uniform(400, 600, 200)]\n    ),\n    \"API Users\": np.concatenate([np.random.normal(80, 25, 4500), np.random.uniform(180, 300, 500)]),\n    \"API Orders\": np.concatenate([np.random.lognormal(4, 0.5, 4800), np.random.uniform(300, 500, 200)]),\n    \"API Search\": np.concatenate([np.random.gamma(3, 30, 4600), np.random.uniform(350, 550, 400)]),\n}\n\n# Create figure\np = figure(\n    width=4800,\n    height=2700,\n    title=\"boxen-basic · bokeh · anyplot.ai\",\n    x_axis_label=\"API Endpoint\",\n    y_axis_label=\"Response Time (ms)\",\n    x_range=categories,\n)\n\n# Colors for quantile levels (gradient from light to dark)\ncolors = [\n    \"#a8d4f0\",  # Lightest - 64ths\n    \"#7bbce0\",  # 32nds\n    \"#4da4d0\",  # Sixteenths\n    \"#306998\",  # Eighths\n    \"#1e4d6b\",  # Fourths (25-75%)\n    \"#0d3048\",  # Median (50%)\n]\n\n# Width factors for nested boxes (wider outer, narrower inner)\nwidth_factors = [0.75, 0.65, 0.55, 0.45, 0.35, 0.25]\n\n# Legend items in reverse order (light to dark for clear visual hierarchy)\nlegend_items = []\n\n# Plot boxen for each category\nfor cat_idx, category in enumerate(categories):\n    values = data[category]\n\n    # Compute letter-value quantiles inline: median, fourths, eighths, sixteenths, 32nds, 64ths\n    letter_values = []\n    for i in range(6):\n        q_low = 0.5 ** (i + 1)\n        q_high = 1 - q_low\n        lower = np.percentile(values, q_low * 100)\n        upper = np.percentile(values, q_high * 100)\n        letter_values.append((lower, upper, i))\n\n    # Plot from outer to inner (deepest quantile first, so inner boxes are on top)\n    for level_idx in range(len(letter_values) - 1, -1, -1):\n        lower, upper, _ = letter_values[level_idx]\n        width = width_factors[level_idx]\n        color = colors[level_idx]\n\n        # Create box as a quad\n        source = ColumnDataSource(\n            data={\"left\": [cat_idx - width / 2], \"right\": [cat_idx + width / 2], \"bottom\": [lower], \"top\": [upper]}\n        )\n\n        renderer = p.quad(\n            left=\"left\",\n            right=\"right\",\n            bottom=\"bottom\",\n            top=\"top\",\n            source=source,\n            fill_color=color,\n            line_color=INK_SOFT,\n            line_width=2,\n            fill_alpha=0.95,\n        )\n\n        # Add legend item only once per level (first category)\n        if cat_idx == 0:\n            level_names = [\"64ths\", \"32nds\", \"Sixteenths\", \"Eighths\", \"Fourths (25-75%)\", \"Median (50%)\"]\n            legend_items.append(LegendItem(label=level_names[level_idx], renderers=[renderer]))\n\n    # Add median line\n    median = np.median(values)\n    median_width = width_factors[0]\n    p.line(\n        x=[cat_idx - median_width / 2, cat_idx + median_width / 2],\n        y=[median, median],\n        line_color=\"#009E73\",\n        line_width=5,\n    )\n\n    # Add outliers (beyond 64th percentile level)\n    deepest_lower, deepest_upper, _ = letter_values[-1]\n    outliers = values[(values < deepest_lower) | (values > deepest_upper)]\n    if len(outliers) > 0:\n        # Jitter x positions for visibility\n        jitter = np.random.uniform(-0.12, 0.12, len(outliers))\n        outlier_source = ColumnDataSource(data={\"x\": [cat_idx + j for j in jitter], \"y\": outliers})\n        p.scatter(\n            x=\"x\",\n            y=\"y\",\n            source=outlier_source,\n            size=12,\n            fill_color=\"#009E73\",\n            line_color=INK_SOFT,\n            line_width=1,\n            alpha=0.7,\n        )\n\n# Add median line to legend first\nmedian_renderer = p.line(x=[], y=[], line_color=\"#009E73\", line_width=5)\nlegend_items.insert(0, LegendItem(label=\"Median Line\", renderers=[median_renderer]))\n\n# Add outlier to legend\noutlier_renderer = p.scatter(x=[], y=[], size=12, fill_color=\"#009E73\", line_color=INK_SOFT)\nlegend_items.append(LegendItem(label=\"Outliers\", renderers=[outlier_renderer]))\n\n# Create and add legend\nlegend = Legend(items=legend_items, location=\"top_right\")\nlegend.label_text_font_size = \"18pt\"\nlegend.label_text_color = INK_SOFT\nlegend.glyph_height = 30\nlegend.glyph_width = 30\nlegend.spacing = 12\nlegend.padding = 20\nlegend.background_fill_color = ELEVATED_BG\nlegend.border_line_color = INK_SOFT\np.add_layout(legend, \"right\")\n\n# Styling\np.title.text_font_size = \"28pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"22pt\"\np.yaxis.axis_label_text_font_size = \"22pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_font_size = \"18pt\"\np.yaxis.major_label_text_font_size = \"18pt\"\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.major_label_orientation = 0.0\n\n# Grid\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\n\n# Background\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome\nW, H = 4800, 2700\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}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}