{"spec_id":"stock-event-flags","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nstock-event-flags: Stock Chart with Event Flags\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-27\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_area,\n    geom_line,\n    geom_point,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_shape_manual,\n    scale_x_datetime,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nBRAND = \"#009E73\"  # Imprint palette position 1\n\n# Data\nnp.random.seed(42)\nn_days = 180\ndates = pd.date_range(start=\"2024-01-02\", periods=n_days, freq=\"B\")\n\nreturns = np.random.normal(0.0005, 0.018, n_days)\nclose_prices = 150 * np.cumprod(1 + returns)\ndf_price = pd.DataFrame({\"date\": dates, \"close\": close_prices})\n\nevents = [\n    {\"date\": \"2024-01-25\", \"type\": \"Earnings\", \"label\": \"Q4 Beat\"},\n    {\"date\": \"2024-02-15\", \"type\": \"Dividend\", \"label\": \"Div $0.25\"},\n    {\"date\": \"2024-03-20\", \"type\": \"News\", \"label\": \"Product Launch\"},\n    {\"date\": \"2024-04-25\", \"type\": \"Earnings\", \"label\": \"Q1 Earnings\"},\n    {\"date\": \"2024-05-10\", \"type\": \"Dividend\", \"label\": \"Div $0.25\"},\n    {\"date\": \"2024-06-05\", \"type\": \"News\", \"label\": \"Analyst Upgrade\"},\n    {\"date\": \"2024-07-25\", \"type\": \"Earnings\", \"label\": \"Q2 Miss\"},\n    {\"date\": \"2024-08-15\", \"type\": \"Dividend\", \"label\": \"Div $0.28\"},\n    {\"date\": \"2024-09-12\", \"type\": \"Split\", \"label\": \"2:1 Split\"},\n]\n\ndf_events = pd.DataFrame(events)\ndf_events[\"date\"] = pd.to_datetime(df_events[\"date\"])\n\n# Match each event to the nearest trading day\nevent_prices = []\nevent_dates_matched = []\nfor event_date in df_events[\"date\"]:\n    idx = np.abs(df_price[\"date\"] - event_date).argmin()\n    event_prices.append(df_price.iloc[idx][\"close\"])\n    event_dates_matched.append(df_price.iloc[idx][\"date\"])\n\ndf_events[\"price\"] = event_prices\ndf_events[\"date_matched\"] = event_dates_matched\n\n# Three-tier staggered flag heights — wider spacing reduces Jan/Feb crowding\nprice_range = close_prices.max() - close_prices.min()\ntier_offsets = [price_range * 0.18, price_range * 0.36, price_range * 0.54]\ndf_events[\"flag_y\"] = [p + tier_offsets[i % 3] for i, p in enumerate(event_prices)]\n\n# Y axis limits: give 10% headroom above highest flag and 5% below price floor\ny_min = close_prices.min() - price_range * 0.05\ny_max = df_events[\"flag_y\"].max() + price_range * 0.12\n\n# Shaded band for Q2 Miss period — connects the earnings event to the price drop\ndf_q2_band = pd.DataFrame(\n    {\"xmin\": [pd.Timestamp(\"2024-06-25\")], \"xmax\": [pd.Timestamp(\"2024-08-10\")], \"ymin\": [y_min], \"ymax\": [y_max]}\n)\n\n# Plot\ntitle = \"stock-event-flags · python · letsplot · anyplot.ai\"\nn_title = len(title)\ntitle_fontsize = round(16 * 67 / n_title) if n_title > 67 else 16\n\n# Semantic color mapping — Imprint palette with finance semantics\n# Dividend→green (income), Earnings→blue (reporting), News→ochre, Split→red (major change)\nevent_color_map = {\"Earnings\": \"#4467A3\", \"Dividend\": BRAND, \"News\": \"#BD8233\", \"Split\": \"#AE3030\"}\n# Distinct shapes per event type: diamond, circle, triangle, square\nevent_shape_map = {\"Earnings\": 18, \"Dividend\": 16, \"News\": 17, \"Split\": 15}\n\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major=element_line(color=INK_SOFT, size=0.2),\n    panel_grid_minor=element_blank(),\n    axis_title=element_text(color=INK, size=12),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    axis_line=element_line(color=INK_SOFT),\n    plot_title=element_text(color=INK, size=title_fontsize),\n    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK, size=11),\n    legend_position=\"right\",\n)\n\nplot = (\n    ggplot()\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=df_q2_band,\n        fill=\"#AE3030\",\n        alpha=0.07,\n        color=\"#AE3030\",\n        size=0,\n    )\n    + geom_area(aes(x=\"date\", y=\"close\"), data=df_price, fill=INK_SOFT, alpha=0.12)\n    + geom_line(\n        aes(x=\"date\", y=\"close\"),\n        data=df_price,\n        color=INK_SOFT,\n        size=1.3,\n        tooltips=layer_tooltips().line(\"@date\").line(\"Price: $@close\"),\n    )\n    + geom_segment(\n        aes(x=\"date_matched\", y=\"price\", xend=\"date_matched\", yend=\"flag_y\"),\n        data=df_events,\n        color=INK_MUTED,\n        size=0.5,\n        linetype=\"dashed\",\n    )\n    + geom_point(\n        aes(x=\"date_matched\", y=\"flag_y\", color=\"type\", shape=\"type\"),\n        data=df_events,\n        size=6,\n        tooltips=layer_tooltips().title(\"@type\").line(\"@label\").line(\"Date: @date_matched\").line(\"Price: $@{price}\"),\n    )\n    + geom_text(aes(x=\"date_matched\", y=\"flag_y\", label=\"label\"), data=df_events, vjust=-1.5, size=4, color=INK)\n    + geom_point(aes(x=\"date_matched\", y=\"price\"), data=df_events, color=INK_MUTED, size=2.5)\n    + scale_color_manual(values=event_color_map, name=\"Event Type\")\n    + scale_shape_manual(values=event_shape_map, name=\"Event Type\")\n    + scale_x_datetime(format=\"%b %Y\")\n    + scale_y_continuous(limits=[y_min, y_max])\n    + labs(x=\"Date\", y=\"Stock Price ($)\", title=title)\n    + theme_minimal()\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}