{"spec_id":"line-annotated-events","library":"makie","language":"julia","code":"# anyplot.ai\n# line-annotated-events: Annotated Line Plot with Event Markers\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 88/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\nusing Dates\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\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\nconst ANYPLOT_AMBER = colorant\"#DDCC77\"\n\n# --- Data --------------------------------------------------------------------\nn_days = 365\nstart_date = Date(2024, 1, 1)\ntrading_days = start_date .+ Day.(0:(n_days - 1))\nday_index = 0:(n_days - 1)\n\ndrift = 0.00035 .* day_index\nseasonal = 6.0 .* sin.(2π .* day_index ./ 90)\nnoise = cumsum(randn(n_days) .* 1.1)\nstock_price = 148.0 .+ drift .* 400 .+ seasonal .+ noise\n\nearnings_indices = [17, 108, 199, 290, 351]\nearnings_labels = [\n    \"Q4 Earnings Beat\",\n    \"Q1 Earnings Miss\",\n    \"Product Launch\",\n    \"Q3 Earnings Beat\",\n    \"Analyst Downgrade\",\n]\nevent_dates = trading_days[earnings_indices]\nevent_values = stock_price[earnings_indices]\nevent_is_negative = [occursin(\"Miss\", lbl) || occursin(\"Downgrade\", lbl) for lbl in earnings_labels]\nevent_colors = [neg ? IMPRINT_PALETTE[5] : ANYPLOT_AMBER for neg in event_is_negative]\n\ndate_to_x(d) = Float64(Dates.value(d - start_date))\nx_days = date_to_x.(trading_days)\nevent_x = date_to_x.(event_dates)\n\nmonth_starts = [Date(2024, m, 1) for m in 1:12]\nmonth_ticks = date_to_x.(month_starts)\nmonth_labels = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\",\n                 \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n# --- Plot ----------------------------------------------------------------\ntitle_text = \"line-annotated-events · julia · makie · anyplot.ai\"\n\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = title_text,\n    titlesize         = 20,\n    titlecolor        = INK,\n    xlabel            = \"Date (2024)\",\n    ylabel            = \"Share Price (USD)\",\n    xlabelsize        = 14,\n    ylabelsize        = 14,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelsize    = 12,\n    yticklabelsize    = 12,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\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    xgridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n    xticks            = (month_ticks, month_labels),\n)\n\nprice_range = maximum(stock_price) - minimum(stock_price)\nylims!(ax, minimum(stock_price) - 0.1 * price_range, maximum(stock_price) + 0.28 * price_range)\n\n# Event markers: dashed vertical rules behind the data line, colored by sentiment\n# (matte red = negative surprise, amber = positive/neutral) so the narrative reads\n# without relying on the text labels alone.\nfor (ex, col) in zip(event_x, event_colors)\n    vlines!(ax, [ex]; color = col, linestyle = :dash, linewidth = 1.5)\nend\n\n# Main price series drawn on top of the event rules\nlines!(ax, x_days, stock_price; color = IMPRINT_PALETTE[1], linewidth = 2.5)\n\n# Event point markers and alternating, close-in label offsets so each label stays\n# visually tethered to its marker via the short run of dashed rule beneath it.\nlabel_offsets = [0.14, 0.24, 0.14, 0.24, 0.14] .* price_range\nfor (ex, ey, lbl, off, col) in zip(event_x, event_values, earnings_labels, label_offsets, event_colors)\n    scatter!(ax, [ex], [ey]; color = col, markersize = 14,\n             strokewidth = 1.5, strokecolor = PAGE_BG)\n    # short leader tick linking the marker to its label\n    lines!(ax, [ex, ex], [ey, ey + off - 2.5]; color = col, linewidth = 1.0)\n    text!(ax, ex + 5.0, ey + off; text = lbl, color = INK, fontsize = 12,\n          align = (:left, :bottom), rotation = 0.0)\nend\n\n# Composite inset legend (Makie GridLayout co-location, not axislegend) that\n# explains both the price line and the two event-marker sentiments at once.\nlegend_elements = [\n    LineElement(color = IMPRINT_PALETTE[1], linewidth = 2.5),\n    MarkerElement(color = ANYPLOT_AMBER, marker = :circle, markersize = 14,\n                  strokecolor = PAGE_BG, strokewidth = 1.5),\n    MarkerElement(color = IMPRINT_PALETTE[5], marker = :circle, markersize = 14,\n                  strokecolor = PAGE_BG, strokewidth = 1.5),\n]\nlegend_labels = [\"Share price\", \"Positive event\", \"Negative event\"]\nLegend(fig[1, 1], legend_elements, legend_labels;\n       tellwidth = false, tellheight = false, halign = :left, valign = :top,\n       margin = (10, 10, 10, 10), framevisible = true,\n       backgroundcolor = ELEVATED_BG, labelcolor = INK, framecolor = INK_SOFT)\n\n# --- Save ----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}