{"spec_id":"stock-event-flags","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nstock-event-flags: Stock Chart with Event Flags\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-27\n\"\"\"\n\nimport base64\nimport os\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem\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_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # price line uses position 1\n\n# Data — 180 trading days of synthetic stock prices via geometric Brownian motion\nnp.random.seed(42)\nn_days = 180\ndates = pd.bdate_range(start=pd.Timestamp(\"2024-01-02\"), periods=n_days)\n\ninitial_price = 150.0\ndaily_returns = np.random.normal(0.0005, 0.018, n_days)\nclose_prices = initial_price * np.cumprod(1 + daily_returns)\n\ndf = pd.DataFrame({\"date\": dates, \"close\": close_prices})\n\nevents = [\n    {\"date\": dates[25], \"type\": \"earnings\", \"label\": \"Q4 Earnings\"},\n    {\"date\": dates[50], \"type\": \"dividend\", \"label\": \"Dividend $0.50\"},\n    {\"date\": dates[75], \"type\": \"news\", \"label\": \"Product Launch\"},\n    {\"date\": dates[95], \"type\": \"earnings\", \"label\": \"Q1 Earnings\"},\n    {\"date\": dates[110], \"type\": \"split\", \"label\": \"2:1 Split\"},\n    {\"date\": dates[140], \"type\": \"dividend\", \"label\": \"Dividend $0.55\"},\n    {\"date\": dates[160], \"type\": \"news\", \"label\": \"Partnership\"},\n]\nevents_df = pd.DataFrame(events)\n\n# Event styling — positions 2–5 (position 1 is the price line)\nevent_colors = {\n    \"earnings\": IMPRINT_PALETTE[1],  # lavender\n    \"dividend\": IMPRINT_PALETTE[2],  # blue\n    \"split\": IMPRINT_PALETTE[3],  # ochre\n    \"news\": IMPRINT_PALETTE[4],  # matte red\n}\nevent_markers = {\"earnings\": \"triangle\", \"dividend\": \"circle\", \"split\": \"square\", \"news\": \"diamond\"}\n\n# Plot\ntitle_str = \"stock-event-flags · python · bokeh · anyplot.ai\"\n\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"datetime\",\n    title=title_str,\n    x_axis_label=\"Date\",\n    y_axis_label=\"Price (USD)\",\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)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None  # L-shaped frame: only left/bottom axis lines visible\n\np.title.text_color = INK\np.title.text_font_size = \"50pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\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.15\np.ygrid.grid_line_alpha = 0.15\n\n# Price line\nsource = ColumnDataSource(df)\nprice_line = p.line(x=\"date\", y=\"close\", source=source, line_width=4, line_color=BRAND, alpha=0.9)\n\n# Event flags — vertical connector lines + markers + labels\nlegend_renderers = {}\nall_event_renderers = []\nprice_range = close_prices.max() - close_prices.min()\n\n# Major events (earnings, split) get slightly larger offsets for visual hierarchy\nmajor_events = {\"earnings\", \"split\"}\n\nfor i, event in events_df.iterrows():\n    event_date = event[\"date\"]\n    event_type = event[\"type\"]\n    event_label = event[\"label\"]\n    color = event_colors[event_type]\n    marker = event_markers[event_type]\n\n    idx = df[df[\"date\"] == event_date].index[0]\n    event_price = df.loc[idx, \"close\"]\n\n    offset_dir = 1 if i % 2 == 0 else -1\n    # Major events flagged higher to create visual hierarchy\n    offset_factor = 0.20 if event_type in major_events else 0.13\n    flag_y = event_price + offset_dir * price_range * offset_factor\n\n    p.segment(\n        x0=[event_date],\n        y0=[event_price],\n        x1=[event_date],\n        y1=[flag_y],\n        line_color=color,\n        line_dash=\"dashed\",\n        line_width=3,\n        alpha=0.7,\n    )\n\n    flag_source = ColumnDataSource(\n        data={\n            \"x\": [event_date],\n            \"y\": [flag_y],\n            \"event_type\": [event_type.capitalize()],\n            \"event_label\": [event_label],\n            \"price\": [f\"${event_price:.2f}\"],\n        }\n    )\n    marker_size = 28 if event_type in major_events else 22\n    renderer = p.scatter(\n        x=\"x\",\n        y=\"y\",\n        source=flag_source,\n        size=marker_size,\n        color=color,\n        alpha=0.9,\n        line_color=PAGE_BG,\n        line_width=2,\n        marker=marker,\n    )\n\n    all_event_renderers.append(renderer)\n\n    if event_type not in legend_renderers:\n        legend_renderers[event_type] = renderer\n\n    label = Label(\n        x=event_date,\n        y=flag_y,\n        text=event_label,\n        text_font_size=\"26pt\",\n        text_color=color,\n        x_offset=22,\n        y_offset=8 if offset_dir > 0 else -40,\n        text_font_style=\"bold\",\n    )\n    p.add_layout(label)\n\n# HoverTool on event markers — reveals full event details in the HTML artifact\nhover = HoverTool(\n    renderers=all_event_renderers, tooltips=[(\"Type\", \"@event_type\"), (\"Event\", \"@event_label\"), (\"Price\", \"@price\")]\n)\np.add_tools(hover)\n\n# Legend\nlegend_items = [LegendItem(label=\"Close Price\", renderers=[price_line])]\nfor event_type, renderer in legend_renderers.items():\n    legend_items.append(LegendItem(label=event_type.capitalize(), renderers=[renderer]))\n\nlegend = Legend(\n    items=legend_items,\n    location=\"top_left\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n)\np.add_layout(legend)\n\n# Save HTML artifact\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot with headless Chrome (Selenium 4 / Selenium Manager)\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)\n# Force exact viewport via CDP to override any OS/browser chrome overhead\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)\nscreenshot_b64 = driver.execute_cdp_cmd(\n    \"Page.captureScreenshot\", {\"format\": \"png\", \"fromSurface\": True, \"captureBeyondViewport\": False}\n)[\"data\"]\nwith open(f\"plot-{THEME}.png\", \"wb\") as f:\n    f.write(base64.b64decode(screenshot_b64))\ndriver.quit()\n"}