{"spec_id":"stock-event-flags","library":"makie","language":"julia","code":"# anyplot.ai\n# stock-event-flags: Stock Chart with Event Flags\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 89/100 | Updated: 2026-05-28\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# Theme tokens\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint palette positions 1→3 for event types; position 5 for negative reactions\nconst COLOR_EARNINGS = colorant\"#009E73\"  # 1 — brand green (quarterly earnings)\nconst COLOR_DIVIDEND = colorant\"#C475FD\"  # 2 — lavender   (dividend payments)\nconst COLOR_NEWS     = colorant\"#4467A3\"  # 3 — blue        (analyst / news events)\nconst COLOR_LOSS     = colorant\"#AE3030\"  # 5 — matte red   (negative price reaction)\n\n# Data: simulate one year of tech stock prices via geometric Brownian motion\nn_days = 252\nmu     = 0.18\nsigma  = 0.28\ndt     = 1.0 / 252\n\nlog_returns = (mu - 0.5 * sigma^2) * dt .+ sigma * sqrt(dt) .* randn(n_days)\nprices      = 150.0 .* exp.(cumsum(log_returns))\n\n# Events: (trading_day, event_type, label)\nevents = [\n    (21,  \"earnings\", \"Q4 Earn\"),\n    (63,  \"dividend\", \"Dividend\"),\n    (84,  \"earnings\", \"Q1 Earn\"),\n    (126, \"news\",     \"Merger\"),\n    (147, \"earnings\", \"Q2 Earn\"),\n    (168, \"dividend\", \"Dividend\"),\n    (210, \"earnings\", \"Q3 Earn\"),\n    (231, \"news\",     \"CEO Named\"),\n]\n\nevent_colors  = Dict(\"earnings\" => COLOR_EARNINGS, \"dividend\" => COLOR_DIVIDEND, \"news\" => COLOR_NEWS)\nevent_markers = Dict(\"earnings\" => :utriangle,     \"dividend\" => :diamond,       \"news\" => :circle)\n\n# Month x-axis ticks (~21 trading days per month)\nmonth_centers = collect(11:21:242)\nmonth_labels  = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n                 \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# Flag heights: alternating above the max price to avoid obscuring data\ny_min   = minimum(prices)\ny_max   = maximum(prices)\np_range = y_max - y_min\nflag_lo = y_max + 0.12 * p_range\nflag_hi = y_max + 0.27 * p_range\ny_top   = y_max + 0.48 * p_range\ny_bot   = y_min - 0.05 * p_range\n\n# Post-earnings price reactions: 3-day post vs 2-day pre window\nearn_days = [day for (day, etype, _) in events if etype == \"earnings\"]\nreactions = Dict{Int,Float64}()\nfor day in earn_days\n    pre  = prices[max(day - 2, 1)]\n    post = prices[min(day + 3, n_days)]\n    reactions[day] = (post - pre) / pre\nend\n\n# Title with length-adaptive fontsize\ntitle_str      = \"TechCorp 2024 · stock-event-flags · julia · makie · anyplot.ai\"\ntitle_fontsize = length(title_str) > 67 ? round(Int, 20 * 67.0 / length(title_str)) : 20\n\n# Figure\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(fig[1, 1];\n    title             = title_str,\n    titlesize         = title_fontsize,\n    titlecolor        = INK,\n    xlabel            = \"Month (2024)\",\n    ylabel            = \"Share Price (USD)\",\n    xlabelcolor       = INK,\n    xlabelsize        = 14,\n    ylabelcolor       = INK,\n    ylabelsize        = 14,\n    xticklabelcolor   = INK_SOFT,\n    xticklabelsize    = 12,\n    yticklabelcolor   = INK_SOFT,\n    yticklabelsize    = 12,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = INK_SOFT,\n    backgroundcolor   = PAGE_BG,\n    topspinevisible   = false,\n    rightspinevisible = false,\n    leftspinecolor    = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    xgridvisible      = false,\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n    xticks            = (month_centers, month_labels),\n)\n\n# Earnings blackout windows (±5 trading days — clearly visible to guide reader to earnings dates)\nfor (day, etype, _) in events\n    if etype == \"earnings\"\n        poly!(ax, Rect2f(day - 5.0, y_bot, 10.0, y_top - y_bot);\n              color       = RGBAf(COLOR_EARNINGS.r, COLOR_EARNINGS.g, COLOR_EARNINGS.b, 0.12),\n              strokewidth = 0)\n    end\nend\n\n# Area fill under price line — creates visual mass for the data layer, recedes behind event flags\nband!(ax, 1:n_days, fill(y_bot, n_days), prices;\n      color = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.10))\n\n# Stock price line (thinner so event flags dominate the visual hierarchy)\nlines!(ax, 1:n_days, prices;\n       color = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.9), linewidth = 1.2)\n\n# Event flags: dashed connector + price dot + flag marker + label\nfor (i, (day, etype, label)) in enumerate(events)\n    col    = event_colors[etype]\n    mkr    = event_markers[etype]\n    price  = prices[day]\n    flag_y = isodd(i) ? flag_hi : flag_lo\n\n    lines!(ax, [day, day], [price, flag_y]; color = col, linewidth = 1.3, linestyle = :dash)\n    scatter!(ax, [day], [price]; color = col, markersize = 8,  strokewidth = 0)\n    scatter!(ax, [day], [flag_y]; color = col, marker = mkr, markersize = 17, strokewidth = 0)\n    text!(ax, label;\n          position = (Float64(day), flag_y + 0.04 * p_range),\n          align    = (:center, :bottom),\n          fontsize = 12,\n          color    = INK)\n\n    # Earnings reaction annotation: green/red % shows how the market responded\n    if etype == \"earnings\"\n        pct     = reactions[day]\n        pct_str = (pct >= 0 ? \"+\" : \"\") * string(round(pct * 100; digits = 1)) * \"%\"\n        ann_y   = max(price - 0.08 * p_range, y_bot + 0.02 * p_range)\n        text!(ax, pct_str;\n              position = (Float64(day), ann_y),\n              align    = (:center, :top),\n              fontsize = 9,\n              color    = pct >= 0 ? COLOR_EARNINGS : COLOR_LOSS)\n    end\nend\n\nylims!(ax, y_bot, y_top)\nxlims!(ax, 0, n_days + 2)\n\n# Legend\naxislegend(ax,\n    [MarkerElement(color = COLOR_EARNINGS, marker = :utriangle, markersize = 14, strokewidth = 0),\n     MarkerElement(color = COLOR_DIVIDEND, marker = :diamond,   markersize = 14, strokewidth = 0),\n     MarkerElement(color = COLOR_NEWS,     marker = :circle,    markersize = 14, strokewidth = 0)],\n    [\"Earnings\", \"Dividend\", \"News\"];\n    position        = :lb,\n    backgroundcolor = ELEVATED_BG,\n    framecolor      = INK_SOFT,\n    labelcolor      = INK,\n    fontsize        = 11,\n    patchsize       = (14.0f0, 14.0f0),\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}