diff --git a/plots/timeseries-forecast-uncertainty/implementations/python/bokeh.py b/plots/timeseries-forecast-uncertainty/implementations/python/bokeh.py index 5300c3c2c9..0b8ae31315 100644 --- a/plots/timeseries-forecast-uncertainty/implementations/python/bokeh.py +++ b/plots/timeseries-forecast-uncertainty/implementations/python/bokeh.py @@ -1,7 +1,7 @@ """ anyplot.ai timeseries-forecast-uncertainty: Time Series Forecast with Uncertainty Band Library: bokeh 3.9.0 | Python 3.13.13 -Quality: 92/100 | Updated: 2026-05-16 +Quality: 88/100 | Updated: 2026-05-19 """ import os @@ -11,7 +11,7 @@ import numpy as np import pandas as pd from bokeh.io import output_file, save -from bokeh.models import ColumnDataSource, Legend, Span +from bokeh.models import ColumnDataSource, HoverTool, Legend, Span from bokeh.plotting import figure from selenium import webdriver from selenium.webdriver.chrome.options import Options @@ -26,7 +26,6 @@ OKABE_ITO_1 = "#009E73" # Bluish green (brand) OKABE_ITO_2 = "#D55E00" # Vermillion -OKABE_ITO_3 = "#0072B2" # Blue OKABE_ITO_4 = "#CC79A7" # Reddish purple # Data - Monthly product sales with forecast @@ -58,9 +57,9 @@ # Create figure p = figure( - width=4800, - height=2700, - title="timeseries-forecast-uncertainty · bokeh · anyplot.ai", + width=3200, + height=1800, + title="timeseries-forecast-uncertainty · python · bokeh · anyplot.ai", x_axis_label="Date", y_axis_label="Sales (thousands)", x_axis_type="datetime", @@ -68,23 +67,23 @@ ) # Style title and axes -p.title.text_font_size = "28pt" +p.title.text_font_size = "18pt" p.title.text_color = INK -p.xaxis.axis_label_text_font_size = "22pt" -p.yaxis.axis_label_text_font_size = "22pt" +p.xaxis.axis_label_text_font_size = "14pt" +p.yaxis.axis_label_text_font_size = "14pt" p.xaxis.axis_label_text_color = INK p.yaxis.axis_label_text_color = INK -p.xaxis.major_label_text_font_size = "18pt" -p.yaxis.major_label_text_font_size = "18pt" +p.xaxis.major_label_text_font_size = "12pt" +p.yaxis.major_label_text_font_size = "12pt" p.xaxis.major_label_text_color = INK_SOFT p.yaxis.major_label_text_color = INK_SOFT -# Background and outline +# Background — outline=None removes the enclosing box (top/right spines equivalent) p.background_fill_color = PAGE_BG p.border_fill_color = PAGE_BG -p.outline_line_color = INK_SOFT +p.outline_line_color = None -# Axis styling +# Axis styling — keep left and bottom lines (L-shaped frame) p.xaxis.axis_line_color = INK_SOFT p.yaxis.axis_line_color = INK_SOFT p.xaxis.axis_line_width = 2 @@ -108,17 +107,26 @@ ) band_80 = p.patch(x="x", y="y", source=source_80, fill_color=OKABE_ITO_4, fill_alpha=0.30, line_color=None) -# Historical data line (solid) +# Historical data line (solid) with hover data source_hist = ColumnDataSource(data={"x": dates_hist, "y": actual}) -hist_line = p.line(x="x", y="y", source=source_hist, line_color=OKABE_ITO_1, line_width=4) - -# Forecast line (dashed) -source_forecast = ColumnDataSource(data={"x": dates_forecast, "y": forecast}) -forecast_line = p.line(x="x", y="y", source=source_forecast, line_color=OKABE_ITO_2, line_width=4, line_dash="dashed") +hist_line = p.line(x="x", y="y", source=source_hist, line_color=OKABE_ITO_1, line_width=3) + +# Forecast line (dashed) with CI data for hover tooltip +source_forecast = ColumnDataSource( + data={ + "x": dates_forecast, + "y": forecast, + "lower_80": lower_80, + "upper_80": upper_80, + "lower_95": lower_95, + "upper_95": upper_95, + } +) +forecast_line = p.line(x="x", y="y", source=source_forecast, line_color=OKABE_ITO_2, line_width=3, line_dash="dashed") -# Connection line from last historical to first forecast +# Connection line from last historical point to first forecast point source_connect = ColumnDataSource(data={"x": [dates_hist[-1], dates_forecast[0]], "y": [actual[-1], forecast[0]]}) -p.line(x="x", y="y", source=source_connect, line_color=OKABE_ITO_2, line_width=4, line_dash="dashed") +p.line(x="x", y="y", source=source_connect, line_color=OKABE_ITO_2, line_width=3, line_dash="dashed") # Vertical line at forecast start forecast_start = Span( @@ -126,6 +134,29 @@ ) p.add_layout(forecast_start) +# HoverTool for historical data +hover_hist = HoverTool( + renderers=[hist_line], + tooltips=[("Date", "@x{%b %Y}"), ("Sales", "@y{0.0}k")], + formatters={"@x": "datetime"}, + mode="vline", +) +p.add_tools(hover_hist) + +# HoverTool for forecast with confidence intervals +hover_forecast = HoverTool( + renderers=[forecast_line], + tooltips=[ + ("Date", "@x{%b %Y}"), + ("Forecast", "@y{0.0}k"), + ("80% CI", "[@lower_80{0.0}, @upper_80{0.0}]k"), + ("95% CI", "[@lower_95{0.0}, @upper_95{0.0}]k"), + ], + formatters={"@x": "datetime"}, + mode="vline", +) +p.add_tools(hover_forecast) + # Legend legend = Legend( items=[ @@ -137,7 +168,7 @@ location="top_left", ) -legend.label_text_font_size = "18pt" +legend.label_text_font_size = "12pt" legend.label_text_color = INK_SOFT legend.background_fill_color = ELEVATED_BG legend.background_fill_alpha = 0.95 @@ -149,7 +180,7 @@ legend.glyph_height = 20 p.add_layout(legend) -# Set y-axis range +# Set y-axis range with room for confidence bands p.y_range.start = 55 p.y_range.end = 175 @@ -158,7 +189,7 @@ save(p) # Screenshot with headless Chrome -W, H = 4800, 2700 +W, H = 3200, 1800 opts = Options() for arg in ( "--headless=new", diff --git a/plots/timeseries-forecast-uncertainty/metadata/python/bokeh.yaml b/plots/timeseries-forecast-uncertainty/metadata/python/bokeh.yaml index 47b7476df8..24b05e7a32 100644 --- a/plots/timeseries-forecast-uncertainty/metadata/python/bokeh.yaml +++ b/plots/timeseries-forecast-uncertainty/metadata/python/bokeh.yaml @@ -2,9 +2,9 @@ library: bokeh language: python specification_id: timeseries-forecast-uncertainty created: '2026-01-07T16:30:50Z' -updated: '2026-05-16T22:29:32Z' -generated_by: claude-haiku -workflow_run: 25974524041 +updated: '2026-05-19T13:41:28Z' +generated_by: claude-sonnet +workflow_run: 26099181190 issue: 3188 language_version: 3.13.13 library_version: 3.9.0 @@ -12,87 +12,101 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/timeserie preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/timeseries-forecast-uncertainty/python/bokeh/plot-dark.png preview_html_light: https://storage.googleapis.com/anyplot-images/plots/timeseries-forecast-uncertainty/python/bokeh/plot-light.html preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/timeseries-forecast-uncertainty/python/bokeh/plot-dark.html -quality_score: 92 +quality_score: 88 review: strengths: - - Complete spec compliance with all required features present and correctly implemented - - Excellent visual quality with proper theme adaptation to both light and dark modes - - Idiomatic Bokeh usage with ColumnDataSource, figure, patch, Span, and Legend patterns - - Clean code structure with deterministic data generation and proper imports - - Professional styling with proper font sizes, color tokens, spacing, and typography - hierarchy - - Correct Okabe-Ito palette implementation maintaining color identity across themes - - Nested confidence bands with appropriate opacity creating clear visual distinction + - Full spec compliance — all required features (historical line, forecast, 80%/95% + CI bands, vertical divider, legend) implemented correctly + - Perfect data quality — realistic monthly sales scenario with plausible trend, + seasonality, and growing forecast uncertainty + - 'Proper Bokeh idioms: ColumnDataSource throughout, Span for the divider, dual + HoverTools with datetime formatters and CI data' + - Theme-adaptive chrome implemented correctly in both renders — no dark-on-dark + failures + - Nested CI band layering (95% drawn first, 80% on top) creates correct visual nesting weaknesses: - - Limited use of distinctive Bokeh interactive features such as HoverTool or callbacks - - Standard spine treatment could be enhanced by removing top and right spines for - visual refinement + - Y-axis range (55–175) is wider than needed for data range (~75–165), leaving ~30% + of vertical canvas as empty whitespace; tighten to ~70–170 or compute from min(lower_95)-10 + - CI bands reach the right canvas edge with no x-axis padding, giving a slightly + clipped appearance; add a small x-range buffer after the last forecast date + - 'Dashed forecast line (3px, #D55E00) becomes hard to see inside the confidence + band fill — increase to line_width=4 or use lighter CI band alpha' + - major_tick_line_color not explicitly set for either axis; set p.xaxis.major_tick_line_color + = INK_SOFT and p.yaxis.major_tick_line_color = INK_SOFT + - Bokeh native Band annotation model would be more idiomatic than the patch-polygon + workaround for CI regions image_description: |- Light render (plot-light.png): - Background: Warm off-white (#FAF8F1) - correct theme background - Chrome: Title "timeseries-forecast-uncertainty · bokeh · anyplot.ai" in dark text; axis labels "Date" and "Sales (thousands)" clearly visible; all tick labels readable in dark INK_SOFT color - Data: Solid bluish-green line (#009E73) for historical data; dashed vermillion line (#D55E00) for forecast; nested reddish-purple bands (α=0.30 for 80%, α=0.15 for 95%); connection line from last historical to first forecast - Legend: Clearly identifies all four elements with legible text on elevated background (#FFFDF6) - Legibility verdict: PASS - All elements clearly visible and readable on light background + Background: Warm off-white (#FAF8F1) — correct theme surface. + Chrome: Title "timeseries-forecast-uncertainty · python · bokeh · anyplot.ai" in dark ink (INK token) — readable. Y-axis label "Sales (thousands)" in dark ink — readable. X-axis label "Date" in dark ink — readable. Y-axis tick labels (80, 100, 120, 140, 160) in INK_SOFT — readable. X-axis datetime tick labels present but sparse and small. + Data: Historical line = #009E73 (Okabe-Ito position 1, brand green), solid, 3px — clearly visible. Forecast line = #D55E00 (OI position 2, vermillion), dashed, 3px — visible but partially obscured within CI band fills. 80% CI band = #CC79A7 at fill_alpha=0.30 (darker peach tone). 95% CI band = #CC79A7 at fill_alpha=0.15 (lighter peach tone). Vertical dashed divider at forecast start — visible. Legend in top-left: 4 items readable. Upper y-axis region (~145–175) is largely empty (wasted vertical space). Right edge of CI bands meets canvas boundary with minimal padding. + Legibility verdict: PASS — all text readable, no overflow, no light-on-light failures. Dark render (plot-dark.png): - Background: Warm near-black (#1A1A17) - correct theme background - Chrome: Title in light text (#F0EFE8); axis labels readable in light text; tick labels in light INK_SOFT (#B8B7B0) with excellent contrast against dark background - no dark-on-dark failures - Data: Identical colors to light render - bluish-green historical line, vermillion forecast line, reddish-purple confidence bands with same opacity values - Legend: Light text on elevated dark background (#242420) is clearly readable - Legibility verdict: PASS - All text readable with proper theme contrast; no dark-on-dark failures detected + Background: Warm near-black (#1A1A17) — correct dark theme surface. + Chrome: Title in light ink (INK token = #F0EFE8) — readable against dark background. Y-axis label and axis labels in light ink — readable. Tick labels in INK_SOFT (#B8B7B0) — readable. Legend background in ELEVATED_BG (#242420) with INK_SOFT label text — readable. No dark-on-dark failures observed. + Data: Historical line = #009E73 — identical to light render as required (data colors constant between themes). Forecast dashed line = #D55E00 — visible. CI bands take on darker brownish-orange appearance due to alpha blending over near-black background (expected behavior with alpha-blended fills). Both CI bands remain visually distinguishable from each other and from the background. + Legibility verdict: PASS — all text elements are light-colored against dark surface, no dark-on-dark failures. criteria_checklist: visual_quality: - score: 30 + score: 27 max: 30 items: - id: VQ-01 name: Text Legibility - score: 8 + score: 7 max: 8 passed: true - comment: All text explicitly sized; readable in both themes with proper color - tokens + comment: 'All font sizes explicitly set (title 18pt, axis labels 14pt, ticks + 12pt, legend 12pt). Good proportions. Minor: x-axis datetime tick labels + sparse and potentially small at mobile scale.' - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping elements; legend, lines, bands, labels all clearly - separated + comment: No overlapping text or data collisions. Legend sits cleanly without + intersecting data. - id: VQ-03 name: Element Visibility - score: 6 + score: 5 max: 6 passed: true - comment: Confidence bands properly opacity-blended; line widths and sizes - appropriate + comment: 'Lines and bands visible. Minor: dashed forecast line partially obscured + within CI band fills due to alpha overlap; increase line_width or reduce + band alpha.' - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Okabe-Ito palette used correctly; colorblind-safe + comment: Okabe-Ito palette is colorblind-safe. Green/orange combination provides + adequate luminance contrast. - id: VQ-05 name: Layout & Canvas - score: 4 + score: 3 max: 4 passed: true - comment: 4800×2700 px well-proportioned; generous margins and padding + comment: 'Good horizontal fill. Deduction: y-range 55–175 vs data range ~75–165 + leaves ~30% vertical whitespace. CI bands reach right canvas edge with no + padding.' - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: Descriptive labels with units; title follows spec format + comment: 'X-axis: ''Date''. Y-axis: ''Sales (thousands)'' with units. Title + correctly formatted.' - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First series #009E73; backgrounds correct; both themes correct' + comment: 'First series #009E73, second #D55E00. CI bands use OI position 4 + (#CC79A7) — intentional design grouping. Background #FAF8F1/#1A1A17 correct. + Chrome fully theme-adaptive.' design_excellence: - score: 14 + score: 13 max: 20 items: - id: DE-01 @@ -100,21 +114,25 @@ review: score: 5 max: 8 passed: true - comment: Good use of nested confidence bands; professional polish; not maximally - sophisticated + comment: 'Above baseline: distinct color coding for historical vs forecast, + nested CI bands with alpha layering, clean L-shaped spine removal, theme-adaptive + tokens. Not publication-ready.' - id: DE-02 name: Visual Refinement score: 4 max: 6 passed: true - comment: Subtle grid; refined legend styling; could benefit from spine removal + comment: 'Y-axis-only grid at 10% opacity. Top/right spines removed. Legend + styled with elevated background. Deduction: wasted vertical whitespace, + missing major_tick_line_color.' - id: DE-03 name: Data Storytelling - score: 5 + score: 4 max: 6 passed: true - comment: Clear focal point with forecast bands; effective visual hierarchy - through dashing and color + comment: Vertical divider marks regime change, growing CI bands convey increasing + uncertainty, green→orange shift distinguishes observed from projected. Clear + narrative without annotation. spec_compliance: score: 15 max: 15 @@ -124,26 +142,29 @@ review: score: 5 max: 5 passed: true - comment: Correct time series forecast plot with uncertainty + comment: 'Correct: time series with historical period, forecast projection, + and nested 80%/95% confidence bands.' - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: 'All features present: historical, forecast, confidence bands, vertical - line marker' + comment: 'All spec features: solid historical line, dashed forecast, vertical + forecast-start marker, 80% CI, 95% CI, legend.' - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: X/Y correct; axes show all data appropriately + comment: Date on x-axis, Sales (thousands) on y-axis. All 48 data points correctly + mapped. - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Correct title format; legend labels match spec + comment: 'Title: ''timeseries-forecast-uncertainty · python · bokeh · anyplot.ai''. + Legend: Historical Data, Forecast, 80% CI, 95% CI — all correct.' data_quality: score: 15 max: 15 @@ -153,19 +174,22 @@ review: score: 6 max: 6 passed: true - comment: 'Shows all aspects: trend, seasonality, forecast uncertainty growth' + comment: 'Shows all aspects: trend, seasonality in history, growing uncertainty + width over forecast horizon, two CI levels, smooth handoff at boundary.' - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Monthly sales data realistic and neutral + comment: Monthly product sales with 3-year history and 12-month forecast — + real, neutral, business-relevant scenario. - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Values (80-175 thousands) sensible for domain + comment: Sales 80–140k, uncertainty ±5–25k growing over forecast. Seasonal + amplitude ~15k. All proportionally realistic. code_quality: score: 10 max: 10 @@ -175,31 +199,35 @@ review: score: 3 max: 3 passed: true - comment: Simple linear script; no unnecessary functions + comment: 'Flat: theme tokens → data generation → figure + styling → glyphs + → legend → save. No functions or classes.' - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Seed set with np.random.seed(42) + comment: np.random.seed(42) set. - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports used; no extraneous dependencies + comment: 'All imports used: ColumnDataSource, HoverTool, Legend, Span all + referenced.' - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Appropriate complexity; no fake functionality + comment: Idiomatic patch-polygon approach for CI bands. HoverTool with datetime + formatters clean and well-parameterized. - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Correctly saves as plot-{THEME}.html and .png + comment: Saves plot-{THEME}.html + plot-{THEME}.png via Selenium screenshot. + Current Bokeh 3.x API. library_mastery: score: 8 max: 10 @@ -209,20 +237,25 @@ review: score: 5 max: 5 passed: true - comment: 'Idiomatic Bokeh: ColumnDataSource, figure, patch, Span, Legend' + comment: ColumnDataSource for all renderers, Span for vertical line, manual + Legend with explicit items, HoverTool with formatters and mode=vline. Patch-polygon + for CI bands is correct Bokeh idiom. - id: LM-02 name: Distinctive Features score: 3 max: 5 passed: true - comment: Good use of Span and nested patch; could add HoverTool for interactivity + comment: Dual HoverTools with CI data formatted for forecast line is Bokeh-distinctive. + HTML export leverages Bokeh's interactivity advantage. Could use native + Band model instead of patch-polygon for more idiomatic approach. verdict: APPROVED impl_tags: dependencies: - selenium techniques: - - custom-legend + - hover-tooltips - html-export + - custom-legend patterns: - data-generation - columndatasource @@ -230,4 +263,3 @@ impl_tags: - time-series styling: - alpha-blending - - grid-styling