{"spec_id":"bar-tornado-sensitivity","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nbar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-02\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent current directory from shadowing the plotnine package\nsys.path = [p for p in sys.path if p and not p.endswith(\"implementations\") and not p.endswith(\"python\")]\n\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_flip,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_col,\n    geom_hline,\n    geom_text,\n    ggplot,\n    labs,\n    scale_alpha_identity,\n    scale_fill_manual,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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 — semantic exception applies:\n# High Scenario (positive/gain) → brand green; Low Scenario (negative/loss) → matte red\nCLR_HIGH = \"#009E73\"  # Imprint position 1 — brand green, gain\nCLR_LOW = \"#AE3030\"  # Imprint semantic anchor — matte red, loss\n\n# Data\nbase_npv = 120.0\n\nparameters = [\n    \"Discount Rate\",\n    \"Revenue Growth\",\n    \"Material Cost\",\n    \"Labor Cost\",\n    \"Tax Rate\",\n    \"Inflation Rate\",\n    \"Market Share\",\n    \"Capex\",\n    \"Operating Margin\",\n    \"Terminal Value\",\n]\nlow_values = [95.0, 98.0, 102.0, 105.0, 108.0, 110.0, 112.0, 113.0, 115.0, 116.0]\nhigh_values = [148.0, 145.0, 140.0, 137.0, 134.0, 131.0, 129.0, 127.0, 126.0, 124.5]\n\n# Compute deviations from base case\nrecords = []\nfor param, low, high in zip(parameters, low_values, high_values, strict=True):\n    total_range = high - low\n    records.append(\n        {\n            \"parameter\": param,\n            \"scenario\": \"Low Scenario\",\n            \"deviation\": low - base_npv,\n            \"npv_label\": f\"${low:.0f}M\",\n            \"total_range\": total_range,\n        }\n    )\n    records.append(\n        {\n            \"parameter\": param,\n            \"scenario\": \"High Scenario\",\n            \"deviation\": high - base_npv,\n            \"npv_label\": f\"${high:.0f}M\",\n            \"total_range\": total_range,\n        }\n    )\n\ndf = pd.DataFrame(records)\n\n# Sort by total range: ascending so widest bar sits at top after coord_flip\nsort_order = df.groupby(\"parameter\")[\"total_range\"].first().sort_values(ascending=True).index.tolist()\ntop_range = int(high_values[0] - low_values[0])  # Discount Rate: 148 - 95 = 53\ndf[\"parameter\"] = pd.Categorical(df[\"parameter\"], categories=sort_order, ordered=True)\n\n# Visual emphasis: top 3 influential parameters at full opacity, rest muted\ntop3 = set(sort_order[-3:])\ndf[\"bar_alpha\"] = df[\"parameter\"].apply(lambda p: 1.0 if p in top3 else 0.55)\n\ndf_low = df[df[\"scenario\"] == \"Low Scenario\"]\ndf_high = df[df[\"scenario\"] == \"High Scenario\"]\n\n# Title — scale fontsize if title exceeds 67-char baseline\ntitle = \"bar-tornado-sensitivity · python · plotnine · anyplot.ai\"\ntitle_n = len(title)\ndefault_title_fs = 12\ntitle_fontsize = round(default_title_fs * 67 / title_n) if title_n > 67 else default_title_fs\ntitle_fontsize = max(title_fontsize, 8)\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"parameter\", y=\"deviation\", fill=\"scenario\"))\n    + geom_col(aes(alpha=\"bar_alpha\"), position=\"identity\", width=0.7)\n    + geom_hline(yintercept=0, linetype=\"dashed\", color=INK_SOFT, size=0.8)\n    + geom_text(aes(label=\"npv_label\", y=\"deviation\"), data=df_low, ha=\"right\", nudge_y=-1.0, size=3.2, color=INK_MUTED)\n    + geom_text(aes(label=\"npv_label\", y=\"deviation\"), data=df_high, ha=\"left\", nudge_y=1.0, size=3.2, color=INK_MUTED)\n    + annotate(\n        \"text\",\n        x=10.35,\n        y=30.0,\n        label=f\"Primary driver: ${top_range}M NPV range\",\n        size=2.8,\n        color=INK_MUTED,\n        ha=\"left\",\n        va=\"center\",\n    )\n    + coord_flip()\n    + scale_fill_manual(values={\"Low Scenario\": CLR_LOW, \"High Scenario\": CLR_HIGH})\n    + scale_alpha_identity(guide=None)\n    + scale_y_continuous(labels=lambda vals: [f\"${base_npv + v:.0f}M\" for v in vals], expand=(0.15, 0.15))\n    + labs(\n        x=\"\", y=\"Net Present Value ($M)\", title=title, fill=\"\", caption=f\"Dashed line: base case NPV ${base_npv:.0f}M\"\n    )\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        text=element_text(size=7, color=INK_SOFT),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=8, weight=\"bold\", color=INK),\n        plot_title=element_text(size=title_fontsize, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"top\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major_x=element_line(color=INK, size=0.3, alpha=0.15),\n        panel_grid_major_y=element_blank(),\n        panel_grid_minor=element_blank(),\n        axis_line_x=element_line(size=0.5, color=INK_SOFT),\n        plot_caption=element_text(size=6, color=INK_MUTED, ha=\"right\"),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}