{"spec_id":"bar-tornado-sensitivity","library":"makie","language":"julia","code":"# anyplot.ai\n# bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 90/100 | Created: 2026-06-02\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\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 INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\n# Imprint palette — semantic: green = high/upside, red = low/downside\nconst COLOR_HIGH = colorant\"#009E73\"  # Imprint position 1\nconst COLOR_LOW  = colorant\"#AE3030\"  # Imprint position 5 (semantic anchor: downside)\n\n# NPV sensitivity analysis — capital investment project, values in millions USD\nconst BASE_NPV = 120.0\n\nparameters = [\n    \"Discount Rate\",\n    \"Revenue Growth\",\n    \"Operating Costs\",\n    \"Terminal Value Multiple\",\n    \"Market Share\",\n    \"Tax Rate\",\n    \"Capex Intensity\",\n    \"Working Capital\",\n    \"Inflation Rate\",\n]\nlow_values  = [155.0,  95.0, 142.0, 102.0, 104.0, 135.0, 131.0, 126.0, 123.0]\nhigh_values = [ 87.0, 148.0,  99.0, 141.0, 138.0, 109.0, 112.0, 114.0, 116.0]\n\n# Sort ascending by sensitivity range — widest bar plots at top (highest y index)\nranges = abs.(high_values .- low_values)\norder  = sortperm(ranges)\nn      = length(parameters)\nparams = parameters[order]\nlows   = low_values[order]\nhighs  = high_values[order]\n\nlow_deltas  = lows  .- BASE_NPV\nhigh_deltas = highs .- BASE_NPV\n\n# Figure\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\ntitle_str = \"NPV Sensitivity Analysis · bar-tornado-sensitivity · julia · makie · anyplot.ai\"\ntitle_fs  = round(Int, 20 * 67 / length(title_str))\n\nax = Axis(\n    fig[1, 1];\n    title              = title_str,\n    titlesize          = title_fs,\n    titlefont          = :bold,\n    titlecolor         = INK,\n    xlabel             = \"Net Present Value (millions USD)\",\n    xlabelcolor        = INK,\n    xlabelsize         = 14,\n    xticklabelsize     = 14,\n    xticklabelcolor    = INK_SOFT,\n    yticklabelsize     = 14,\n    yticklabelcolor    = INK_SOFT,\n    xtickcolor         = INK_SOFT,\n    ytickcolor         = INK_SOFT,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    bottomspinecolor   = INK_SOFT,\n    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    ygridvisible       = false,\n    xminorgridvisible  = false,\n    yminorgridvisible  = false,\n)\n\n# Tornado bars — low and high input scenarios\nbarplot!(ax, 1:n, low_deltas;\n    direction   = :x,\n    offset      = BASE_NPV,\n    color       = COLOR_LOW,\n    strokewidth = 0,\n)\nbarplot!(ax, 1:n, high_deltas;\n    direction   = :x,\n    offset      = BASE_NPV,\n    color       = COLOR_HIGH,\n    strokewidth = 0,\n)\n\n# Reference line at base case NPV\nvlines!(ax, BASE_NPV; color = INK, linewidth = 2.0, linestyle = :dash)\n\n# Y-axis parameter labels and limits\nax.yticks = (1:n, params)\nylims!(ax, 0.3, Float64(n) + 0.7)\n\n# X-axis limits with padding for bar-end labels\nx_pad = 10.0\nxlims!(ax,\n    minimum(min.(lows, highs)) - x_pad,\n    maximum(max.(lows, highs)) + x_pad,\n)\n\n# Reference line annotation\ntext!(ax, [Point2f(BASE_NPV + 2.0, n + 0.4)];\n    text     = [\"Base: \\$120M\"],\n    color    = INK,\n    fontsize = 11,\n    align    = (:left, :center),\n)\n\n# Bar-end value labels — one label per bar tip showing the resulting NPV\nfor i in 1:n\n    xl = lows[i]\n    xoff_l  = xl > BASE_NPV ? 2.0 : -2.0\n    align_l = xl > BASE_NPV ? :left : :right\n    text!(ax, [Point2f(xl + xoff_l, Float64(i))];\n        text     = [\"$(round(Int, xl))\"],\n        color    = COLOR_LOW,\n        fontsize = 11,\n        align    = (align_l, :center),\n    )\n    xh = highs[i]\n    xoff_h  = xh > BASE_NPV ? 2.0 : -2.0\n    align_h = xh > BASE_NPV ? :left : :right\n    text!(ax, [Point2f(xh + xoff_h, Float64(i))];\n        text     = [\"$(round(Int, xh))\"],\n        color    = COLOR_HIGH,\n        fontsize = 11,\n        align    = (align_h, :center),\n    )\nend\n\n# Legend — framevisible=false per style guide (no legend box borders)\nelem_high = PolyElement(color = COLOR_HIGH, strokecolor = :transparent)\nelem_low  = PolyElement(color = COLOR_LOW,  strokecolor = :transparent)\nLegend(\n    fig[1, 2],\n    [elem_high, elem_low],\n    [\"High Input Scenario\", \"Low Input Scenario\"];\n    framevisible    = false,\n    backgroundcolor = ELEVATED_BG,\n    labelcolor      = INK_SOFT,\n    labelsize       = 13,\n    rowgap          = 8,\n)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}