{"spec_id":"bar-tornado-sensitivity","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 89/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport plotly.graph_objects as go\n\n\n# Theme tokens (Imprint palette + adaptive chrome)\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\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint palette — semantic: green = high/upside, blue = low/downside\n_HIGH_RGB = \"0,158,115\"  # #009E73 — Imprint position 1, brand green\n_LOW_RGB = \"68,103,163\"  # #4467A3 — Imprint position 3, blue\n\n# Data — NPV sensitivity analysis for a capital investment project\nparameters = [\n    \"Discount Rate\",\n    \"Revenue Growth\",\n    \"Initial Investment\",\n    \"Operating Costs\",\n    \"Tax Rate\",\n    \"Terminal Value\",\n    \"Working Capital\",\n    \"Salvage Value\",\n]\nbase_npv = 12.5  # Base case NPV in $M\n\n# Output NPV at each scenario — varied asymmetry highlights different parameter behaviours\nlow_values = np.array([16.2, 8.0, 14.7, 15.3, 14.3, 10.3, 13.5, 12.0])\nhigh_values = np.array([9.1, 16.0, 10.1, 10.5, 11.3, 15.6, 11.8, 13.2])\n\n# Sort by total range — widest bar at top, narrowest at bottom (tornado shape)\ntotal_range = np.abs(high_values - low_values)\nsort_idx = np.argsort(total_range)\nparameters = [parameters[i] for i in sort_idx]\nlow_values = low_values[sort_idx]\nhigh_values = high_values[sort_idx]\ntotal_range = total_range[sort_idx]\n\n# Deltas from base case (bar width/direction)\nlow_deltas = low_values - base_npv\nhigh_deltas = high_values - base_npv\n\n# Gradient opacity by influence rank — more vivid = wider bar = stronger driver\nn = len(parameters)\nlow_colors = [f\"rgba({_LOW_RGB},{0.50 + 0.50 * i / (n - 1):.2f})\" for i in range(n)]\nhigh_colors = [f\"rgba({_HIGH_RGB},{0.50 + 0.50 * i / (n - 1):.2f})\" for i in range(n)]\n\n# Plot\nfig = go.Figure()\n\nfig.add_trace(\n    go.Bar(\n        y=parameters,\n        x=low_deltas,\n        base=base_npv,\n        orientation=\"h\",\n        name=\"Low Scenario\",\n        marker={\"color\": low_colors, \"line\": {\"width\": 0}},\n        text=[f\"${v:.1f}M\" for v in low_values],\n        textposition=\"outside\",\n        textfont={\"size\": 11, \"color\": INK_SOFT},\n        cliponaxis=False,\n        hovertemplate=\"%{y}<br>Low: %{text}<br>Change: %{x:+.1f}M<extra></extra>\",\n    )\n)\n\nfig.add_trace(\n    go.Bar(\n        y=parameters,\n        x=high_deltas,\n        base=base_npv,\n        orientation=\"h\",\n        name=\"High Scenario\",\n        marker={\"color\": high_colors, \"line\": {\"width\": 0}},\n        text=[f\"${v:.1f}M\" for v in high_values],\n        textposition=\"outside\",\n        textfont={\"size\": 11, \"color\": INK_SOFT},\n        cliponaxis=False,\n        hovertemplate=\"%{y}<br>High: %{text}<br>Change: %{x:+.1f}M<extra></extra>\",\n    )\n)\n\n# Base case reference line\nfig.add_vline(x=base_npv, line={\"color\": INK_MUTED, \"width\": 1.5, \"dash\": \"dot\"})\n\n# Base case label above reference line\nfig.add_annotation(\n    x=base_npv,\n    y=1.0,\n    yref=\"paper\",\n    text=f\"Base: <b>${base_npv}M</b>\",\n    showarrow=False,\n    font={\"size\": 10, \"color\": INK_SOFT},\n    yshift=14,\n    xanchor=\"center\",\n)\n\n# Largest swing annotation — top driver highlighted for the reader\nfig.add_annotation(\n    x=max(high_values[-1], low_values[-1]) + 0.35,\n    y=parameters[-1],\n    text=f\"<b>Largest swing: ${total_range[-1]:.1f}M</b>\",\n    showarrow=False,\n    xanchor=\"left\",\n    font={\"size\": 10, \"color\": INK_SOFT},\n    yshift=20,\n)\n\ntitle_text = \"bar-tornado-sensitivity · python · plotly · anyplot.ai\"\ntitle_size = round(16 * min(1.0, 67 / len(title_text)))  # 54 chars < 67, stays 16\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": title_text,\n        \"font\": {\"size\": title_size, \"color\": INK, \"family\": \"Arial, sans-serif\"},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Net Present Value ($M)\", \"font\": {\"size\": 12, \"color\": INK}, \"standoff\": 15},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"tickprefix\": \"$\",\n        \"ticksuffix\": \"M\",\n        \"range\": [5.0, 19.0],\n        \"showgrid\": True,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"zeroline\": False,\n        \"showline\": False,\n        \"mirror\": False,\n    },\n    yaxis={\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"automargin\": True,\n        \"showline\": False,\n        \"mirror\": False,\n    },\n    barmode=\"overlay\",\n    bargap=0.25,\n    legend={\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"orientation\": \"h\",\n        \"yanchor\": \"bottom\",\n        \"y\": 1.06,\n        \"xanchor\": \"center\",\n        \"x\": 0.5,\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"itemsizing\": \"constant\",\n    },\n    margin={\"l\": 10, \"r\": 140, \"t\": 90, \"b\": 60},\n    plot_bgcolor=PAGE_BG,\n    paper_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n)\n\n# Save — theme-suffixed PNG + interactive HTML\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}