{"spec_id":"stock-event-flags","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nstock-event-flags: Stock Chart with Event Flags\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-27\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the installed altair package\nsys.path[:] = [p for p in sys.path if os.path.abspath(p or \".\") != os.path.dirname(os.path.abspath(__file__))]\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\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\"\nBRAND = \"#009E73\"  # anyplot position 1 — price line\n\n# Event type colors — Imprint palette positions 2–5\nEVENT_COLORS = {\"Earnings\": \"#C475FD\", \"Dividend\": \"#4467A3\", \"News\": \"#BD8233\", \"Split\": \"#AE3030\"}\nEVENT_SHAPES = {\"Earnings\": \"triangle-up\", \"Dividend\": \"diamond\", \"News\": \"circle\", \"Split\": \"square\"}\n\n# Data\nnp.random.seed(42)\ndates = pd.date_range(\"2024-01-01\", periods=180, freq=\"B\")\nrets = np.random.normal(0.0005, 0.018, len(dates))\nprices = 100 * np.exp(np.cumsum(rets))\ndf_price = pd.DataFrame({\"date\": dates, \"close\": prices})\n\nevents = [\n    {\"event_date\": \"2024-01-25\", \"event_type\": \"Earnings\", \"event_label\": \"Q4 Beat\"},\n    {\"event_date\": \"2024-02-15\", \"event_type\": \"Dividend\", \"event_label\": \"$0.25 Div\"},\n    {\"event_date\": \"2024-03-20\", \"event_type\": \"News\", \"event_label\": \"Product Launch\"},\n    {\"event_date\": \"2024-04-24\", \"event_type\": \"Earnings\", \"event_label\": \"Q1 Results\"},\n    {\"event_date\": \"2024-05-10\", \"event_type\": \"Dividend\", \"event_label\": \"$0.28 Div\"},\n    {\"event_date\": \"2024-06-05\", \"event_type\": \"News\", \"event_label\": \"Partnership\"},\n    {\"event_date\": \"2024-07-24\", \"event_type\": \"Earnings\", \"event_label\": \"Q2 Growth\"},\n    {\"event_date\": \"2024-08-20\", \"event_type\": \"Split\", \"event_label\": \"2:1 Split\"},\n]\n\ndf_events = pd.DataFrame(events)\ndf_events[\"event_date\"] = pd.to_datetime(df_events[\"event_date\"])\ndf_events = df_events.merge(\n    df_price.rename(columns={\"date\": \"event_date\", \"close\": \"price_at_event\"}), on=\"event_date\", how=\"left\"\n)\n\nfor idx, row in df_events.iterrows():\n    if pd.isna(row[\"price_at_event\"]):\n        ni = (df_price[\"date\"] - row[\"event_date\"]).abs().idxmin()\n        df_events.loc[idx, \"price_at_event\"] = df_price.loc[ni, \"close\"]\n        df_events.loc[idx, \"event_date\"] = df_price.loc[ni, \"date\"]\n\ny_min, y_max = df_price[\"close\"].min(), df_price[\"close\"].max()\nflag_offset = (y_max - y_min) * 0.18\n\ndf_events[\"above\"] = df_events.index % 2 == 0\ndf_events[\"flag_y\"] = df_events.apply(\n    lambda r: r[\"price_at_event\"] + flag_offset if r[\"above\"] else r[\"price_at_event\"] - flag_offset, axis=1\n)\n\ndf_above = df_events[df_events[\"above\"]].copy()\ndf_below = df_events[~df_events[\"above\"]].copy()\n\n# Connector data — None rows create line breaks in Vega-Lite\nconn_rows = []\nfor _, row in df_events.iterrows():\n    conn_rows.append({\"event_date\": row[\"event_date\"], \"y\": row[\"price_at_event\"]})\n    conn_rows.append({\"event_date\": row[\"event_date\"], \"y\": row[\"flag_y\"]})\n    conn_rows.append({\"event_date\": row[\"event_date\"], \"y\": None})\ndf_conn = pd.DataFrame(conn_rows)\n\n# Shared scales\ncolor_scale = alt.Scale(domain=list(EVENT_COLORS.keys()), range=list(EVENT_COLORS.values()))\nshape_scale = alt.Scale(domain=list(EVENT_SHAPES.keys()), range=list(EVENT_SHAPES.values()))\ny_domain = [y_min - flag_offset * 1.8, y_max + flag_offset * 1.8]\n\n# Chart layers\nprice_line = (\n    alt.Chart(df_price)\n    .mark_line(strokeWidth=2.5, color=BRAND)\n    .encode(\n        x=alt.X(\"date:T\", title=\"Date\", axis=alt.Axis(format=\"%b %Y\", labelAngle=-45)),\n        y=alt.Y(\"close:Q\", title=\"Stock Price ($)\", scale=alt.Scale(domain=y_domain)),\n        tooltip=[alt.Tooltip(\"date:T\", title=\"Date\"), alt.Tooltip(\"close:Q\", title=\"Price\", format=\"$.2f\")],\n    )\n)\n\nconnector_lines = (\n    alt.Chart(df_conn)\n    .mark_line(strokeDash=[4, 4], strokeWidth=1.5, opacity=0.6, color=INK_SOFT)\n    .encode(x=\"event_date:T\", y=\"y:Q\", detail=\"event_date:T\")\n)\n\nflags = (\n    alt.Chart(df_events)\n    .mark_point(size=320, filled=True, strokeWidth=2, stroke=PAGE_BG)\n    .encode(\n        x=\"event_date:T\",\n        y=\"flag_y:Q\",\n        color=alt.Color(\"event_type:N\", scale=color_scale, legend=alt.Legend(title=\"Event Type\")),\n        shape=alt.Shape(\"event_type:N\", scale=shape_scale, legend=None),\n        tooltip=[\n            alt.Tooltip(\"event_date:T\", title=\"Date\"),\n            alt.Tooltip(\"event_type:N\", title=\"Type\"),\n            alt.Tooltip(\"event_label:N\", title=\"Event\"),\n            alt.Tooltip(\"price_at_event:Q\", title=\"Price\", format=\"$.2f\"),\n        ],\n    )\n)\n\nlabels_above = (\n    alt.Chart(df_above)\n    .mark_text(fontSize=11, fontWeight=\"bold\", dy=-18)\n    .encode(\n        x=\"event_date:T\",\n        y=\"flag_y:Q\",\n        text=\"event_label:N\",\n        color=alt.Color(\"event_type:N\", scale=color_scale, legend=None),\n    )\n)\n\nlabels_below = (\n    alt.Chart(df_below)\n    .mark_text(fontSize=11, fontWeight=\"bold\", dy=16)\n    .encode(\n        x=\"event_date:T\",\n        y=\"flag_y:Q\",\n        text=\"event_label:N\",\n        color=alt.Color(\"event_type:N\", scale=color_scale, legend=None),\n    )\n)\n\n# Title with length-scaled fontsize\ntitle_str = \"Tech Stock 2024 · stock-event-flags · python · altair · anyplot.ai\"\nn = len(title_str)\nratio = 67 / n if n > 67 else 1.0\ntitle_fs = max(11, round(16 * ratio))\n\nchart = (\n    (price_line + connector_lines + flags + labels_above + labels_below)\n    .properties(\n        width=620, height=320, background=PAGE_BG, title=alt.Title(title_str, fontSize=title_fs, anchor=\"middle\")\n    )\n    .configure_view(fill=PAGE_BG, stroke=INK_SOFT)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        gridColor=INK,\n        gridOpacity=0.15,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n    )\n    .configure_title(color=INK)\n    .configure_legend(\n        fillColor=ELEVATED_BG,\n        strokeColor=INK_SOFT,\n        labelFontSize=10,\n        titleFontSize=10,\n        labelColor=INK_SOFT,\n        titleColor=INK,\n        symbolSize=150,\n    )\n    .interactive()\n)\n\n# Save PNG with PIL padding to exactly 3200×1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\nTW, TH = 3200, 1800\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}x{_h}, exceeds target {TW}x{TH}. \"\n        \"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\nchart.save(f\"plot-{THEME}.html\")\n"}