{"spec_id":"line-stock-comparison","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nline-stock-comparison: Stock Price Comparison Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-23\n\"\"\"\n\nimport base64\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove the current directory from sys.path to avoid circular imports with bokeh.py\nsys.path = [p for p in sys.path if p not in (\"\", \".\", os.getcwd(), os.path.dirname(__file__))]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import Band, ColumnDataSource, HoverTool, Label, Legend, Range1d, Span\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\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#AE3030\", \"#4467A3\"]\n\n# Data — synthetic stock price paths via geometric Brownian motion\nn_days = 252\ndates = pd.date_range(\"2024-01-02\", periods=n_days, freq=\"B\")\n\nstocks = {\n    \"AAPL\": {\"drift\": 0.0006, \"volatility\": 0.018, \"seed\": 42},\n    \"GOOGL\": {\"drift\": 0.0005, \"volatility\": 0.020, \"seed\": 43},\n    \"MSFT\": {\"drift\": 0.0002, \"volatility\": 0.016, \"seed\": 44},\n    \"SPY\": {\"drift\": 0.0003, \"volatility\": 0.009, \"seed\": 45},\n}\n\nprice_data = {\"date\": dates}\nfor symbol, params in stocks.items():\n    rng = np.random.RandomState(params[\"seed\"])\n    returns = rng.normal(params[\"drift\"], params[\"volatility\"], n_days)\n    prices = 100 * np.exp(np.cumsum(returns))\n    prices = prices / prices[0] * 100  # Rebase to exactly 100 at start\n    price_data[symbol] = prices\n\ndf = pd.DataFrame(price_data)\n\n# Determine visual hierarchy by final performance\nfinal_vals = {symbol: df[symbol].iloc[-1] for symbol in stocks}\nranked = sorted(final_vals, key=lambda s: final_vals[s])\nbest, worst = ranked[-1], ranked[0]\n\n# Extend x-axis range to accommodate end-of-series labels\nDAY_MS = 24 * 60 * 60 * 1000\nstart_ms = int((dates[0] - pd.Timedelta(days=5)).timestamp() * 1000)\nend_ms = int((dates[-1] + pd.Timedelta(days=60)).timestamp() * 1000)\n\n# Figure — 3200×1800 with toolbar disabled for correct PNG dimensions\np = figure(\n    width=3200,\n    height=1800,\n    title=\"line-stock-comparison · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Date\",\n    y_axis_label=\"Rebased Price (Start = 100)\",\n    x_axis_type=\"datetime\",\n    x_range=Range1d(start=start_ms, end=end_ms),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=80,\n)\n\n# Font sizes — bokeh CSS pt sizing (~1.333 source-px per pt)\np.title.text_font_size = \"50pt\"\np.title.text_font_style = \"bold\"\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\"\n\n# Subtle ±15% performance band — visually anchors the \"normal\" return envelope\nband_source = ColumnDataSource(data={\"x\": df[\"date\"], \"lower\": np.full(n_days, 85.0), \"upper\": np.full(n_days, 115.0)})\nperf_band = Band(\n    base=\"x\", lower=\"lower\", upper=\"upper\", source=band_source, fill_color=INK, fill_alpha=0.04, line_color=None\n)\np.add_layout(perf_band)\n\n# Reference line at 100 (starting point indicator)\nhline = Span(location=100, dimension=\"width\", line_color=INK_SOFT, line_dash=\"dashed\", line_width=3)\np.add_layout(hline)\n\n# Smart label y-position assignment — spread overlapping end-of-series labels\nsorted_syms = sorted(stocks.keys(), key=lambda s: final_vals[s])\nmin_gap = 8  # minimum vertical gap in data units\nlabel_ys: dict[str, float] = {}\nprev_y = -999.0\nfor sym in sorted_syms:\n    y = max(final_vals[sym], prev_y + min_gap)\n    label_ys[sym] = y\n    prev_y = y\n\n# Plot each stock series with performance-based line widths for visual hierarchy\nlegend_items = []\nlabel_ms_offset = 8 * DAY_MS\nfor i, symbol in enumerate(stocks):\n    lw = 7 if symbol in (best, worst) else 4\n    line = p.line(x=df[\"date\"], y=df[symbol], line_width=lw, line_color=IMPRINT[i], alpha=0.9)\n    legend_items.append((symbol, [line]))\n\n    # End-of-series label showing symbol and final value with vertical alignment fix\n    final_date_ms = int(df[\"date\"].iloc[-1].timestamp() * 1000)\n    p.add_layout(\n        Label(\n            x=final_date_ms + label_ms_offset,\n            y=label_ys[symbol],\n            text=f\"{symbol} {final_vals[symbol]:.0f}\",\n            text_color=IMPRINT[i],\n            text_font_size=\"28pt\",\n            text_font_style=\"bold\",\n            text_baseline=\"middle\",\n            text_align=\"left\",\n        )\n    )\n\n# Hover tool — active in HTML artifact\nhover = HoverTool(tooltips=[(\"Date\", \"@x{%F}\"), (\"Value\", \"@y{0.1f}\")], formatters={\"@x\": \"datetime\"}, mode=\"vline\")\np.add_tools(hover)\n\n# Legend\nlegend = Legend(\n    items=legend_items,\n    location=\"top_left\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=60,\n    glyph_height=30,\n    spacing=15,\n    padding=20,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    click_policy=\"hide\",\n)\np.add_layout(legend)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None  # Remove box; L-shaped frame via axis lines only\n\np.title.text_color = INK\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\n\n# Y-axis grid only (appropriate for line charts)\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.10\np.xgrid.grid_line_color = None\n\n# Save interactive HTML artifact\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome — use CDP clip to capture exactly W×H px\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 + 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)\nscreenshot = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\",\n    {\"format\": \"png\", \"clip\": {\"x\": 0, \"y\": 0, \"width\": W, \"height\": H, \"scale\": 1}, \"captureBeyondViewport\": True},\n)\nwith open(f\"plot-{THEME}.png\", \"wb\") as f:\n    f.write(base64.b64decode(screenshot[\"data\"]))\ndriver.quit()\n"}