{"spec_id":"point-and-figure-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# point-and-figure-basic: Point and Figure Chart\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 94/100 | Created: 2026-09-02\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\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\nconst BULL_COLOR = IMPRINT_PALETTE[1]  # brand green, doubles as \"gain\" semantic anchor\nconst BEAR_COLOR = IMPRINT_PALETTE[5]  # matte red, semantic anchor for \"loss\"\nconst SIGNAL_COLOR = colorant\"#DDCC77\"  # anyplot amber, reserved for caution/callout marks\n\n# --- Data: synthetic daily closing prices with alternating trend regimes ----\nn_days = 260\nday_idx = 0:(n_days - 1)\nrising_regime = mod.(day_idx, 84) .< 42\ndrift_seq = ifelse.(rising_regime, 0.16, -0.16)\ndaily_steps = drift_seq .+ randn(n_days) .* 1.3\ndaily_steps[1] = 0.0\nprices = 100.0 .+ cumsum(daily_steps)\n\n# --- Point & Figure construction --------------------------------------------\nbox_size = 2.0\nreversal_boxes = 3\n\ncurrent_box = floor(Int, prices[1] / box_size)\ndirection = 0  # 0 = undetermined, 1 = X column (rising), -1 = O column (falling)\n\ncol_dir = Int[]\ncol_low = Int[]\ncol_high = Int[]\n\nfor price in prices[2:end]\n    box = floor(Int, price / box_size)\n    if direction == 0\n        if box > current_box\n            global direction = 1\n            push!(col_dir, 1); push!(col_low, current_box); push!(col_high, box)\n            global current_box = box\n        elseif box < current_box\n            global direction = -1\n            push!(col_dir, -1); push!(col_low, box); push!(col_high, current_box)\n            global current_box = box\n        end\n    elseif direction == 1\n        if box > current_box\n            col_high[end] = box\n            global current_box = box\n        elseif box <= current_box - reversal_boxes\n            global direction = -1\n            push!(col_dir, -1); push!(col_low, box); push!(col_high, current_box - 1)\n            global current_box = box\n        end\n    else\n        if box < current_box\n            col_low[end] = box\n            global current_box = box\n        elseif box >= current_box + reversal_boxes\n            global direction = 1\n            push!(col_dir, 1); push!(col_low, current_box + 1); push!(col_high, box)\n            global current_box = box\n        end\n    end\nend\n\nn_cols = length(col_dir)\n\nx_up = Float64[]; y_up = Float64[]\nx_down = Float64[]; y_down = Float64[]\nfor col in 1:n_cols\n    boxes = col_low[col]:col_high[col]\n    if col_dir[col] == 1\n        append!(x_up, fill(Float64(col), length(boxes)))\n        append!(y_up, boxes .* box_size)\n    else\n        append!(x_down, fill(Float64(col), length(boxes)))\n        append!(y_down, boxes .* box_size)\n    end\nend\n\nprice_min = minimum(prices)\nprice_max = maximum(prices)\n\n# 45-degree support line: rises one box per column from an early swing low\n# (restricted to the first part of the chart so the line has room to run)\ntrend_window = 1:max(1, n_cols - 5)\nlow_col = trend_window[argmin(col_low[trend_window])]\nsupport_x = collect(low_col:n_cols)\nsupport_y = [(col_low[low_col] + (col - low_col)) * box_size for col in support_x]\nkeep_support = support_y .<= price_max + box_size\nsupport_x = support_x[keep_support]\nsupport_y = support_y[keep_support]\n\n# 45-degree resistance line: falls one box per column from an early swing high\nhigh_col = trend_window[argmax(col_high[trend_window])]\nresistance_x = collect(high_col:n_cols)\nresistance_y = [(col_high[high_col] - (col - high_col)) * box_size for col in resistance_x]\nkeep_resistance = resistance_y .>= price_min - box_size\nresistance_x = resistance_x[keep_resistance]\nresistance_y = resistance_y[keep_resistance]\n\n# Breakout point: analytic intersection of the two 45-degree lines\n# (support(x) = resistance(x)), used to anchor the crossover annotation\ncross_x = (col_high[high_col] + high_col - col_low[low_col] + low_col) / 2\ncross_y = (col_low[low_col] + (cross_x - low_col)) * box_size\nhas_crossover = cross_x >= max(low_col, high_col) && cross_x <= n_cols\n\n# --- Plot ---------------------------------------------------------------\ntitle_text = \"NovaTech Inc. — box \\$2.00, reversal 3 · point-and-figure-basic · julia · makie · anyplot.ai\"\ntitle_ratio = length(title_text) > 67 ? 67 / length(title_text) : 1.0\ntitle_size = max(14, round(Int, 20 * title_ratio))\n\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\ny_min = floor(price_min / box_size) * box_size\ny_max = ceil(price_max / box_size) * box_size\nxtick_step = max(1, round(Int, n_cols / 10))\n\nax = Axis(\n    fig[1, 1];\n    title              = title_text,\n    titlesize          = title_size,\n    titlecolor         = INK,\n    xlabel             = \"Column (Price Reversal)\",\n    ylabel             = \"Price (\\$)\",\n    xlabelcolor        = INK,\n    ylabelcolor        = INK,\n    xlabelsize         = 14,\n    ylabelsize         = 14,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelcolor    = INK_SOFT,\n    xticklabelsize     = 12,\n    yticklabelsize     = 12,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible     = false,\n    rightspinevisible   = false,\n    leftspinecolor      = INK_SOFT,\n    bottomspinecolor    = INK_SOFT,\n    xgridvisible        = false,\n    ygridvisible        = true,\n    ygridcolor          = RGBAf(INK.r, INK.g, INK.b, 0.15),\n    xticks              = 1:xtick_step:n_cols,\n    yticks              = y_min:box_size:y_max,\n)\n\nscatter!(ax, x_up, y_up;\n    marker = :xcross, markersize = 16, color = BULL_COLOR,\n    label = \"Bullish (X)\")\nscatter!(ax, x_down, y_down;\n    marker = :circle, markersize = 15, color = :transparent,\n    strokecolor = BEAR_COLOR, strokewidth = 2.5,\n    label = \"Bearish (O)\")\nlines!(ax, support_x, support_y;\n    color = INK, linestyle = :dash, linewidth = 2.5, label = \"Support\")\nlines!(ax, resistance_x, resistance_y;\n    color = INK, linestyle = :dash, linewidth = 2.5, label = \"Resistance\")\n\n# Annotate the swing points that anchor each trend line, so the viewer sees\n# *why* the lines start where they do rather than just that they exist.\ntext!(ax, low_col, col_low[low_col] * box_size;\n    text = \"Swing low\", color = INK_SOFT, fontsize = 12,\n    align = (:left, :top), offset = (6, -6))\ntext!(ax, high_col, col_high[high_col] * box_size;\n    text = \"Swing high\", color = INK_SOFT, fontsize = 12,\n    align = (:left, :bottom), offset = (6, 6))\n\n# Call out the support/resistance crossover as the chart's focal breakout\n# signal, rather than leaving the viewer to spot it unassisted.\nif has_crossover\n    scatter!(ax, [cross_x], [cross_y];\n        marker = :star5, markersize = 24, color = SIGNAL_COLOR,\n        strokecolor = INK, strokewidth = 1, label = \"Breakout signal\")\n    text!(ax, cross_x, cross_y;\n        text = \"Breakout\", color = INK, fontsize = 13, font = :bold,\n        align = (:center, :bottom), offset = (0, 16))\nend\n\nLegend(fig[1, 1], ax;\n    tellwidth = false, tellheight = false,\n    halign = :right, valign = :top,\n    margin = (12, 12, 12, 12),\n    padding = (10, 10, 8, 8),\n    patchsize = (18, 18),\n    rowgap = 4,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor = INK,\n    framevisible = false)\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}