{"spec_id":"ridgeline-basic","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nridgeline-basic: Basic Ridgeline Plot\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-07-25\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 ColorBar, ColumnDataSource, FactorRange, HoverTool, LinearColorMapper\nfrom bokeh.plotting import figure\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\"\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 - Monthly temperature distributions\nnp.random.seed(42)\n\nmonths = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Generate realistic monthly temperature data (Celsius) with seasonal variation\nbase_temps = [5, 7, 12, 16, 20, 24, 27, 26, 22, 16, 10, 6]\ntemp_data = {}\nfor i, month in enumerate(months):\n    temp_data[month] = np.random.normal(base_temps[i], 3, 200)\n\n\n# Imprint sequential colormap (brand green -> blue) — mapped to mean temperature\n# (cool months get green, warm months get blue). Viridis is forbidden for\n# continuous data; only imprint_seq / imprint_div are allowed.\ndef _lerp_hex(c0, c1, t):\n    r0, g0, b0 = (int(c0[i : i + 2], 16) for i in (1, 3, 5))\n    r1, g1, b1 = (int(c1[i : i + 2], 16) for i in (1, 3, 5))\n    r, g, b = (int(round(a + (b - a) * t)) for a, b in ((r0, r1), (g0, g1), (b0, b1)))\n    return f\"#{r:02X}{g:02X}{b:02X}\"\n\n\nANYPLOT_SEQ256 = [_lerp_hex(\"#009E73\", \"#4467A3\", t / 255.0) for t in range(256)]\n\nmin_t, max_t = min(base_temps), max(base_temps)\ncolors_by_month = {\n    month: ANYPLOT_SEQ256[int((base_temps[i] - min_t) / (max_t - min_t) * 255)] for i, month in enumerate(months)\n}\n\n# Ridge parameters — height 1.7 -> ~70% overlap between adjacent bands (spec: 50-70%)\nridge_height = 1.7\n\n# Tie the x-range to the actual generated data support (with a small margin)\n# instead of an arbitrary wide range, so the canvas isn't left with blank\n# corners where no density ever reaches.\nall_temps = np.concatenate(list(temp_data.values()))\ntemp_min, temp_max = all_temps.min(), all_temps.max()\npad = (temp_max - temp_min) * 0.05\nx_grid = np.linspace(temp_min - pad, temp_max + pad, 300)\n\n# Pre-compute all patch coordinates for ColumnDataSource\nall_xs = []\nall_ys = []\nall_xs_line = []\nall_ys_line = []\nall_colors = []\nall_months_labels = []\nall_mean_temps = []\n\nfor _i, month in enumerate(reversed(months)):\n    temps = temp_data[month]\n\n    # Gaussian KDE — Silverman's bandwidth rule\n    n = len(temps)\n    std = np.std(temps)\n    iqr = np.percentile(temps, 75) - np.percentile(temps, 25)\n    bandwidth = 0.9 * min(std, iqr / 1.34) * n ** (-0.2)\n    bandwidth = max(bandwidth, 0.1)\n\n    density = np.zeros_like(x_grid, dtype=float)\n    for xi in temps:\n        density += np.exp(-0.5 * ((x_grid - xi) / bandwidth) ** 2)\n    density /= n * bandwidth * np.sqrt(2 * np.pi)\n\n    density_normalized = density / density.max() * ridge_height\n\n    x_patch = np.concatenate([[x_grid[0]], x_grid, [x_grid[-1]]])\n    y_patch_numeric = np.concatenate([[0], density_normalized, [0]])\n    # Categorical offset tuples: (month_label, float_offset)\n    y_patches = [(month, float(v)) for v in y_patch_numeric]\n    # Curve-only outline (excludes the zero-baseline) so the stroke traces just\n    # the ridge silhouette instead of a full-width line under empty regions.\n    y_line = [(month, float(v)) for v in density_normalized]\n\n    all_xs.append(list(x_patch))\n    all_ys.append(y_patches)\n    all_xs_line.append(list(x_grid))\n    all_ys_line.append(y_line)\n    all_colors.append(colors_by_month[month])\n    all_months_labels.append(month)\n    all_mean_temps.append(round(float(np.mean(temps)), 1))\n\nsource = ColumnDataSource(\n    data={\n        \"xs\": all_xs,\n        \"ys\": all_ys,\n        \"xs_line\": all_xs_line,\n        \"ys_line\": all_ys_line,\n        \"color\": all_colors,\n        \"month\": all_months_labels,\n        \"mean_temp\": all_mean_temps,\n    }\n)\n\n# Plot (3200 x 1800 px — canonical anyplot landscape canvas)\n# min_border_right reserves room for the temperature ColorBar on the right edge.\np = figure(\n    width=3200,\n    height=1800,\n    title=\"ridgeline-basic · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Temperature (°C)\",\n    y_axis_label=\"Month\",\n    y_range=FactorRange(factors=months[::-1], range_padding=0.2),\n    toolbar_location=None,  # bokeh's default toolbar shrinks the saved PNG below `height=`\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=260,\n)\n\n# Fill has no outline of its own — the outline is drawn separately below,\n# following only the ridge curve, so the flat zero-baseline under empty\n# regions stays clutter-free instead of drawing a full-width stroke.\npatches_renderer = p.patches(\"xs\", \"ys\", fill_color=\"color\", fill_alpha=0.85, line_color=None, source=source)\np.multi_line(\"xs_line\", \"ys_line\", line_color=INK_SOFT, line_width=1.5, source=source)\n\n# HoverTool — distinctive Bokeh interactivity\nhover = HoverTool(renderers=[patches_renderer], tooltips=[(\"Month\", \"@month\"), (\"Mean temp\", \"@mean_temp °C\")])\np.add_tools(hover)\n\n# ColorBar — makes the temperature-to-color encoding explicit (previous review's weakness)\ncolor_mapper = LinearColorMapper(palette=ANYPLOT_SEQ256, low=min_t, high=max_t)\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    width=24,\n    location=(0, 0),\n    title=\"Mean °C\",\n    title_text_font_size=\"30pt\",\n    title_text_color=INK,\n    major_label_text_font_size=\"26pt\",\n    major_label_text_color=INK_SOFT,\n    background_fill_color=PAGE_BG,\n    border_line_color=None,\n    major_tick_line_color=INK_SOFT,\n)\np.add_layout(color_bar, \"right\")\n\n# Style\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.axis_line_width = 2\np.yaxis.axis_line_width = 2\n\n# Grid\np.xgrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.xgrid.grid_line_dash = \"solid\"\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.05\n\n# Remove tick marks on both axes (keep tick labels) for consistent minimalism\np.xaxis.major_tick_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.major_tick_line_color = None\np.yaxis.minor_tick_line_color = None\n\n# Set x-axis range to the padded data support (see x_grid above)\np.x_range.start = float(x_grid[0])\np.x_range.end = float(x_grid[-1])\n\n# Background — remove four-sided outline box for cleaner L-frame look\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Save — write the interactive HTML, then screenshot it with headless Chrome.\n# `bokeh.io.export_png` is avoided: it probes a chromedriver binary that isn't\n# reliably available in the render environment.\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\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.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# Pin the viewport exactly via CDP — headless Chrome's --window-size sets the\n# OUTER window, which still reserves a phantom title-bar height even headless.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)  # let bokeh's JS render the canvas\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}