{"spec_id":"line-multi","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nline-multi: Multi-Line Comparison Plot\nLibrary: bokeh 3.9.2 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-08-05\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, HoverTool, Label\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (see prompts/default-style-guide.md \"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\"\n\n# Imprint palette (first series is always #009E73)\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data: monthly sales (thousands $) for 4 product lines over 12 months\nnp.random.seed(42)\nmonths = np.arange(1, 13)\nmonth_labels = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\nelectronics = 45 + np.cumsum(np.random.randn(12) * 3) + months * 2\nclothing = 35 + np.cumsum(np.random.randn(12) * 2.5) + np.sin(months * np.pi / 6) * 8\nfurniture = 25 + np.cumsum(np.random.randn(12) * 2) + months * 0.5\ngroceries = 55 + np.cumsum(np.random.randn(12) * 1.5)\n\nseries_names = [\"Electronics\", \"Clothing\", \"Furniture\", \"Groceries\"]\nseries_data = [electronics, clothing, furniture, groceries]\nline_dashes = [\"solid\", \"solid\", \"dashed\", \"dotted\"]\n\n# Electronics closes the year as the strongest performer — give it visual\n# emphasis (full opacity, thicker line) so the eye has a focal point; the\n# other series recede slightly but stay fully legible.\nfocal_series = \"Electronics\"\n\n# Figure — 3200x1800 landscape canvas (see prompts/library/bokeh.md \"Canvas — hard rule\")\ntitle = \"line-multi · python · bokeh · anyplot.ai\"\np = figure(\n    width=3200,\n    height=1800,\n    title=title,\n    x_axis_label=\"Month\",\n    y_axis_label=\"Sales (thousands $)\",\n    toolbar_location=None,  # avoids the ~30-50px toolbar padding that shrinks the PNG\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\nfor name, data, color, dash in zip(series_names, series_data, IMPRINT_PALETTE, line_dashes, strict=True):\n    source = ColumnDataSource(data={\"x\": months, \"y\": data, \"month\": month_labels})\n    is_focal = name == focal_series\n\n    p.line(\n        x=\"x\",\n        y=\"y\",\n        source=source,\n        line_width=4.5 if is_focal else 3,\n        line_color=color,\n        line_dash=dash,\n        line_alpha=1.0 if is_focal else 0.75,\n        legend_label=name,\n    )\n    scatter = p.scatter(\n        x=\"x\", y=\"y\", source=source, size=20 if is_focal else 15, color=color, alpha=1.0 if is_focal else 0.75\n    )\n\n    hover = HoverTool(renderers=[scatter], tooltips=[(\"Month\", \"@month\"), (\"Sales\", \"$@y{0,0.0}k\")])\n    p.add_tools(hover)\n\n# End-of-line value annotation on the focal series — reinforces the \"strongest\n# performer\" story beyond what the line/marker emphasis alone conveys.\nfocal_index = series_names.index(focal_series)\nfocal_end_x, focal_end_y = months[-1], series_data[focal_index][-1]\nend_label = Label(\n    x=focal_end_x,\n    y=focal_end_y,\n    text=f\"${focal_end_y:.0f}k\",\n    text_font_size=\"30pt\",\n    text_font_style=\"bold\",\n    text_color=IMPRINT_PALETTE[focal_index],\n    text_align=\"right\",\n    text_baseline=\"bottom\",\n    x_offset=-8,\n    y_offset=-16,\n)\np.add_layout(end_label)\n\n# Legend — inside the plot frame, click a label to hide that series\nif p.legend:\n    p.legend.location = \"top_left\"\n    p.legend.click_policy = \"hide\"\n    p.legend.label_text_font_size = \"34pt\"\n    p.legend.glyph_height = 36\n    p.legend.glyph_width = 36\n    p.legend.spacing = 14\n    p.legend.padding = 24\n    p.legend.background_fill_color = ELEVATED_BG\n    p.legend.background_fill_alpha = 0.9\n    p.legend.border_line_color = INK_SOFT\n    p.legend.label_text_color = INK_SOFT\n\n# Title styling\np.title.text_font_size = \"50pt\"\np.title.align = \"center\"\np.title.text_color = INK\n\n# Axis label styling\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\n\n# Axis and grid styling\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.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\n# Background — no outer frame, for a cleaner, more minimal look\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Generous breathing room around the data\np.y_range.range_padding = 0.12\np.x_range.range_padding = 0.04\n\n# Month tick labels\np.xaxis.ticker = list(range(1, 13))\np.xaxis.major_label_overrides = dict(zip(range(1, 13), month_labels, strict=True))\n\n# Save HTML\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — Selenium 4 auto-resolves a working driver.\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# Headless Chrome's --window-size sets the OUTER window, which still reserves\n# a phantom title-bar height even headless — pin the viewport exactly via CDP.\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"}