Tornado Diagram for Sensitivity Analysis — plotnine

A horizontal bar chart where bars are sorted by influence magnitude, extending left and right from a base case vertical reference line. Each bar represents one input parameter and shows how varying that parameter between its low and high values affects the output, creating a characteristic tornado shape (widest bars at top, narrowest at bottom). Dual colors distinguish low-input from high-input effects, making it immediately clear which parameters drive the most uncertainty.

Tornado Diagram for Sensitivity Analysis rendered with plotnine

Python source (plotnine)

""" anyplot.ai
bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis
Library: plotnine 0.15.5 | Python 3.13.13
Quality: 90/100 | Updated: 2026-06-02
"""

import os
import sys


# Prevent current directory from shadowing the plotnine package
sys.path = [p for p in sys.path if p and not p.endswith("implementations") and not p.endswith("python")]

import pandas as pd
from plotnine import (
    aes,
    annotate,
    coord_flip,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_col,
    geom_hline,
    geom_text,
    ggplot,
    labs,
    scale_alpha_identity,
    scale_fill_manual,
    scale_y_continuous,
    theme,
    theme_minimal,
)


# Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome")
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — semantic exception applies:
# High Scenario (positive/gain) → brand green; Low Scenario (negative/loss) → matte red
CLR_HIGH = "#009E73"  # Imprint position 1 — brand green, gain
CLR_LOW = "#AE3030"  # Imprint semantic anchor — matte red, loss

# Data
base_npv = 120.0

parameters = [
    "Discount Rate",
    "Revenue Growth",
    "Material Cost",
    "Labor Cost",
    "Tax Rate",
    "Inflation Rate",
    "Market Share",
    "Capex",
    "Operating Margin",
    "Terminal Value",
]
low_values = [95.0, 98.0, 102.0, 105.0, 108.0, 110.0, 112.0, 113.0, 115.0, 116.0]
high_values = [148.0, 145.0, 140.0, 137.0, 134.0, 131.0, 129.0, 127.0, 126.0, 124.5]

# Compute deviations from base case
records = []
for param, low, high in zip(parameters, low_values, high_values, strict=True):
    total_range = high - low
    records.append(
        {
            "parameter": param,
            "scenario": "Low Scenario",
            "deviation": low - base_npv,
            "npv_label": f"${low:.0f}M",
            "total_range": total_range,
        }
    )
    records.append(
        {
            "parameter": param,
            "scenario": "High Scenario",
            "deviation": high - base_npv,
            "npv_label": f"${high:.0f}M",
            "total_range": total_range,
        }
    )

df = pd.DataFrame(records)

# Sort by total range: ascending so widest bar sits at top after coord_flip
sort_order = df.groupby("parameter")["total_range"].first().sort_values(ascending=True).index.tolist()
top_range = int(high_values[0] - low_values[0])  # Discount Rate: 148 - 95 = 53
df["parameter"] = pd.Categorical(df["parameter"], categories=sort_order, ordered=True)

# Visual emphasis: top 3 influential parameters at full opacity, rest muted
top3 = set(sort_order[-3:])
df["bar_alpha"] = df["parameter"].apply(lambda p: 1.0 if p in top3 else 0.55)

df_low = df[df["scenario"] == "Low Scenario"]
df_high = df[df["scenario"] == "High Scenario"]

# Title — scale fontsize if title exceeds 67-char baseline
title = "bar-tornado-sensitivity · python · plotnine · anyplot.ai"
title_n = len(title)
default_title_fs = 12
title_fontsize = round(default_title_fs * 67 / title_n) if title_n > 67 else default_title_fs
title_fontsize = max(title_fontsize, 8)

# Plot
plot = (
    ggplot(df, aes(x="parameter", y="deviation", fill="scenario"))
    + geom_col(aes(alpha="bar_alpha"), position="identity", width=0.7)
    + geom_hline(yintercept=0, linetype="dashed", color=INK_SOFT, size=0.8)
    + geom_text(aes(label="npv_label", y="deviation"), data=df_low, ha="right", nudge_y=-1.0, size=3.2, color=INK_MUTED)
    + geom_text(aes(label="npv_label", y="deviation"), data=df_high, ha="left", nudge_y=1.0, size=3.2, color=INK_MUTED)
    + annotate(
        "text",
        x=10.35,
        y=30.0,
        label=f"Primary driver: ${top_range}M NPV range",
        size=2.8,
        color=INK_MUTED,
        ha="left",
        va="center",
    )
    + coord_flip()
    + scale_fill_manual(values={"Low Scenario": CLR_LOW, "High Scenario": CLR_HIGH})
    + scale_alpha_identity(guide=None)
    + scale_y_continuous(labels=lambda vals: [f"${base_npv + v:.0f}M" for v in vals], expand=(0.15, 0.15))
    + labs(
        x="", y="Net Present Value ($M)", title=title, fill="", caption=f"Dashed line: base case NPV ${base_npv:.0f}M"
    )
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        text=element_text(size=7, color=INK_SOFT),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        axis_text_y=element_text(size=8, weight="bold", color=INK),
        plot_title=element_text(size=title_fontsize, weight="bold", color=INK),
        legend_text=element_text(size=8, color=INK_SOFT),
        legend_position="top",
        legend_background=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        panel_grid_major_x=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_major_y=element_blank(),
        panel_grid_minor=element_blank(),
        axis_line_x=element_line(size=0.5, color=INK_SOFT),
        plot_caption=element_text(size=6, color=INK_MUTED, ha="right"),
    )
)

# Save
plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

Part of Tornado Diagram for Sensitivity Analysis on anyplot.ai.

Other implementations