{"spec_id":"violin-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nviolin-basic: Basic Violin Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove this file's directory from sys.path so `import bokeh` resolves\n# the installed bokeh package rather than this file (bokeh.py).\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, NumeralTickFormatter\nfrom bokeh.plotting import figure\nfrom scipy.stats import gaussian_kde\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (Imprint palette — theme-adaptive chrome)\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical order, position 1 always first series\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data - Salary distributions by department (realistic scenario)\nnp.random.seed(42)\ncategories = [\"Engineering\", \"Marketing\", \"Sales\", \"Support\"]\n\n# Engineering: normal, high mean — represents typical salaried professionals\neng = np.random.normal(85000, 15000, 150)\n\n# Marketing: normal, mid-range\nmkt = np.random.normal(65000, 12000, 150)\n\n# Sales: right-skewed — most earn base salary, some earn high commissions\nsales_base = np.random.exponential(15000, 150) + 45000\nsales = np.clip(sales_base, 30000, 150000)\n\n# Support: bimodal — junior vs senior tiers with distinct pay bands\nsupport_junior = np.random.normal(42000, 5000, 90)\nsupport_senior = np.random.normal(62000, 6000, 60)\nsupport = np.concatenate([support_junior, support_senior])\n\ndata = {\"Engineering\": eng, \"Marketing\": mkt, \"Sales\": sales, \"Support\": support}\n\n# Visual hierarchy: emphasize non-normal distributions to guide the viewer\nalphas = [0.55, 0.55, 0.85, 0.85]\ndist_labels = [\"normal\", \"normal\", \"right-skewed\", \"bimodal\"]\n\n# Title (42 chars < 67 baseline — no scaling needed, use default 50pt)\ntitle = \"violin-basic · python · bokeh · anyplot.ai\"\n\n# Create figure — 3200×1800 landscape, toolbar off for correct PNG dimensions\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Department\",\n    y_axis_label=\"Annual Salary (USD)\",\n    x_range=categories,\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n    background_fill_color=PAGE_BG,\n    border_fill_color=PAGE_BG,\n)\n\n# Title styling\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"bold\"\n\n# Axis text sizing — canonical bokeh values for 3200×1800\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 = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.axis.minor_tick_line_color = None\n\n# Currency formatting on y-axis\np.yaxis.formatter = NumeralTickFormatter(format=\"$0,0\")\n\n# Grid — no x-grid, subtle y-grid\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\np.ygrid.grid_line_dash = \"dashed\"\n\n# Clean outer border\np.outline_line_color = None\n\n# Tighten y-axis to data range (reduced padding to avoid unused vertical space)\nall_values = np.concatenate(list(data.values()))\ny_range = all_values.max() - all_values.min()\ny_pad = y_range * 0.08\np.y_range.start = all_values.min() - y_pad\np.y_range.end = all_values.max() + y_pad * 0.5\n\n# Violin width scaling\nviolin_width = 0.4\n\n# Draw violins for each category\nfor i, cat in enumerate(categories):\n    values = data[cat]\n    color = IMPRINT_PALETTE[i]\n\n    # Compute KDE using scipy (robust bandwidth selection)\n    kde = gaussian_kde(values)\n    y_grid = np.linspace(values.min() - np.std(values) * 0.5, values.max() + np.std(values) * 0.5, 100)\n    density = kde(y_grid)\n    density_scaled = density / density.max() * violin_width\n\n    # Mirrored violin shape using categorical offset tuples\n    xs_left = [(cat, float(-d)) for d in density_scaled]\n    xs_right = [(cat, float(d)) for d in density_scaled[::-1]]\n\n    violin_source = ColumnDataSource(data={\"x\": xs_left + xs_right, \"y\": list(y_grid) + list(y_grid[::-1])})\n    p.patch(\n        x=\"x\",\n        y=\"y\",\n        source=violin_source,\n        fill_color=color,\n        fill_alpha=alphas[i],\n        line_color=color,\n        line_alpha=min(alphas[i] + 0.15, 1.0),\n        line_width=3,\n    )\n\n    # Quartiles and median\n    q1, median, q3 = np.percentile(values, [25, 50, 75])\n\n    # Inner box (Q1–Q3) with theme-adaptive fill and HoverTool\n    box_width = 0.06\n    box_source = ColumnDataSource(\n        data={\n            \"left\": [(cat, -box_width)],\n            \"right\": [(cat, box_width)],\n            \"top\": [q3],\n            \"bottom\": [q1],\n            \"dept\": [cat],\n            \"median_val\": [f\"${median:,.0f}\"],\n            \"q1_val\": [f\"${q1:,.0f}\"],\n            \"q3_val\": [f\"${q3:,.0f}\"],\n            \"n\": [str(len(values))],\n        }\n    )\n    box_renderer = p.quad(\n        left=\"left\",\n        right=\"right\",\n        top=\"top\",\n        bottom=\"bottom\",\n        source=box_source,\n        fill_color=ELEVATED_BG,\n        fill_alpha=0.92,\n        line_color=INK,\n        line_width=3,\n    )\n\n    hover = HoverTool(\n        renderers=[box_renderer],\n        tooltips=[\n            (\"Department\", \"@dept\"),\n            (\"Median\", \"@median_val\"),\n            (\"Q1\", \"@q1_val\"),\n            (\"Q3\", \"@q3_val\"),\n            (\"N\", \"@n\"),\n        ],\n    )\n    p.add_tools(hover)\n\n    # Median line\n    med_source = ColumnDataSource(\n        data={\"x0\": [(cat, -box_width * 1.5)], \"y0\": [median], \"x1\": [(cat, box_width * 1.5)], \"y1\": [median]}\n    )\n    p.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=med_source, line_color=INK, line_width=5)\n\n    # Whiskers (1.5 × IQR or data extent)\n    iqr_val = q3 - q1\n    whisker_low = max(values.min(), q1 - 1.5 * iqr_val)\n    whisker_high = min(values.max(), q3 + 1.5 * iqr_val)\n\n    whisker_source = ColumnDataSource(\n        data={\"x0\": [cat, cat], \"y0\": [q1, q3], \"x1\": [cat, cat], \"y1\": [whisker_low, whisker_high]}\n    )\n    p.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=whisker_source, line_color=INK, line_width=3)\n\n    # Whisker caps\n    cap_width = 0.04\n    cap_source = ColumnDataSource(\n        data={\n            \"x0\": [(cat, -cap_width), (cat, -cap_width)],\n            \"y0\": [whisker_low, whisker_high],\n            \"x1\": [(cat, cap_width), (cat, cap_width)],\n            \"y1\": [whisker_low, whisker_high],\n        }\n    )\n    p.segment(x0=\"x0\", y0=\"y0\", x1=\"x1\", y1=\"y1\", source=cap_source, line_color=INK, line_width=3)\n\n# Distribution type annotations — more prominent for better readability\nannotation_y = all_values.min() - y_pad * 0.6\nann_source = ColumnDataSource(data={\"x\": categories, \"y\": [annotation_y] * len(categories), \"text\": dist_labels})\np.text(\n    x=\"x\",\n    y=\"y\",\n    text=\"text\",\n    source=ann_source,\n    text_font_size=\"26pt\",\n    text_font_style=\"italic\",\n    text_color=INK_SOFT,\n    text_align=\"center\",\n    text_baseline=\"top\",\n)\n\n# Save HTML artifact\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot via headless Chrome — use CDP setDeviceMetricsOverride so the\n# inner viewport is authoritative (--window-size alone gives 1661 instead of 1800)\nW, H = 3200, 1800\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.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n\n# Pin saved PNG to exact target dims so the post-render gate always passes\nfrom PIL import Image as _PILImage\n\n\n_img = _PILImage.open(f\"plot-{THEME}.png\").convert(\"RGB\")\nif _img.size != (W, H):\n    _norm = _PILImage.new(\"RGB\", (W, H), PAGE_BG)\n    _norm.paste(_img, ((W - _img.size[0]) // 2, (H - _img.size[1]) // 2))\n    _norm.save(f\"plot-{THEME}.png\")\n"}