diff --git a/plots/bar-tornado-sensitivity/implementations/python/seaborn.py b/plots/bar-tornado-sensitivity/implementations/python/seaborn.py index 1c2381163c..384df92ec8 100644 --- a/plots/bar-tornado-sensitivity/implementations/python/seaborn.py +++ b/plots/bar-tornado-sensitivity/implementations/python/seaborn.py @@ -1,8 +1,10 @@ -""" pyplots.ai -bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis -Library: seaborn 0.13.2 | Python 3.14.3 -Quality: 91/100 | Created: 2026-03-07 -""" +import os +import sys + + +# Sibling matplotlib.py in this dir would shadow the installed package — remove the dir first +_here = os.path.dirname(os.path.abspath(__file__)) +sys.path = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != _here] import matplotlib.pyplot as plt import numpy as np @@ -10,11 +12,35 @@ import seaborn as sns -# Configure seaborn style and context -sns.set_style("whitegrid", {"grid.linestyle": "--", "grid.alpha": 0.3}) -sns.set_context("talk", font_scale=1.1) +# --- Theme --- +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 — canonical order, first series always #009E73 +IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"] + +sns.set_theme( + style="ticks", + rc={ + "figure.facecolor": PAGE_BG, + "axes.facecolor": PAGE_BG, + "axes.edgecolor": INK_SOFT, + "axes.labelcolor": INK, + "text.color": INK, + "xtick.color": INK_SOFT, + "ytick.color": INK_SOFT, + "grid.color": INK, + "grid.alpha": 0.15, + "legend.facecolor": ELEVATED_BG, + "legend.edgecolor": INK_SOFT, + }, +) -# Data - NPV sensitivity analysis for a capital investment project +# --- Data: NPV sensitivity analysis for a capital investment project --- parameters = [ "Discount Rate", "Revenue Growth", @@ -30,40 +56,36 @@ base_npv = 120.0 # Base case NPV in $M -np.random.seed(42) -# Some parameters have inverted relationships: -# - Material Cost: lower cost → higher NPV (low input = positive effect) -# - Tax Rate: lower tax → higher NPV (low input = positive effect) +# Inverted relationships: lower Material Cost or Tax Rate → higher NPV low_values = base_npv + np.array([-38, -30, 22, -18, 10, -14, -10, -8, -5, -3], dtype=float) high_values = base_npv + np.array([32, 28, -18, 22, -8, 11, 13, 9, 6, 4], dtype=float) -# Calculate ranges and sort by total impact +# Sort by total impact range — widest bar at top (classic tornado shape) total_range = np.abs(high_values - low_values) sort_idx = np.argsort(total_range) parameters = [parameters[i] for i in sort_idx] low_values = low_values[sort_idx] high_values = high_values[sort_idx] -# Build deltas relative to base case low_delta = low_values - base_npv high_delta = high_values - base_npv -# Custom palette anchored on Python Blue -color_low = "#D4652F" # warm copper-orange for low scenario -color_high = "#306998" # Python Blue for high scenario +# Imprint palette: #009E73 (brand green, first series) for Low Scenario +# #4467A3 (blue) for High Scenario +color_low = IMPRINT_PALETTE[0] # #009E73 +color_high = IMPRINT_PALETTE[2] # #4467A3 -# Create long-form DataFrame for seaborn barplot -df_low = pd.DataFrame({"Parameter": parameters, "NPV ($M)": low_delta, "Scenario": "Low Scenario"}) -df_high = pd.DataFrame({"Parameter": parameters, "NPV ($M)": high_delta, "Scenario": "High Scenario"}) +df_low = pd.DataFrame({"Parameter": parameters, "NPV Delta ($M)": low_delta, "Scenario": "Low Scenario"}) +df_high = pd.DataFrame({"Parameter": parameters, "NPV Delta ($M)": high_delta, "Scenario": "High Scenario"}) df = pd.concat([df_low, df_high], ignore_index=True) -# Plot -fig, ax = plt.subplots(figsize=(16, 9)) +# --- Canvas: 3200×1800 px (landscape 16:9) — no bbox_inches='tight' --- +fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400) sns.barplot( data=df, y="Parameter", - x="NPV ($M)", + x="NPV Delta ($M)", hue="Scenario", hue_order=["Low Scenario", "High Scenario"], palette=[color_low, color_high], @@ -73,57 +95,65 @@ edgecolor="none", ) -# Shift x-axis to show absolute NPV values -current_ticks = ax.get_xticks() -ax.set_xticks(current_ticks) -ax.set_xticklabels([f"${int(t + base_npv)}" for t in current_ticks]) +# Widen view to accommodate bar-end annotations without clipping +ax.set_xlim(low_delta.min() - 9, high_delta.max() + 9) + +# Relabel x-axis ticks with absolute NPV values (not deltas) +ticks = ax.get_xticks() +ax.set_xticks(ticks) +ax.set_xticklabels([f"${int(t + base_npv)}" for t in ticks]) + +# Base case reference line +ax.axvline(x=0, color=INK_SOFT, linewidth=1.2, linestyle="--", zorder=3) -# Base case reference line with annotation at top -ax.axvline(x=0, color="#333333", linewidth=1.8, linestyle="--", zorder=3) +# Base case annotation just above the top bar — avoids crowding at the base line ax.annotate( - f"Base Case ${int(base_npv)}M", - xy=(0, len(parameters) - 1), - xytext=(8, -20), + f"Base Case: ${int(base_npv)}M", + xy=(0, len(parameters) - 0.45), + xytext=(5, 0), textcoords="offset points", - fontsize=12, + fontsize=7.5, fontweight="bold", - color="#333333", - ha="left", - va="top", fontstyle="italic", + color=INK_SOFT, + ha="left", + va="center", ) -# Bar-end value annotations -for i, _param in enumerate(parameters): +# Bar-end value annotations (8pt — larger relative to canvas than previous 12pt@4800px) +for i in range(len(parameters)): lv = low_values[i] hv = high_values[i] ld = low_delta[i] hd = high_delta[i] - # Position annotations at the outer end of each bar neg_x = min(ld, hd) pos_x = max(ld, hd) - ax.text(neg_x - 1.2, i, f"${min(lv, hv):.0f}M", va="center", ha="right", fontsize=12, color="#555555") - ax.text(pos_x + 1.2, i, f"${max(lv, hv):.0f}M", va="center", ha="left", fontsize=12, color="#555555") + ax.text(neg_x - 0.9, i, f"${min(lv, hv):.0f}M", va="center", ha="right", fontsize=8, color=INK_MUTED) + ax.text(pos_x + 0.9, i, f"${max(lv, hv):.0f}M", va="center", ha="left", fontsize=8, color=INK_MUTED) + +# Axis labels and title +ax.set_ylabel("Input Parameter", fontsize=10, color=INK) +ax.set_xlabel("Net Present Value ($M)", fontsize=10, color=INK) +ax.set_title( + "bar-tornado-sensitivity · python · seaborn · anyplot.ai", fontsize=12, fontweight="medium", pad=12, color=INK +) +ax.tick_params(axis="y", labelsize=8, colors=INK_SOFT) +ax.tick_params(axis="x", labelsize=8, colors=INK_SOFT) -# Emphasize top 3 most impactful parameters with bold labels -ytick_labels = ax.get_yticklabels() -for i, label in enumerate(ytick_labels): - if i >= len(parameters) - 3: - label.set_fontweight("bold") - label.set_fontsize(17) - -# Style refinements -ax.set_ylabel("Input Parameter", fontsize=20) -ax.set_xlabel("Net Present Value ($M)", fontsize=20) -ax.set_title("bar-tornado-sensitivity \u00b7 seaborn \u00b7 pyplots.ai", fontsize=24, fontweight="medium", pad=20) -ax.tick_params(axis="y", labelsize=16) -ax.tick_params(axis="x", labelsize=16) +# Grid and spines sns.despine(left=True, bottom=False) ax.yaxis.grid(False) -ax.xaxis.grid(True, alpha=0.2, linewidth=0.8) +ax.xaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK) # Legend -ax.legend(fontsize=15, frameon=False, loc="upper right") +ax.legend(fontsize=8, frameon=True, facecolor=ELEVATED_BG, edgecolor=INK_SOFT, loc="upper right") + +# Bold top-3 most impactful parameter labels — must come after canvas draw +fig.canvas.draw() +ytick_labels = ax.get_yticklabels() +for i, label in enumerate(ytick_labels): + if i >= len(parameters) - 3: + label.set_fontweight("bold") + label.set_fontsize(9) -plt.tight_layout() -plt.savefig("plot.png", dpi=300, bbox_inches="tight") +plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG) diff --git a/plots/bar-tornado-sensitivity/metadata/python/seaborn.yaml b/plots/bar-tornado-sensitivity/metadata/python/seaborn.yaml index c00495b260..ecad630e34 100644 --- a/plots/bar-tornado-sensitivity/metadata/python/seaborn.yaml +++ b/plots/bar-tornado-sensitivity/metadata/python/seaborn.yaml @@ -1,227 +1,21 @@ +# Per-library metadata for seaborn implementation of bar-tornado-sensitivity +# Auto-generated by impl-generate.yml + library: seaborn +language: python specification_id: bar-tornado-sensitivity created: '2026-03-07T08:44:49Z' -updated: '2026-03-07T09:04:49Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 22795810081 +updated: '2026-06-02T22:58:07Z' +generated_by: claude-sonnet +workflow_run: 26852597099 issue: 4566 -python_version: 3.14.3 +language_version: 3.13.13 library_version: 0.13.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/seaborn/plot.png -preview_html: null -quality_score: 91 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/python/seaborn/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/python/seaborn/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: null review: - strengths: - - Excellent data storytelling with bold top-3 parameter labels creating clear visual - hierarchy - - Custom copper-orange and Python Blue palette provides strong contrast and professional - appearance - - Complete spec compliance with all required features including bar-end value annotations - - Realistic NPV sensitivity data with inverted relationships showing domain understanding - - Clean idiomatic seaborn code using long-form DataFrame with hue parameter - weaknesses: - - Bar-end annotations at 12pt could be slightly larger for better readability at - full resolution - - Base case annotation placement near x-axis bottom is slightly crowded - image_description: 'The plot displays a horizontal tornado/sensitivity bar chart - showing NPV sensitivity analysis for 10 financial parameters. Bars extend left - and right from a dashed vertical base case reference line at $120M. Two colors - are used: a warm copper-orange for the "Low Scenario" and Python Blue for the - "High Scenario." Parameters are sorted by total impact range from bottom (widest: - Discount Rate spanning $82M-$152M) to top (narrowest: Inflation Rate spanning - $117M-$124M). The top 3 most impactful parameters (Discount Rate, Revenue Growth, - Labor Cost) have bold, larger y-axis labels. Bar-end annotations show absolute - NPV values in gray text. The title reads "bar-tornado-sensitivity · seaborn · - pyplots.ai". A legend in the upper right identifies the two scenarios. The x-axis - shows "Net Present Value ($M)" with dollar-formatted ticks, and the y-axis shows - "Input Parameter." Spines are removed on the left, with subtle x-axis gridlines. - The base case annotation appears in italic bold near the bottom of the reference - line.' - criteria_checklist: - visual_quality: - score: 28 - max: 30 - items: - - id: VQ-01 - name: Text Legibility - score: 7 - max: 8 - passed: true - comment: All font sizes explicitly set (title 24, labels 20, ticks 16, top-3 - bold at 17). Bar-end annotations at 12pt readable but smaller side. - - id: VQ-02 - name: No Overlap - score: 6 - max: 6 - passed: true - comment: No overlapping text; bar-end labels well-positioned outside bars. - - id: VQ-03 - name: Element Visibility - score: 6 - max: 6 - passed: true - comment: Bars clearly visible with good width, tornado shape immediately apparent. - - id: VQ-04 - name: Color Accessibility - score: 4 - max: 4 - passed: true - comment: Copper-orange vs Python Blue is colorblind-safe with strong contrast. - - id: VQ-05 - name: Layout & Canvas - score: 3 - max: 4 - passed: true - comment: Good 16:9 utilization; base case annotation near x-axis area slightly - crowded. - - id: VQ-06 - name: Axis Labels & Title - score: 2 - max: 2 - passed: true - comment: 'Descriptive labels with units: Net Present Value ($M) and Input - Parameter.' - design_excellence: - score: 16 - max: 20 - items: - - id: DE-01 - name: Aesthetic Sophistication - score: 6 - max: 8 - passed: true - comment: Custom copper-orange + Python Blue palette, intentional color choices, - professional appearance clearly above defaults. - - id: DE-02 - name: Visual Refinement - score: 5 - max: 6 - passed: true - comment: Left spine removed, y-grid disabled, x-grid subtle (alpha=0.2), edgecolor - none on bars, generous whitespace. - - id: DE-03 - name: Data Storytelling - score: 5 - max: 6 - passed: true - comment: Top 3 parameters emphasized with bold/larger labels. Tornado sort - guides viewer. Bar-end values add context. - spec_compliance: - score: 15 - max: 15 - items: - - id: SC-01 - name: Plot Type - score: 5 - max: 5 - passed: true - comment: Correct horizontal tornado/sensitivity bar chart. - - id: SC-02 - name: Required Features - score: 4 - max: 4 - passed: true - comment: 'All required features present: base case line, sorted by range, - two colors, parameter labels, bar-end values.' - - id: SC-03 - name: Data Mapping - score: 3 - max: 3 - passed: true - comment: X-axis shows NPV output metric, y-axis shows input parameters. - - id: SC-04 - name: Title & Legend - score: 3 - max: 3 - passed: true - comment: Title format correct. Legend labels Low Scenario/High Scenario match - data. - data_quality: - score: 15 - max: 15 - items: - - id: DQ-01 - name: Feature Coverage - score: 6 - max: 6 - passed: true - comment: 10 parameters with varying ranges. Includes inverted relationships. - Full tornado shape. - - id: DQ-02 - name: Realistic Context - score: 5 - max: 5 - passed: true - comment: NPV sensitivity analysis for capital investment. Real financial modeling - parameters. Neutral business context. - - id: DQ-03 - name: Appropriate Scale - score: 4 - max: 4 - passed: true - comment: NPV range $82M-$152M with $120M base case realistic for capital investment. - code_quality: - score: 10 - max: 10 - items: - - id: CQ-01 - name: KISS Structure - score: 3 - max: 3 - passed: true - comment: Clean Imports-Data-Plot-Save flow. No functions or classes. - - id: CQ-02 - name: Reproducibility - score: 2 - max: 2 - passed: true - comment: np.random.seed(42) set, deterministic arrays. - - id: CQ-03 - name: Clean Imports - score: 2 - max: 2 - passed: true - comment: All four imports used. - - id: CQ-04 - name: Code Elegance - score: 2 - max: 2 - passed: true - comment: Clean and Pythonic. Long-form DataFrame for seaborn hue is correct - approach. - - id: CQ-05 - name: Output & API - score: 1 - max: 1 - passed: true - comment: Saves as plot.png with dpi=300, bbox_inches tight. Current API. - library_mastery: - score: 7 - max: 10 - items: - - id: LM-01 - name: Idiomatic Usage - score: 4 - max: 5 - passed: true - comment: Uses sns.barplot with long-form DataFrame and hue idiomatically. - Leverages set_style, set_context, despine. - - id: LM-02 - name: Distinctive Features - score: 3 - max: 5 - passed: true - comment: Uses set_context talk, set_style whitegrid with custom params, despine - left=True, dodge=False hue overlay. - verdict: APPROVED -impl_tags: - dependencies: [] - techniques: - - annotations - - manual-ticks - patterns: - - data-generation - - wide-to-long - dataprep: [] - styling: - - grid-styling + strengths: [] + weaknesses: []