{"spec_id":"bar-tornado-sensitivity","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 88/100 | Updated: 2026-06-02\n\"\"\"\n\nimport importlib.util\nimport os\nimport sys\n\n\n# Prevent this file (pygal.py) from shadowing the installed pygal package\npygal_spec = importlib.util.find_spec(\"pygal\")\nif pygal_spec and pygal_spec.origin != __file__:\n    import pygal\n    from pygal.style import Style\nelse:\n    _here = os.path.dirname(os.path.abspath(__file__))\n    sys.path = [p for p in sys.path if os.path.abspath(p) != _here]\n    try:\n        import pygal\n        from pygal.style import Style\n    finally:\n        sys.path.insert(0, _here)\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\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 categorical palette — canonical order; first series always #009E73\nIMPRINT_PALETTE = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\")\n\n# Data — NPV sensitivity analysis for a capital investment project\n# Base case NPV = $2.5M; each parameter varied between its low and high scenarios\nparameters = [\n    \"Discount Rate\",\n    \"Revenue Growth\",\n    \"Material Cost\",\n    \"Labor Cost\",\n    \"Tax Rate\",\n    \"Salvage Value\",\n    \"Initial Investment\",\n    \"Operating Margin\",\n    \"Inflation Rate\",\n]\nbase_value = 2.5  # $2.5M base case NPV\n\nlow_values = [3.8, 1.6, 2.9, 2.7, 2.9, 2.3, 2.8, 1.9, 2.3]\nhigh_values = [1.4, 3.6, 2.0, 2.2, 2.1, 2.7, 2.2, 3.2, 2.6]\n\n# Compute deviations from base and sort by total range (widest bar at top)\ndeviations = []\nfor i, param in enumerate(parameters):\n    low_dev = low_values[i] - base_value\n    high_dev = high_values[i] - base_value\n    total_range = abs(high_values[i] - low_values[i])\n    deviations.append((param, low_dev, high_dev, total_range))\n\n# Ascending sort: pygal draws the last-added x_label at the top\ndeviations.sort(key=lambda x: x[3], reverse=False)\n\nsorted_params = [d[0] for d in deviations]\nsorted_low_devs = [d[1] for d in deviations]\nsorted_high_devs = [d[2] for d in deviations]\n\n# Mark the top driver (last in ascending sort = widest bar = top of chart)\nsorted_params[-1] = f\"* {sorted_params[-1]}\"\n\n# Title with length-based fontsize scaling (67-char baseline at size 66)\ntitle_text = \"NPV Sensitivity Analysis · bar-tornado-sensitivity · python · pygal · anyplot.ai\"\nn = len(title_text)\ntitle_fs = max(round(66 * 67 / n), 44) if n > 67 else 66  # floor 44\n\n# Style — Imprint palette, theme-adaptive chrome\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=title_fs,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    value_label_font_size=36,\n    tooltip_font_size=36,\n    title_font_family=\"Helvetica, Arial, sans-serif\",\n    label_font_family=\"Helvetica, Arial, sans-serif\",\n    value_font_family=\"Helvetica, Arial, sans-serif\",\n    legend_font_family=\"Helvetica, Arial, sans-serif\",\n    major_label_font_family=\"Helvetica, Arial, sans-serif\",\n)\n\n# Plot — HorizontalStackedBar with zero reference line (tornado shape)\nchart = pygal.HorizontalStackedBar(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=title_text,\n    x_title=\"Change in NPV ($M)  |  * = top driver\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    legend_box_size=30,\n    show_x_guides=False,\n    show_y_guides=False,\n    y_labels_major=[0],\n    range=(-1.4, 1.4),\n    print_values=True,\n    print_values_position=\"center\",\n    value_formatter=lambda x: f\"{x:+.1f}\" if x else \"\",\n    margin=50,\n    margin_left=80,\n    margin_right=50,\n    margin_bottom=110,\n    spacing=24,\n    truncate_label=-1,\n    rounded_bars=8,\n    zero=0,\n)\n\nchart.x_labels = sorted_params\n\n# Dict-based values provide rich interactive tooltips — distinctive pygal feature\nlow_series = [\n    {\"value\": v, \"label\": f\"{p}: NPV ${base_value + v:.1f}M (base ${base_value}M)\"}\n    for v, p in zip(sorted_low_devs, sorted_params, strict=True)\n]\nhigh_series = [\n    {\"value\": v, \"label\": f\"{p}: NPV ${base_value + v:.1f}M (base ${base_value}M)\"}\n    for v, p in zip(sorted_high_devs, sorted_params, strict=True)\n]\n\nchart.add(\"Low Input Effect\", low_series)\nchart.add(\"High Input Effect\", high_series)\n\n# Save — interactive HTML + static PNG (pygal is an interactive library)\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\nchart.render_to_png(f\"plot-{THEME}.png\")\n"}