diff --git a/plots/timeseries-forecast-uncertainty/implementations/python/seaborn.py b/plots/timeseries-forecast-uncertainty/implementations/python/seaborn.py index 79ade74284..7970c2c235 100644 --- a/plots/timeseries-forecast-uncertainty/implementations/python/seaborn.py +++ b/plots/timeseries-forecast-uncertainty/implementations/python/seaborn.py @@ -1,7 +1,7 @@ """ anyplot.ai timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band Library: seaborn 0.13.2 | Python 3.13.13 -Quality: 90/100 | Updated: 2026-05-16 +Quality: 90/100 | Updated: 2026-05-19 """ import os @@ -10,6 +10,8 @@ import numpy as np import pandas as pd import seaborn as sns +from matplotlib.lines import Line2D +from matplotlib.patches import Patch # Theme tokens @@ -21,45 +23,31 @@ # Okabe-Ito palette OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442"] -COLOR_HISTORICAL = OKABE_ITO[0] # Green -COLOR_FORECAST = OKABE_ITO[1] # Vermillion +COLOR_HISTORICAL = OKABE_ITO[0] +COLOR_FORECAST = OKABE_ITO[1] -# Set seed for reproducibility np.random.seed(42) -# Generate stock price time series with forecast -n_historical = 60 # 60 trading days (~3 months) -n_forecast = 20 # 20 trading days (~4 weeks) -n_total = n_historical + n_forecast +# Data — stock price with ~3-month history and 4-week forecast +n_historical = 60 +n_forecast = 20 +dates = pd.date_range(start="2025-01-01", periods=n_historical + n_forecast, freq="B") -dates = pd.date_range(start="2025-01-01", periods=n_total, freq="B") # Business days - -# Generate historical stock prices with trend and volatility t = np.arange(n_historical) -base_price = 150 -trend = 0.15 * t # Slight upward trend -volatility = 2.5 * np.sin(2 * np.pi * t / 20) # 20-day cycles -noise = np.random.normal(0, 1.5, n_historical) -historical_prices = base_price + trend + volatility + noise - -# Generate forecast with increasing uncertainty -t_forecast = np.arange(n_historical, n_total) -trend_forecast = base_price + 0.15 * t_forecast -seasonality_forecast = 2.5 * np.sin(2 * np.pi * t_forecast / 20) -forecast_prices = trend_forecast + seasonality_forecast - -# Confidence intervals widen over forecast horizon -forecast_horizon = np.arange(1, n_forecast + 1) -std_base = 1.5 -std_growth = std_base * np.sqrt(forecast_horizon) +historical_prices = 150 + 0.15 * t + 2.5 * np.sin(2 * np.pi * t / 20) + np.random.normal(0, 1.5, n_historical) + +t_fc = np.arange(n_historical, n_historical + n_forecast) +forecast_prices = 150 + 0.15 * t_fc + 2.5 * np.sin(2 * np.pi * t_fc / 20) +horizon = np.arange(1, n_forecast + 1) +std_growth = 1.5 * np.sqrt(horizon) lower_95 = forecast_prices - 1.96 * std_growth upper_95 = forecast_prices + 1.96 * std_growth lower_80 = forecast_prices - 1.28 * std_growth upper_80 = forecast_prices + 1.28 * std_growth -# Create DataFrame -df = pd.DataFrame( +# Wide-form for CI bands; long-form for seaborn's data-aware lineplot +df_wide = pd.DataFrame( { "date": dates, "actual": list(historical_prices) + [np.nan] * n_forecast, @@ -71,7 +59,14 @@ } ) -# Configure seaborn theme with theme-adaptive colors +long_data = pd.concat( + [ + df_wide[["date", "actual"]].rename(columns={"actual": "price"}).assign(series="Historical"), + df_wide[["date", "forecast"]].rename(columns={"forecast": "price"}).assign(series="Forecast"), + ] +).dropna() + +# Configure seaborn theme sns.set_theme( style="ticks", rc={ @@ -89,52 +84,60 @@ }, ) -# Create figure +# Plot fig, ax = plt.subplots(figsize=(16, 9), facecolor=PAGE_BG) ax.set_facecolor(PAGE_BG) -# Plot confidence intervals (95% lighter, 80% darker) -ax.fill_between(df["date"], df["lower_95"], df["upper_95"], alpha=0.15, color=COLOR_FORECAST, label="95% Confidence") -ax.fill_between(df["date"], df["lower_80"], df["upper_80"], alpha=0.25, color=COLOR_FORECAST, label="80% Confidence") - -# Plot historical data -ax.plot(df["date"], df["actual"], color=COLOR_HISTORICAL, linewidth=3, label="Historical", zorder=3) - -# Plot forecast with dashed line -ax.plot( - df[df["forecast"].notna()]["date"], - df[df["forecast"].notna()]["forecast"], - color=COLOR_FORECAST, +# Confidence interval bands (95% lightest, 80% more opaque — nested) +ax.fill_between(df_wide["date"], df_wide["lower_95"], df_wide["upper_95"], alpha=0.15, color=COLOR_FORECAST) +ax.fill_between(df_wide["date"], df_wide["lower_80"], df_wide["upper_80"], alpha=0.28, color=COLOR_FORECAST) + +# Seaborn lineplot — idiomatic long-form API with hue + style + dashes +sns.lineplot( + data=long_data, + x="date", + y="price", + hue="series", + style="series", + palette={"Historical": COLOR_HISTORICAL, "Forecast": COLOR_FORECAST}, + dashes={"Historical": (1, 0), "Forecast": (6, 2)}, linewidth=3, - linestyle="--", - label="Forecast", - zorder=3, + ax=ax, + legend=False, ) -# Add vertical line at forecast start -forecast_start = dates[n_historical - 1] -ax.axvline(x=forecast_start, color=INK_SOFT, linestyle=":", linewidth=1.5, alpha=0.5) +# Forecast boundary marker +ax.axvline(x=dates[n_historical - 1], color=INK_SOFT, linestyle=":", linewidth=1.5, alpha=0.5) -# Styling +# Style ax.set_title( - "timeseries-forecast-uncertainty · seaborn · anyplot.ai", fontsize=24, fontweight="medium", color=INK, pad=20 + "timeseries-forecast-uncertainty · python · seaborn · anyplot.ai", + fontsize=24, + fontweight="medium", + color=INK, + pad=20, ) ax.set_xlabel("Date", fontsize=20, color=INK) ax.set_ylabel("Stock Price ($)", fontsize=20, color=INK) ax.tick_params(axis="both", labelsize=16, colors=INK_SOFT) -# Remove top and right spines -ax.spines["top"].set_visible(False) -ax.spines["right"].set_visible(False) +sns.despine(ax=ax) ax.spines["left"].set_color(INK_SOFT) ax.spines["bottom"].set_color(INK_SOFT) -# Subtle grid on y-axis only ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK) ax.set_axisbelow(True) -# Legend -ax.legend(fontsize=16, loc="upper left", framealpha=1.0, fancybox=False, edgecolor=INK_SOFT) +# Combined legend: line handles + CI band patches +legend_elements = [ + Line2D([0], [0], color=COLOR_HISTORICAL, linewidth=3, label="Historical"), + Line2D([0], [0], color=COLOR_FORECAST, linewidth=3, linestyle=(0, (6, 2)), label="Forecast"), + Patch(facecolor=COLOR_FORECAST, alpha=0.28, label="80% Confidence"), + Patch(facecolor=COLOR_FORECAST, alpha=0.15, label="95% Confidence"), +] +ax.legend(handles=legend_elements, fontsize=16, loc="upper left", framealpha=1.0, fancybox=False, edgecolor=INK_SOFT) plt.tight_layout() + +# Save plt.savefig(f"plot-{THEME}.png", dpi=300, bbox_inches="tight", facecolor=PAGE_BG) diff --git a/plots/timeseries-forecast-uncertainty/metadata/python/seaborn.yaml b/plots/timeseries-forecast-uncertainty/metadata/python/seaborn.yaml index 48dca0d764..70bd4161ad 100644 --- a/plots/timeseries-forecast-uncertainty/metadata/python/seaborn.yaml +++ b/plots/timeseries-forecast-uncertainty/metadata/python/seaborn.yaml @@ -2,9 +2,9 @@ library: seaborn language: python specification_id: timeseries-forecast-uncertainty created: '2026-01-07T16:28:34Z' -updated: '2026-05-16T22:34:02Z' -generated_by: claude-haiku -workflow_run: 25974439026 +updated: '2026-05-19T13:32:03Z' +generated_by: claude-sonnet +workflow_run: 26099068645 issue: 3188 language_version: 3.13.13 library_version: 0.13.2 @@ -15,89 +15,85 @@ preview_html_dark: null quality_score: 90 review: strengths: - - Perfect visual quality across all dimensions (text legibility, layout, palette - compliance, no overlap) - - Excellent theme adaptation with proper chrome switching while maintaining data - color consistency - - Strong visual storytelling through confidence band widening and color hierarchy - - Robust code structure with proper reproducibility and clean imports - - All spec requirements met comprehensively - - Realistic, well-chosen example data demonstrating the plot type effectively - - Proper use of seaborn theme system for systematic color management + - Idiomatic seaborn long-form API with hue+style+dashes for clean multi-series line + styling + - 'Full theme-adaptive chrome: all tokens correctly set for both light and dark + renders' + - 'All spec requirements met: dual CI bands (80%+95%), forecast boundary marker, + distinct line styles, comprehensive legend' + - Realistic self-contained data generation with widening CI bands over forecast + horizon (±1.96σ√t) + - Clean code structure with proper seed, KISS linear flow, and all imports used weaknesses: - - Design excellence could be elevated through more distinctive library-specific - features or advanced visualization techniques + - Axis label fontsize=20 slightly disproportionate for short labels 'Date' and 'Stock + Price ($)' on the 4800x2700 canvas — reduce to ~14pt + - CI band visual separation slightly compressed in dark render — increase alphas + to 0.20 (95%) and 0.35 (80%) for better differentiation + - Canvas is 4800x2700 (dpi=300, figsize=(16,9)) — exceeds standard 3200x1800; use + dpi=200 or adjust figsize to match standard pixel target image_description: |- Light render (plot-light.png): - Background: Warm off-white (#FAF8F1) - correct, not pure white - Chrome: Title (24pt), axis labels (20pt), tick labels (16pt) all in dark text and clearly readable - Data: Historical line in green (#009E73), forecast line in orange dashed (#D55E00), confidence bands in orange with nested alpha (0.25 for 80%, 0.15 for 95%) - Grid: Subtle y-axis only with low alpha (0.15) - Legend: Clean and well-positioned in upper left with descriptive labels - Spines: L-shaped (top/right removed) - Legibility verdict: PASS - all elements readable + Background: Warm off-white #FAF8F1 — correct light surface token + Chrome: Title "timeseries-forecast-uncertainty · python · seaborn · anyplot.ai" in dark ink, readable. Axis labels "Date" (x) and "Stock Price ($)" (y) in dark INK. Tick labels in INK_SOFT. All text clearly visible against light background. + Data: Historical line in #009E73 (brand green, Okabe-Ito position 1) as solid line. Forecast in #D55E00 (vermillion, Okabe-Ito position 2) as dashed line. Two nested CI bands in same vermillion family (80% alpha=0.28, 95% alpha=0.15). Vertical dotted boundary line at forecast start. + Legibility verdict: PASS Dark render (plot-dark.png): - Background: Warm near-black (#1A1A17) - correct, not pure black - Chrome: Title, axis labels, tick labels now in light text (#F0EFE8 and #B8B7B0) and clearly readable against dark background - Data: Historical green (#009E73) and forecast orange (#D55E00) colors are identical to light render - only chrome has changed - Grid: Subtle gridlines properly styled - Vertical line: Dotted line at forecast boundary clearly visible - Legibility verdict: PASS - no dark-on-dark failures, all text light-colored and readable, brand green remains clearly visible - - Both renders demonstrate perfect theme adaptation: data colors constant, chrome properly theme-dependent. + Background: Warm near-black #1A1A17 — correct dark surface token + Chrome: Title, axis labels, and tick labels render in light text (F0EFE8/B8B7B0) — all readable against dark background. Legend background uses elevated dark token #242420. No dark-on-dark text failures observed. + Data: Historical green line and forecast orange line identical in color to light render (only chrome flips). CI bands appear as dark brownish-orange (95%) and brighter orange (80%) — distinguishable though slightly compressed contrast compared to light render. + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 30 + score: 29 max: 30 items: - id: VQ-01 name: Text Legibility - score: 8 + score: 7 max: 8 passed: true - comment: All font sizes explicitly set (24pt, 20pt, 16pt); perfectly readable - in both light and dark themes + comment: All font sizes explicitly set (title=24, labels=20, ticks=16); proportional + for large canvas but axis labels slightly generous for short labels - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping elements; legend well-positioned; date labels properly - spaced + comment: No overlapping text or data elements in either render - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Historical line (3px), forecast line (3px dashed), confidence bands - (alpha-blended) optimally visible + comment: Lines and CI bands well-sized and clearly visible; appropriate alpha + for overlapping bands - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Okabe-Ito palette (green vs. orange) provides excellent contrast; - CVD-safe + comment: Okabe-Ito green/vermillion pair is CVD-safe; series also distinguished + by line style - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Plot fills 60-70% of canvas with balanced margins; no wasted space + comment: Good canvas utilization, balanced margins, tight_layout applied - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: 'Descriptive labels with units: ''Date'' and ''Stock Price ($)''' + comment: Y-axis has units (Stock Price ($)), X-axis is Date - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First series #009E73, second #D55E00; backgrounds #FAF8F1 light - / #1A1A17 dark; data colors identical in both themes' + comment: 'First series #009E73, second #D55E00 (Okabe-Ito order); backgrounds + #FAF8F1/#1A1A17; all chrome tokens theme-adaptive' design_excellence: score: 13 max: 20 @@ -107,22 +103,24 @@ review: score: 5 max: 8 passed: true - comment: Intentional color hierarchy with nested alpha-blended confidence - bands; professional polish above defaults + comment: 'Above defaults: two-color Okabe-Ito scheme, solid/dashed distinction, + nested CI bands, vertical boundary marker, clean legend. Not publication-ready + — CI band differentiation weak in dark render' - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: L-shaped spines, subtle y-axis grid, generous whitespace, refined - legend; strong attention to detail + comment: Spines despined, subtle y-axis-only grid (alpha=0.15), elevated legend + background, fancybox=False - id: DE-03 name: Data Storytelling score: 4 max: 6 passed: true - comment: Visual hierarchy with solid vs. dashed lines; band widening tells - story of increasing uncertainty + comment: Growing uncertainty bands communicate forecast uncertainty naturally; + vertical divider clearly marks transition; color+linestyle reinforcement + makes narrative immediately readable spec_compliance: score: 15 max: 15 @@ -132,26 +130,28 @@ review: score: 5 max: 5 passed: true - comment: 'Correct: time series with forecast and dual confidence intervals' + comment: Correct time-series forecast with uncertainty; historical line, forecast + line, dual CI bands all present - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All features present: historical, forecast, 80%/95% bands, boundary - marker, legend' + comment: Solid historical line, dashed forecast, 80%+95% CI bands, vertical + boundary marker, semi-transparent fills, comprehensive legend - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: Date and stock price correctly mapped; all data visible + comment: Date on x-axis, stock price on y-axis, all data periods visible - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title format correct; legend labels match data + comment: 'Title exact format; legend labels: Historical, Forecast, 80% Confidence, + 95% Confidence' data_quality: score: 15 max: 15 @@ -161,21 +161,21 @@ review: score: 6 max: 6 passed: true - comment: 'Shows all aspects: trend, volatility, forecast, dual confidence - levels, boundary condition' + comment: 'All aspects shown: trending historical with noise, forecast continuation, + widening CI bands, clear transition' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Stock price forecasting is real-world, neutral, plausible scenario + comment: Stock price forecasting — neutral, comprehensible real-world scenario - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Realistic prices ($145-175); correct business day frequency; mathematically - correct CI expansion + comment: ~$150 stock with gentle upward trend; CI widths grow proportionally + (±1.96σ√t) — all values plausible code_quality: score: 10 max: 10 @@ -185,33 +185,35 @@ review: score: 3 max: 3 passed: true - comment: Linear flow; no unnecessary functions/classes + comment: 'Linear flow: imports → tokens → data → theme → plot → style → legend + → save; no functions or classes' - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: np.random.seed(42) set; deterministic + comment: np.random.seed(42) set - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: 'All imports used: os, matplotlib, numpy, pandas, seaborn' + comment: 'All 7 imports used: os, plt, np, pd, sns, Line2D, Patch' - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean, Pythonic, well-organized; no fake functionality + comment: Clean, Pythonic; wide-form for CI bands + long-form for seaborn lineplot + is sensible separation - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Correct output format (plot-{THEME}.png); current API + comment: Saves as plot-{THEME}.png; current seaborn/matplotlib API library_mastery: - score: 7 + score: 8 max: 10 items: - id: LM-01 @@ -219,14 +221,16 @@ review: score: 5 max: 5 passed: true - comment: Expertly uses sns.set_theme(), axes-level API, proper legend handling + comment: 'Exemplary seaborn: long-form data with hue+style+dashes dict; sns.set_theme() + with rc; sns.despine()' - id: LM-02 name: Distinctive Features - score: 2 + score: 3 max: 5 - passed: false - comment: Uses seaborn theme system and matplotlib fill_between(); solid but - somewhat standard + passed: true + comment: Seaborn-distinctive dashes parameter for per-hue line styles and + theme/rc system; CI bands use matplotlib fill_between (correct — seaborn + CI is for statistical aggregates) verdict: APPROVED impl_tags: dependencies: [] @@ -234,6 +238,7 @@ impl_tags: - custom-legend patterns: - data-generation + - wide-to-long - explicit-figure dataprep: - time-series