diff --git a/plots/timeseries-decomposition/implementations/python/bokeh.py b/plots/timeseries-decomposition/implementations/python/bokeh.py index 7f9ab41c28..ba23146466 100644 --- a/plots/timeseries-decomposition/implementations/python/bokeh.py +++ b/plots/timeseries-decomposition/implementations/python/bokeh.py @@ -1,18 +1,34 @@ -""" pyplots.ai +""" anyplot.ai timeseries-decomposition: Time Series Decomposition Plot -Library: bokeh 3.8.1 | Python 3.13.11 -Quality: 91/100 | Created: 2025-12-31 +Library: bokeh 3.9.0 | Python 3.13.13 +Quality: 86/100 | Updated: 2026-05-14 """ +import os +import time +from pathlib import Path + import numpy as np import pandas as pd -from bokeh.io import export_png +from bokeh.io import output_file, save from bokeh.layouts import column -from bokeh.models import Title +from bokeh.models import ColumnDataSource from bokeh.plotting import figure +from selenium import webdriver +from selenium.webdriver.chrome.options import Options from statsmodels.tsa.seasonal import seasonal_decompose +# Theme tokens +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" + +# Okabe-Ito palette for visual distinction of components +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"] # Positions 1-4 + # Data - Monthly airline passengers (classic time series dataset) np.random.seed(42) date_range = pd.date_range(start="2018-01-01", periods=120, freq="MS") # 10 years monthly @@ -35,88 +51,110 @@ seasonal_component = decomposition.seasonal.values residual_component = decomposition.resid.values -# Color scheme (Python Blue primary) -line_color = "#306998" - -# Create subplots (4800 x 2700 total, divided into 4 panels) -panel_height = 620 - -# Panel 1: Original Series -p1 = figure(width=4800, height=panel_height, x_axis_type="datetime") -p1.line(dates, original, line_width=4, color=line_color) -p1.title.text = "Original Series" -p1.title.text_font_size = "26pt" -p1.yaxis.axis_label = "Passengers (thousands)" -p1.yaxis.axis_label_text_font_size = "22pt" -p1.xaxis.major_label_text_font_size = "18pt" -p1.yaxis.major_label_text_font_size = "18pt" -p1.xgrid.grid_line_alpha = 0.3 -p1.ygrid.grid_line_alpha = 0.3 -p1.xgrid.grid_line_dash = "dashed" -p1.ygrid.grid_line_dash = "dashed" -p1.outline_line_color = None -p1.xaxis.visible = False -p1.min_border_left = 120 - -# Panel 2: Trend Component -p2 = figure(width=4800, height=panel_height, x_axis_type="datetime", x_range=p1.x_range) -p2.line(dates, trend_component, line_width=4, color=line_color) -p2.title.text = "Trend Component" -p2.title.text_font_size = "26pt" -p2.yaxis.axis_label = "Trend" -p2.yaxis.axis_label_text_font_size = "22pt" -p2.xaxis.major_label_text_font_size = "18pt" -p2.yaxis.major_label_text_font_size = "18pt" -p2.xgrid.grid_line_alpha = 0.3 -p2.ygrid.grid_line_alpha = 0.3 -p2.xgrid.grid_line_dash = "dashed" -p2.ygrid.grid_line_dash = "dashed" -p2.outline_line_color = None -p2.xaxis.visible = False -p2.min_border_left = 120 - -# Panel 3: Seasonal Component -p3 = figure(width=4800, height=panel_height, x_axis_type="datetime", x_range=p1.x_range) -p3.line(dates, seasonal_component, line_width=4, color=line_color) -p3.title.text = "Seasonal Component" -p3.title.text_font_size = "26pt" -p3.yaxis.axis_label = "Seasonal" -p3.yaxis.axis_label_text_font_size = "22pt" -p3.xaxis.major_label_text_font_size = "18pt" -p3.yaxis.major_label_text_font_size = "18pt" -p3.xgrid.grid_line_alpha = 0.3 -p3.ygrid.grid_line_alpha = 0.3 -p3.xgrid.grid_line_dash = "dashed" -p3.ygrid.grid_line_dash = "dashed" -p3.outline_line_color = None -p3.xaxis.visible = False -p3.min_border_left = 120 - -# Panel 4: Residual Component -p4 = figure(width=4800, height=panel_height, x_axis_type="datetime", x_range=p1.x_range) -p4.line(dates, residual_component, line_width=4, color=line_color) -p4.title.text = "Residual Component" -p4.title.text_font_size = "26pt" -p4.yaxis.axis_label = "Residual" -p4.yaxis.axis_label_text_font_size = "22pt" -p4.xaxis.axis_label = "Date" -p4.xaxis.axis_label_text_font_size = "22pt" -p4.xaxis.major_label_text_font_size = "18pt" -p4.yaxis.major_label_text_font_size = "18pt" -p4.xgrid.grid_line_alpha = 0.3 -p4.ygrid.grid_line_alpha = 0.3 -p4.xgrid.grid_line_dash = "dashed" -p4.ygrid.grid_line_dash = "dashed" -p4.outline_line_color = None -p4.min_border_left = 120 - -# Add main title to top panel -p1.add_layout( - Title(text="timeseries-decomposition · bokeh · pyplots.ai", text_font_size="32pt", align="center"), "above" +# Panel dimensions (4 panels in vertical layout) +panel_height = 650 +total_width = 4800 + + +# Helper function to create themed figure +def create_themed_figure(width, height, title_text, y_label, show_x_axis=False, x_range=None): + kwargs = { + "width": width, + "height": height, + "x_axis_type": "datetime", + "title": title_text, + "toolbar_location": None, # Hide toolbar for cleaner look + } + if x_range is not None: + kwargs["x_range"] = x_range + + p = figure(**kwargs) + + # Theme-adaptive styling + p.background_fill_color = PAGE_BG + p.border_fill_color = PAGE_BG + p.outline_line_color = INK_SOFT + + p.title.text_color = INK + p.title.text_font_size = "26pt" + p.title.text_font_style = "bold" + + p.xaxis.axis_label_text_color = INK + p.yaxis.axis_label_text_color = INK + p.xaxis.axis_label_text_font_size = "20pt" + p.yaxis.axis_label_text_font_size = "20pt" + + p.xaxis.major_label_text_color = INK_SOFT + p.yaxis.major_label_text_color = INK_SOFT + p.xaxis.major_label_text_font_size = "16pt" + p.yaxis.major_label_text_font_size = "16pt" + + p.xaxis.axis_line_color = INK_SOFT + p.yaxis.axis_line_color = INK_SOFT + p.xaxis.major_tick_line_color = INK_SOFT + p.yaxis.major_tick_line_color = INK_SOFT + + p.ygrid.grid_line_color = INK + p.ygrid.grid_line_alpha = 0.12 + p.xgrid.grid_line_color = INK + p.xgrid.grid_line_alpha = 0.08 + + p.yaxis.axis_label = y_label + if not show_x_axis: + p.xaxis.visible = False + else: + p.xaxis.axis_label = "Date" + + p.min_border_left = 100 + p.min_border_right = 40 + p.min_border_top = 60 + p.min_border_bottom = 60 + + return p + + +# Create subplots with theme-aware styling and distinct colors +p1 = create_themed_figure(total_width, panel_height, "Original Series", "Passengers (thousands)") +source1 = ColumnDataSource(data={"date": dates, "value": original}) +p1.line("date", "value", source=source1, line_width=3, color=OKABE_ITO[0]) + +p2 = create_themed_figure(total_width, panel_height, "Trend Component", "Trend", x_range=p1.x_range) +source2 = ColumnDataSource(data={"date": dates, "value": trend_component}) +p2.line("date", "value", source=source2, line_width=3, color=OKABE_ITO[1]) + +p3 = create_themed_figure(total_width, panel_height, "Seasonal Component", "Seasonal", x_range=p1.x_range) +source3 = ColumnDataSource(data={"date": dates, "value": seasonal_component}) +p3.line("date", "value", source=source3, line_width=3, color=OKABE_ITO[2]) + +p4 = create_themed_figure( + total_width, panel_height, "Residual Component", "Residual", show_x_axis=True, x_range=p1.x_range ) +source4 = ColumnDataSource(data={"date": dates, "value": residual_component}) +p4.line("date", "value", source=source4, line_width=3, color=OKABE_ITO[3]) # Combine all panels into vertical layout layout = column(p1, p2, p3, p4) -# Save -export_png(layout, filename="plot.png") +# Save interactive HTML +output_file(f"plot-{THEME}.html") +save(layout) + +# Screenshot with headless Chrome using Selenium +W, H = 4800, 2700 +opts = Options() +for arg in ( + "--headless=new", + "--no-sandbox", + "--disable-dev-shm-usage", + "--disable-gpu", + f"--window-size={W},{H}", + "--hide-scrollbars", +): + opts.add_argument(arg) + +driver = webdriver.Chrome(options=opts) +driver.set_window_size(W, H) +driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}") +time.sleep(3) # Let bokeh's JS render the canvas +driver.save_screenshot(f"plot-{THEME}.png") +driver.quit() diff --git a/plots/timeseries-decomposition/metadata/python/bokeh.yaml b/plots/timeseries-decomposition/metadata/python/bokeh.yaml index 57c6e67c56..ec012bc582 100644 --- a/plots/timeseries-decomposition/metadata/python/bokeh.yaml +++ b/plots/timeseries-decomposition/metadata/python/bokeh.yaml @@ -1,184 +1,174 @@ library: bokeh +language: python specification_id: timeseries-decomposition created: '2025-12-31T10:55:05Z' -updated: '2025-12-31T11:03:04Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20617481231 +updated: '2026-05-14T23:02:19Z' +generated_by: claude-haiku +workflow_run: 25890104219 issue: 2992 -python_version: 3.13.11 -library_version: 3.8.1 -preview_url: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/bokeh/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/bokeh/plot.html -quality_score: 91 -impl_tags: - dependencies: - - statsmodels - techniques: - - html-export - patterns: - - data-generation - dataprep: - - time-series - styling: [] +python_version: 3.13.13 +library_version: 3.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/python/bokeh/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/python/bokeh/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/python/bokeh/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/timeseries-decomposition/python/bokeh/plot-dark.html +quality_score: 86 review: strengths: - - Excellent implementation of all four decomposition components with clear visual - separation - - Uses statsmodels seasonal_decompose for proper statistical decomposition - - Clean shared x-axis implementation using x_range linking - - Appropriate font sizes for the 4800px canvas width - - Classic airline passenger example with realistic trend and seasonal patterns - - Good use of dashed grid lines that do not distract from the data + - Perfect visual quality with all text readable in both themes using proper Okabe-Ito + palette colors + - Flawless spec compliance with all 4 decomposition components shown correctly + - Clean, reproducible code with proper theme token separation (light/dark) + - Effective use of shared x_range for linked axis panning across subplots + - Proper Okabe-Ito palette order (positions 1-4) with consistent application across + themes weaknesses: - - Bokeh toolbar visible on right side adds visual clutter; could be hidden for static - export - - Y-axis label rotation on Passengers (thousands) makes it slightly harder to read - - Panel heights (620px each) leave some unused vertical space in 2700px target - - Could use ColumnDataSource for more idiomatic Bokeh code - image_description: 'The plot displays four vertically stacked panels showing a time - series decomposition of airline passenger data from 2018-2028. The main title - "timeseries-decomposition · bokeh · pyplots.ai" appears at the top. All panels - use the Python blue color (#306998) for the line plots with dashed grid lines - at 0.3 alpha. - - - - **Original Series** (top): Shows the raw data with clear upward trend and seasonal - fluctuations, ranging from ~80 to ~280 thousand passengers. Y-axis labeled "Passengers - (thousands)". - - - **Trend Component**: Displays a smooth monotonically increasing line from ~110 - to ~240, capturing the long-term growth. + - Generic design using default styling without custom visual refinement or sophistication + - No visual hierarchy or emphasis among components - all treated equally + - Missing data annotations or callouts to guide interpretation and highlight key + patterns + - Could leverage HoverTool for interactive value exploration at each timestamp + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1), not pure white ✓ + Chrome: All titles, axis labels, and tick labels clearly visible in dark text (INK #1A1A17 for titles, INK_SOFT #4A4A44 for ticks) ✓ + Data colors: Original Series #009E73 (brand green, position 1), Trend #D55E00 (position 2), Seasonal #0072B2 (position 3), Residual #CC79A7 (position 4) - all Okabe-Ito correct ✓ + Grid: Subtle, low alpha (0.12 y-axis, 0.08 x-axis), doesn't compete with data ✓ + Layout: Generous spacing, clear Y-axis labels per component, readable Y-axis ranges ✓ + Legibility verdict: PASS - excellent readability with proper contrast and color precision - - **Seasonal Component**: Shows a repeating sinusoidal pattern oscillating between - approximately -30 and +30, with consistent annual cycles. - - - **Residual Component** (bottom): Shows random noise fluctuating around zero - (approximately -20 to +20), with the x-axis labeled "Date". - - - All panels share a linked x-axis from 2018 to 2028. The Bokeh toolbar is visible - on the right side of each panel with interactive tools.' + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17), not pure black ✓ + Chrome: All titles, labels, and ticks clearly visible in light text (INK #F0EFE8 for titles, INK_SOFT #B8B7B0 for ticks) ✓ + Data colors: Identical to light render - green, vermillion, blue, reddish purple in same positions (only chrome flips theme, not data) ✓ + Grid: Subtle low-alpha lines, appropriately visible without competing with data ✓ + Layout: Matches light render perfectly with proper spacing and alignment ✓ + Dark-on-dark check: PASS - no dark text on dark background; all text uses appropriate light colors ✓ + Brand green (#009E73) visibility: PASS - maintains visibility on both surfaces ✓ + Legibility verdict: PASS - excellent theme adaptation with no readability failures; light text readable on dark surface criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 30 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 9 - max: 10 + score: 8 + max: 8 passed: true - comment: Title at 32pt, subplot titles at 26pt, axis labels at 22pt, tick - labels at 18pt - all clearly readable, though y-axis labels on left side - are slightly rotated and could be positioned better + comment: All font sizes explicitly set; readable in both themes with proper + INK tokens - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text elements anywhere + comment: No text collisions; proper spacing; all elements have breathing room - id: VQ-03 name: Element Visibility - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: Line width of 4 is perfect for the 4800px width, data density well - handled + comment: Lines well-sized; all data distinguishable from backgrounds - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Single Python blue color is colorblind-safe + comment: Okabe-Ito palette; adequate contrast; no red-green sole signal - id: VQ-05 - name: Layout Balance - score: 3 - max: 5 + name: Layout & Canvas + score: 4 + max: 4 passed: true - comment: Good panel distribution but some wasted space on right with Bokeh - toolbar; panels could use slightly more vertical space + comment: Good proportions; nothing cut off; proper margins - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: '"Passengers (thousands)" includes units, "Date", "Trend", "Seasonal", - "Residual" are descriptive' + comment: Clear titles; labels with units; X-axis labeled - id: VQ-07 - name: Grid & Legend - score: 1 + name: Palette Compliance + score: 2 max: 2 passed: true - comment: Grid at 0.3 alpha with dashed style is subtle and appropriate; no - legend needed for this plot type but the Bokeh toolbar is visible and somewhat - distracting + comment: 'First series #009E73; Okabe-Ito order 1-4; backgrounds correct; + both renders theme-correct' + design_excellence: + score: 10 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 4 + max: 8 + passed: false + comment: Functional but lacks custom visual flourishes + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: false + comment: Subtle grids and outlines, but could use more refinement + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: false + comment: Logical arrangement but no emphasis or focal points spec_compliance: - score: 25 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct time series decomposition with 4 stacked subplots - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Date on x-axis, values on y-axis, correctly mapped - - id: SC-03 + comment: Correct decomposition with 4 stacked subplots + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: 'All 4 components present: Original, Trend, Seasonal, Residual' - - id: SC-04 - name: Data Range + comment: Original, Trend, Seasonal, Residual all shown + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All data visible, no clipping - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Each subplot clearly labeled with component name - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Date on X-axis; components on Y; all visible + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: 'Uses correct format: "timeseries-decomposition · bokeh · pyplots.ai"' + comment: Clear subplot titles; single series per panel data_quality: - score: 18 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows all decomposition components well; trend/seasonal/residual - separation is clear, though residuals could show more interesting patterns + comment: All 4 components shown; complete decomposition - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Airline passenger data is a classic, real-world time series example + comment: Airline passengers (classic); 10 years; realistic trend and seasonality - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Values are realistic for airline passengers in thousands; 120 months - (10 years) provides good coverage, though the trend gaps at start/end from - decomposition windowing are visible + comment: Sensible timeframe and values; appropriate seasonal amplitude code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 @@ -186,41 +176,61 @@ review: score: 3 max: 3 passed: true - comment: Simple linear script with no functions/classes + comment: Helper function justified; no unnecessary classes - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Uses np.random.seed(42) + comment: Seed set; deterministic data generation - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports are used + comment: All imports used - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Using current Bokeh API + comment: No fake UI; appropriate complexity - id: CQ-05 - name: Output Correct - score: 0 + name: Output & API + score: 1 max: 1 - passed: false - comment: Saves as "plot.png" which is correct - library_features: - score: 3 - max: 5 + passed: true + comment: plot-{THEME}.png and .html; current API + library_mastery: + score: 6 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features - score: 3 + - id: LM-01 + name: Idiomatic Usage + score: 4 max: 5 passed: true - comment: Uses figure, line, column layout, x_range linking between panels, - Title model, export_png - good Bokeh usage but could leverage ColumnDataSource - and HoverTool for enhanced interactivity + comment: 'Recommended patterns: figure(), ColumnDataSource, column(), datetime + axis, shared x_range' + - id: LM-02 + name: Distinctive Features + score: 2 + max: 5 + passed: false + comment: Theme-adaptive styling, Selenium integration, shared x_range; could + explore HoverTool verdict: APPROVED +impl_tags: + dependencies: + - statsmodels + - selenium + techniques: + - subplots + - html-export + patterns: + - data-generation + - columndatasource + dataprep: + - time-series + styling: + - grid-styling