{"spec_id":"stock-event-flags","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nstock-event-flags: Stock Chart with Event Flags\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-27\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport plotly.graph_objects as go\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Event type colors — Imprint palette positions 0→3 in canonical order\nevent_colors = {\n    \"Earnings\": IMPRINT_PALETTE[0],  # #009E73 brand green\n    \"Dividend\": IMPRINT_PALETTE[1],  # #C475FD lavender\n    \"News\": IMPRINT_PALETTE[2],  # #4467A3 blue\n    \"Split\": IMPRINT_PALETTE[3],  # #BD8233 ochre\n}\nevent_symbols = {\"Earnings\": \"star\", \"Dividend\": \"diamond\", \"News\": \"triangle-up\", \"Split\": \"square\"}\n\n# Data\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(start=\"2024-01-02\", periods=n_days, freq=\"B\")\n\ninitial_price = 150.0\nreturns = np.random.randn(n_days) * 0.02\nclose_prices = initial_price * np.exp(np.cumsum(returns))\nhigh_prices = close_prices * (1 + np.abs(np.random.randn(n_days)) * 0.015)\nlow_prices = close_prices * (1 - np.abs(np.random.randn(n_days)) * 0.015)\nopen_prices = np.roll(close_prices, 1)\nopen_prices[0] = initial_price\n\nhigh_prices = np.maximum(high_prices, np.maximum(open_prices, close_prices))\nlow_prices = np.minimum(low_prices, np.minimum(open_prices, close_prices))\n\n# Convert dates to strings for kaleido/plotly JSON serialization\ndate_strings = dates.strftime(\"%Y-%m-%d\")\n\ndf = pd.DataFrame(\n    {\"date\": date_strings, \"open\": open_prices, \"high\": high_prices, \"low\": low_prices, \"close\": close_prices}\n)\n\nevents = pd.DataFrame(\n    {\n        \"event_date\": [\n            \"2024-01-25\",\n            \"2024-03-15\",\n            \"2024-04-18\",\n            \"2024-05-10\",\n            \"2024-06-07\",\n            \"2024-07-18\",\n            \"2024-08-22\",\n        ],\n        \"event_type\": [\"Earnings\", \"Dividend\", \"News\", \"Earnings\", \"Split\", \"Dividend\", \"Earnings\"],\n        \"event_label\": [\"Q4 Beat\", \"Div $0.50\", \"Product Launch\", \"Q1 Miss\", \"4:1 Split\", \"Div $0.55\", \"Q2 Beat\"],\n    }\n)\n\n# Plot\nfig = go.Figure()\n\n# Candlestick — semantic colors: green=bullish, red=bearish\nfig.add_trace(\n    go.Candlestick(\n        x=df[\"date\"],\n        open=df[\"open\"],\n        high=df[\"high\"],\n        low=df[\"low\"],\n        close=df[\"close\"],\n        name=\"Price\",\n        increasing_line_color=\"#009E73\",\n        decreasing_line_color=\"#AE3030\",\n        increasing_fillcolor=\"#009E73\",\n        decreasing_fillcolor=\"#AE3030\",\n        line_width=1.5,\n        showlegend=False,\n    )\n)\n\n# Event flags with alternating heights to reduce visual clustering\nprice_range = df[\"high\"].max() - df[\"low\"].min()\nflag_height_offsets = [0.07, 0.14, 0.10, 0.17, 0.11, 0.15, 0.08]\n\nfor i, (_, event) in enumerate(events.iterrows()):\n    event_date = event[\"event_date\"]\n    # Find closest trading day (string comparison works for ISO date strings)\n    date_idx = (pd.to_datetime(df[\"date\"]) - pd.to_datetime(event_date)).abs().argmin()\n    actual_date = df[\"date\"].iloc[date_idx]\n    price_at_event = df[\"high\"].iloc[date_idx]\n\n    height_offset = flag_height_offsets[i % len(flag_height_offsets)]\n    flag_y = price_at_event + price_range * height_offset\n\n    color = event_colors.get(event[\"event_type\"], IMPRINT_PALETTE[0])\n    symbol = event_symbols.get(event[\"event_type\"], \"circle\")\n\n    # Vertical dashed connector line\n    fig.add_trace(\n        go.Scatter(\n            x=[actual_date, actual_date],\n            y=[price_at_event, flag_y],\n            mode=\"lines\",\n            line={\"color\": color, \"width\": 1.5, \"dash\": \"dash\"},\n            showlegend=False,\n            hoverinfo=\"skip\",\n        )\n    )\n\n    # Flag marker with label\n    fig.add_trace(\n        go.Scatter(\n            x=[actual_date],\n            y=[flag_y],\n            mode=\"markers+text\",\n            marker={\"size\": 18, \"color\": color, \"symbol\": symbol, \"line\": {\"color\": PAGE_BG, \"width\": 2}},\n            text=[event[\"event_label\"]],\n            textposition=\"top center\",\n            textfont={\"size\": 11, \"color\": color, \"family\": \"Arial Black\"},\n            name=event[\"event_type\"],\n            showlegend=False,\n            hovertemplate=(\n                f\"<b>{event['event_type']}</b><br>{event['event_label']}<br>Date: %{{x|%Y-%m-%d}}<extra></extra>\"\n            ),\n        )\n    )\n\n# Legend entries for event types only — no redundant Price entry\nfor event_type in event_colors:\n    fig.add_trace(\n        go.Scatter(\n            x=[None],\n            y=[None],\n            mode=\"markers\",\n            marker={\"size\": 14, \"color\": event_colors[event_type], \"symbol\": event_symbols[event_type]},\n            name=event_type,\n        )\n    )\n\ntitle = \"stock-event-flags · python · plotly · anyplot.ai\"\n\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    title={\"text\": title, \"font\": {\"size\": 16, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    xaxis={\n        \"title\": {\"text\": \"Date\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"rangeslider\": {\"visible\": False},\n        \"gridcolor\": GRID,\n        \"showgrid\": True,\n        \"showline\": True,\n        \"mirror\": False,\n        \"linecolor\": INK_SOFT,\n        \"zerolinecolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Price ($)\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"tickformat\": \"$.0f\",\n        \"gridcolor\": GRID,\n        \"showgrid\": True,\n        \"showline\": True,\n        \"mirror\": False,\n        \"linecolor\": INK_SOFT,\n        \"zerolinecolor\": INK_SOFT,\n    },\n    legend={\n        \"yanchor\": \"top\",\n        \"y\": 0.99,\n        \"xanchor\": \"right\",\n        \"x\": 0.99,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n    },\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n    hovermode=\"x unified\",\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}