{"spec_id":"bar-tornado-sensitivity","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nbar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-16\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme tokens (see prompts/default-style-guide.md \"Theme-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\"\n\n# Imprint palette — first categorical series is ALWAYS brand green\nBRAND = \"#009E73\"  # Low Scenario\nLAVENDER = \"#C475FD\"  # High Scenario (Imprint position 2)\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data — NPV sensitivity analysis for a capital investment project\nparameters = [\n    \"Discount Rate\",\n    \"Revenue Growth\",\n    \"Material Cost\",\n    \"Labor Cost\",\n    \"Tax Rate\",\n    \"Initial Investment\",\n    \"Operating Margin\",\n    \"Terminal Value\",\n    \"Working Capital\",\n    \"Inflation Rate\",\n]\n\nbase_npv = 120.0  # Base case NPV in $M\n\n# Resulting NPV when each parameter is set to its low / high value.\n# Some parameters invert (lower material cost or tax rate → higher NPV).\nlow_values = base_npv + np.array([-38, -30, 22, -18, 10, -14, -10, -8, -5, -3], dtype=float)\nhigh_values = base_npv + np.array([32, 28, -18, 22, -8, 11, 13, 9, 6, 4], dtype=float)\n\n# Sort by total impact range so the widest bar lands at the top (tornado shape).\n# Seaborn draws the first category at the top row, so order widest-first.\ntotal_range = np.abs(high_values - low_values)\nsort_idx = np.argsort(total_range)[::-1]\nparameters = [parameters[i] for i in sort_idx]\nlow_values = low_values[sort_idx]\nhigh_values = high_values[sort_idx]\n\n# Deltas relative to the base case (bars diverge from the reference line)\nlow_delta = low_values - base_npv\nhigh_delta = high_values - base_npv\n\n# Long-form DataFrame for seaborn's hue API\ndf = pd.concat(\n    [\n        pd.DataFrame({\"Parameter\": parameters, \"Delta\": low_delta, \"Scenario\": \"Low Scenario\"}),\n        pd.DataFrame({\"Parameter\": parameters, \"Delta\": high_delta, \"Scenario\": \"High Scenario\"}),\n    ],\n    ignore_index=True,\n)\n\n# Plot — landscape 3200×1800\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nsns.barplot(\n    data=df,\n    y=\"Parameter\",\n    x=\"Delta\",\n    hue=\"Scenario\",\n    hue_order=[\"Low Scenario\", \"High Scenario\"],\n    palette=[BRAND, LAVENDER],\n    dodge=False,\n    edgecolor=PAGE_BG,\n    linewidth=0.6,\n    zorder=2,\n    ax=ax,\n)\n\n# Base case reference line (neutral / baseline anchor)\nax.axvline(x=0, color=INK, linewidth=1.2, linestyle=\"--\", zorder=3)\nax.annotate(\n    f\"Base case  ${int(base_npv)}M\",\n    xy=(0, 0),\n    xytext=(5, 16),\n    textcoords=\"offset points\",\n    fontsize=7.5,\n    fontstyle=\"italic\",\n    color=INK_MUTED,\n    ha=\"left\",\n    va=\"bottom\",\n    annotation_clip=False,\n)\n\n# Bar-end value labels — resulting NPV at each scenario end\nfor i in range(len(parameters)):\n    ld, hd = low_delta[i], high_delta[i]\n    lv, hv = low_values[i], high_values[i]\n    left_x, right_x = min(ld, hd), max(ld, hd)\n    left_v, right_v = (lv, hv) if ld <= hd else (hv, lv)\n    ax.text(left_x - 1.0, i, f\"${left_v:.0f}M\", va=\"center\", ha=\"right\", fontsize=7, color=INK_SOFT)\n    ax.text(right_x + 1.0, i, f\"${right_v:.0f}M\", va=\"center\", ha=\"left\", fontsize=7, color=INK_SOFT)\n\n# Relabel x ticks to absolute NPV values\nticks = ax.get_xticks()\nax.set_xticks(ticks)\nax.set_xticklabels([f\"${int(t + base_npv)}\" for t in ticks])\n\n# Emphasize the three most impactful parameters (widest bars, at the top)\nfor i, label in enumerate(ax.get_yticklabels()):\n    if i < 3:\n        label.set_fontweight(\"bold\")\n        label.set_color(INK)\n\n# Style\nax.set_xlabel(\"Net Present Value ($M)\", fontsize=10, color=INK)\nax.set_ylabel(\"Input Parameter\", fontsize=10, color=INK)\nax.set_title(\n    \"bar-tornado-sensitivity · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK, pad=12\n)\nax.tick_params(axis=\"both\", labelsize=8)\nax.margins(x=0.12)\n\nsns.despine(left=True, bottom=False, ax=ax)\nax.yaxis.grid(False)\nax.xaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)\nax.set_axisbelow(True)\n\nax.legend(fontsize=8, frameon=False, loc=\"lower right\")\n\nfig.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}