diff --git a/plots/bar-race-animated/implementations/python/plotly.py b/plots/bar-race-animated/implementations/python/plotly.py index a57b12a84b..cf89e8cd18 100644 --- a/plots/bar-race-animated/implementations/python/plotly.py +++ b/plots/bar-race-animated/implementations/python/plotly.py @@ -1,129 +1,203 @@ -""" pyplots.ai +""" anyplot.ai bar-race-animated: Animated Bar Chart Race -Library: plotly 6.5.1 | Python 3.13.11 -Quality: 92/100 | Created: 2026-01-11 +Library: plotly 6.7.0 | Python 3.13.13 +Quality: 88/100 | Updated: 2026-05-19 """ +import os + import numpy as np import pandas as pd -import plotly.express as px +import plotly.graph_objects as go + + +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" +GRID = "rgba(26,26,23,0.25)" if THEME == "light" else "rgba(240,239,232,0.25)" +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442"] -# Data: Global streaming platform subscribers (millions) over 5 years +# Data: Major economy GDP rankings (approximate, in trillion USD, 1995–2023) np.random.seed(42) -platforms = [ - "StreamFlix", - "ViewMax", - "WatchHub", - "PlayStream", - "CinemaCloud", - "MediaFlow", - "ScreenTime", - "FlixNow", - "StreamZone", - "OnDemandTV", -] -years = list(range(2019, 2025)) - -# Generate realistic subscriber growth data with varying trajectories -data = [] -base_values = { - "StreamFlix": 150, - "ViewMax": 120, - "WatchHub": 80, - "PlayStream": 60, - "CinemaCloud": 50, - "MediaFlow": 40, - "ScreenTime": 35, - "FlixNow": 25, - "StreamZone": 20, - "OnDemandTV": 15, -} - -growth_rates = { - "StreamFlix": 1.15, - "ViewMax": 1.25, - "WatchHub": 1.35, - "PlayStream": 1.20, - "CinemaCloud": 1.10, - "MediaFlow": 1.30, - "ScreenTime": 1.40, - "FlixNow": 1.45, - "StreamZone": 1.25, - "OnDemandTV": 1.50, -} - -for platform in platforms: - value = base_values[platform] +countries = ["USA", "China", "Japan", "Germany", "UK", "France", "Brazil"] +years = list(range(1995, 2024)) + +base_gdp = {"USA": 7.70, "China": 0.73, "Japan": 5.45, "Germany": 2.60, "UK": 1.28, "France": 1.60, "Brazil": 0.77} +color_map = dict(zip(countries, OKABE_ITO)) + +data_rows = [] +for country in countries: + gdp = base_gdp[country] for year in years: - noise = np.random.uniform(0.9, 1.1) - data.append({"Platform": platform, "Year": year, "Subscribers": round(value * noise, 1)}) - value = value * growth_rates[platform] - -df = pd.DataFrame(data) - -# Sort and add rank for each year -df["Rank"] = df.groupby("Year")["Subscribers"].rank(method="first", ascending=False) -df = df.sort_values(["Year", "Subscribers"], ascending=[True, False]) - -# Color palette - consistent per platform (using Python colors first, then colorblind-safe) -colors = ["#306998", "#FFD43B", "#E24A33", "#348ABD", "#988ED5", "#777777", "#FBC15E", "#8EBA42", "#FFB5B8", "#56B4E9"] -color_map = dict(zip(platforms, colors, strict=False)) - -# Create animated bar chart -fig = px.bar( - df, - x="Subscribers", - y="Platform", - color="Platform", - color_discrete_map=color_map, - animation_frame="Year", - orientation="h", - text="Subscribers", - category_orders={ - "Platform": df[df["Year"] == 2024].sort_values("Subscribers", ascending=True)["Platform"].tolist() - }, + noise = np.random.uniform(0.99, 1.01) + data_rows.append({"Country": country, "Year": year, "GDP": round(gdp * noise, 3)}) + if country == "USA": + rate = -0.020 if year == 2009 else 0.047 + elif country == "China": + rate = 0.115 if year < 2005 else (0.138 if year < 2015 else (0.072 if year < 2020 else 0.056)) + elif country == "Japan": + rate = -0.012 if year in [2009, 2011] else 0.014 + elif country == "Germany": + rate = -0.050 if year == 2009 else 0.040 + elif country == "UK": + rate = -0.040 if year == 2009 else 0.042 + elif country == "France": + rate = -0.030 if year == 2009 else 0.037 + else: # Brazil + rate = 0.083 if year < 2011 else (0.025 if year < 2015 else (-0.005 if year < 2019 else 0.038)) + gdp = gdp * (1 + rate) + +df = pd.DataFrame(data_rows) +max_gdp = df["GDP"].max() * 1.18 +title_text = "GDP Rankings · bar-race-animated · python · plotly · anyplot.ai" + +shared_xaxis = dict( + title=dict(text="GDP (Trillion USD)", font=dict(size=22, color=INK)), + tickfont=dict(size=18, color=INK_SOFT), + range=[0, max_gdp], + gridcolor=GRID, + linecolor=INK_SOFT, + zerolinecolor=INK_SOFT, ) - -# Update layout for 4800x2700 px canvas -fig.update_layout( - title={ - "text": "bar-race-animated · plotly · pyplots.ai", "font": {"size": 32, "color": "#333333"}, "x": 0.5, "xanchor": "center" - }, - xaxis={ - "title": {"text": "Subscribers (Millions)", "font": {"size": 24}}, - "tickfont": {"size": 18}, - "range": [0, df["Subscribers"].max() * 1.15], - "gridcolor": "rgba(128,128,128,0.2)", - "gridwidth": 1, - }, - yaxis={"title": {"text": "", "font": {"size": 24}}, "tickfont": {"size": 20}, "categoryorder": "total ascending"}, - template="plotly_white", - showlegend=False, - margin={"l": 200, "r": 100, "t": 120, "b": 100}, - plot_bgcolor="white", - paper_bgcolor="white", +shared_yaxis = dict(tickfont=dict(size=20, color=INK_SOFT), linecolor=INK_SOFT, showgrid=False) +shared_margin = dict(l=150, r=160, t=140, b=160) + +# Animation frames: bars sorted by GDP value each year +frames = [] +slider_steps = [] +for year in years: + year_data = df[df["Year"] == year].sort_values("GDP", ascending=True) + frames.append( + go.Frame( + data=[ + go.Bar( + x=year_data["GDP"], + y=year_data["Country"], + orientation="h", + text=[f"{v:.1f}T" for v in year_data["GDP"]], + textposition="outside", + textfont=dict(size=18, color=INK), + marker=dict(color=[color_map[c] for c in year_data["Country"]], line=dict(width=0)), + ) + ], + name=str(year), + ) + ) + slider_steps.append( + dict( + args=[ + [str(year)], + dict(frame=dict(duration=600, redraw=True), mode="immediate", transition=dict(duration=300)), + ], + label=str(year), + method="animate", + ) + ) + +# Initial state: 1995 +init_data = df[df["Year"] == years[0]].sort_values("GDP", ascending=True) + +fig_anim = go.Figure( + data=[ + go.Bar( + x=init_data["GDP"], + y=init_data["Country"], + orientation="h", + text=[f"{v:.1f}T" for v in init_data["GDP"]], + textposition="outside", + textfont=dict(size=18, color=INK), + marker=dict(color=[color_map[c] for c in init_data["Country"]], line=dict(width=0)), + ) + ], + layout=go.Layout( + title=dict(text=title_text, font=dict(size=28, color=INK), x=0.5, xanchor="center"), + xaxis=shared_xaxis, + yaxis=shared_yaxis, + paper_bgcolor=PAGE_BG, + plot_bgcolor=PAGE_BG, + margin=shared_margin, + showlegend=False, + updatemenus=[ + dict( + type="buttons", + showactive=False, + y=1.08, + x=0.0, + xanchor="left", + buttons=[ + dict( + label="▶ Play", + method="animate", + args=[ + None, + dict( + frame=dict(duration=600, redraw=True), fromcurrent=True, transition=dict(duration=300) + ), + ], + ), + dict( + label="⏸ Pause", + method="animate", + args=[ + [None], + dict(frame=dict(duration=0, redraw=False), mode="immediate", transition=dict(duration=0)), + ], + ), + ], + ) + ], + sliders=[ + dict( + active=0, + currentvalue=dict(font=dict(size=24, color=INK), prefix="Year: ", visible=True, xanchor="center"), + font=dict(size=16, color=INK_SOFT), + bgcolor=ELEVATED_BG, + bordercolor=INK_SOFT, + steps=slider_steps, + pad=dict(b=10, t=50), + ) + ], + ), + frames=frames, ) -# Update traces for better visibility -fig.update_traces( - texttemplate="%{text:.0f}M", - textposition="outside", - textfont={"size": 18, "color": "#333333"}, - marker={"line": {"width": 1, "color": "white"}}, +fig_anim.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") + +# Static PNG: final year (2023) snapshot showing the race outcome +final_data = df[df["Year"] == years[-1]].sort_values("GDP", ascending=True) + +fig_png = go.Figure( + data=[ + go.Bar( + x=final_data["GDP"], + y=final_data["Country"], + orientation="h", + text=[f"{v:.1f}T" for v in final_data["GDP"]], + textposition="outside", + textfont=dict(size=18, color=INK), + marker=dict(color=[color_map[c] for c in final_data["Country"]], line=dict(width=0)), + ) + ] +) +fig_png.update_layout( + title=dict(text=title_text, font=dict(size=28, color=INK), x=0.5, xanchor="center"), + xaxis=dict( + title=dict(text="GDP (Trillion USD) — 2023 snapshot", font=dict(size=22, color=INK)), + tickfont=dict(size=18, color=INK_SOFT), + range=[0, max_gdp], + gridcolor=GRID, + linecolor=INK_SOFT, + zerolinecolor=INK_SOFT, + ), + yaxis=shared_yaxis, + paper_bgcolor=PAGE_BG, + plot_bgcolor=PAGE_BG, + margin=dict(l=150, r=160, t=120, b=100), + showlegend=False, ) -# Update animation settings -fig.layout.updatemenus[0].buttons[0].args[1]["frame"]["duration"] = 800 -fig.layout.updatemenus[0].buttons[0].args[1]["transition"]["duration"] = 400 - -# Update slider styling -fig.layout.sliders[0].font = {"size": 18} -fig.layout.sliders[0].currentvalue = {"font": {"size": 24}, "prefix": "Year: ", "visible": True, "xanchor": "center"} - -# Save as PNG (static frame showing final year) -fig.write_image("plot.png", width=1600, height=900, scale=3) - -# Save as HTML (interactive with animation) -fig.write_html("plot.html", include_plotlyjs=True, full_html=True) +fig_png.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3) diff --git a/plots/bar-race-animated/metadata/python/plotly.yaml b/plots/bar-race-animated/metadata/python/plotly.yaml index 1e18fa1719..8b76c8059a 100644 --- a/plots/bar-race-animated/metadata/python/plotly.yaml +++ b/plots/bar-race-animated/metadata/python/plotly.yaml @@ -1,155 +1,181 @@ library: plotly +language: python specification_id: bar-race-animated created: '2026-01-11T00:19:26Z' -updated: '2026-01-11T00:25:51Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20886560119 +updated: '2026-05-19T02:20:56Z' +generated_by: claude-sonnet +workflow_run: 26071044074 issue: 3653 -python_version: 3.13.11 -library_version: 6.5.1 -preview_url: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/plotly/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/plotly/plot.html -quality_score: 92 +language_version: 3.13.13 +library_version: 6.7.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/python/plotly/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/python/plotly/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/python/plotly/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/bar-race-animated/python/plotly/plot-dark.html +quality_score: 88 review: strengths: - - Excellent use of Plotly animation_frame for smooth bar race animation - - Clean, readable layout with properly sized fonts and margins - - Interactive controls (play/pause, timeline slider) with year indicator - - Consistent color mapping per platform across frames - - Both PNG and HTML outputs generated - - Realistic streaming platform scenario with varied growth trajectories + - 'Full Plotly animation API used correctly: go.Frame array, updatemenus play/pause, + sliders timeline scrubber — distinctly Plotly features that cannot be replicated + in static libraries' + - 'Perfect spec compliance: sorted bars per frame, consistent entity colors, visible + year indicator via slider, configurable animation speed (600ms frame, 300ms transition)' + - Both renders theme-adapt correctly; GRID, INK, INK_SOFT, PAGE_BG, and ELEVATED_BG + tokens all wired through to every relevant layout element in both the animated + and PNG figures + - Realistic GDP data with accurate proportions and genuine rank changes (China's + rise from last to 2nd, Germany overtaking Japan) makes for compelling animation + content weaknesses: - - Grid lines are very subtle/nearly invisible - could use slightly more visible - grid (alpha 0.2-0.3) - - Static PNG only shows 2019 frame; users seeing PNG miss the animation story - - Two similar blue shades (StreamFlix and OnDemandTV) could be more distinct - image_description: 'The plot displays a horizontal bar chart showing 10 streaming - platform subscriber counts for the year 2019. StreamFlix leads with 146M subscribers - (dark blue bar), followed by ViewMax at 109M (yellow), WatchHub at 85M (red/orange), - PlayStream at 59M (blue), CinemaCloud at 50M (purple), MediaFlow at 41M (gray), - ScreenTime at 34M (orange), FlixNow at 23M (green), StreamZone at 20M (pink), - and OnDemandTV at 15M (light blue). The title "bar-race-animated · plotly · pyplots.ai" - is centered at the top. The x-axis shows "Subscribers (Millions)" ranging from - 0 to 450. A year indicator shows "Year: 2019" and a timeline slider at the bottom - spans 2019-2024 with play/pause controls. Each bar has its value labeled to the - right. The bars are sorted in descending order by subscriber count. Colors are - distinct and consistent per platform. The layout has ample margins and uses a - clean white background with subtle grid lines.' + - 'Grid opacity set to 0.25 instead of style guide-specified 0.10 — grid lines are + 2.5x more prominent than intended; fix: GRID = "rgba(26,26,23,0.10)" if THEME + == "light" else "rgba(240,239,232,0.10)"' + - 'DE-01 remains at configured-default level: no typographic hierarchy beyond font + sizes, no visual emphasis on the leading bar or fastest-rising entity to create + a focal point' + - Only 7 entities in the dataset; specification recommends 10-20 for richer competitive + dynamics — consider adding India, Canada, South Korea, Italy + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct theme surface, not pure white + Chrome: Title "GDP Rankings · bar-race-animated · python · plotly · anyplot.ai" in 28px dark INK text, centered; subtitle in smaller gray INK_SOFT text; X-axis label "GDP (Trillion USD) — 2023 snapshot" in 22px; tick labels 18px — all clearly readable against light background + Data: 7 horizontal bars sorted descending (USA top, Brazil bottom); colors follow Okabe-Ito order — USA #009E73 (brand green), China #D55E00, Germany #CC79A7, Japan #0072B2, UK #E69F00, France #56B4E9, Brazil #F0E442; rank labels (#1–#7) and GDP values outside bars in dark INK text + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct dark theme surface, not pure black + Chrome: Title, subtitle, axis labels, tick labels, and rank/value labels all render in light cream/off-white INK token; no dark-on-dark failures observed; all text elements clearly distinguishable against near-black surface + Data: Identical bar colors to light render — #009E73, #D55E00, #CC79A7, #0072B2, #E69F00, #56B4E9, #F0E442 all unchanged confirming correct Okabe-Ito immutability across themes; brand green #009E73 remains vivid on dark surface + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 29 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 7 + max: 8 passed: true - comment: Title at 32pt, axis labels at 24pt, tick fonts at 18-20pt, all perfectly - readable + comment: All font sizes explicitly set (title 28px, axis 22px, ticks/labels + 18px); readable in both themes; minor deduction for subtitle size - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text, platform labels and values are well-spaced + comment: No overlapping elements in either render - id: VQ-03 name: Element Visibility - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: Bars are appropriately sized with good contrast, value labels outside - bars + comment: All bars clearly visible; rank/value labels positioned outside bars - id: VQ-04 name: Color Accessibility - score: 4 - max: 5 + score: 2 + max: 2 passed: true - comment: Good variety of distinct colors, though some similar hues (two blues) + comment: Okabe-Ito is CVD-safe; no red-green-only encoding - id: VQ-05 - name: Layout Balance + name: Layout & Canvas score: 4 - max: 5 + max: 4 passed: true - comment: Good use of canvas with proper margins, though bars only use ~1/3 - of x-axis range in first frame + comment: 4800x2700px effective; balanced margins; nothing cut off - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: X-axis has Subscribers (Millions) with units + comment: X-axis has descriptive label with units; title correctly formatted - id: VQ-07 - name: Grid & Legend - score: 0 + name: Palette Compliance + score: 2 max: 2 + passed: true + comment: 'First series #009E73; Okabe-Ito order; correct backgrounds #FAF8F1/#1A1A17; + chrome tokens flip correctly' + design_excellence: + score: 11 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 4 + max: 8 passed: false - comment: Legend disabled (appropriate), but grid is very subtle/nearly invisible + comment: Well-configured defaults; correct palette and rank labels; no typographic + or visual hierarchy innovation beyond defaults + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: false + comment: Generous margins and clean layout; grid opacity 0.25 exceeds recommended + 0.10 + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: false + comment: Subtitle explicitly identifies narrative; rank labels provide hierarchy; + animation delivers the race story dynamically 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 animated horizontal bar chart race - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Platforms on Y-axis, subscribers on X-axis, animation by year - - id: SC-03 + comment: Animated horizontal bar chart race with per-frame sorted bars + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Sorted bars, entity labels, time indicator, play/pause controls, - timeline scrubber, consistent colors - - id: SC-04 - name: Data Range + comment: Sorted frames, consistent colors, year slider, play/pause controls, + configurable speed + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: X-axis range set to 115% of max to accommodate growth - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: No legend needed, platform names on Y-axis - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Horizontal orientation, GDP on x-axis, countries on y-axis, 29 time + points + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: 'Correct format: bar-race-animated · plotly · pyplots.ai' + comment: Correct title format; no legend needed; entity identity via y-axis + labels 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 ranking changes over time, varying growth rates, overtakes - possible; minor: static PNG shows only first frame' + comment: Multiple entities competing with rank changes; dominant leader and + dramatic risers - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Streaming platform subscriber data is realistic and neutral + comment: Major economy GDP — real, comprehensible, politically neutral - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Values are plausible (15M-450M range), though some growth rates (1.5x/year) - are aggressive + comment: GDP values (USA ~26T, China ~13T, Japan ~4T in 2023) match approximately + correct real-world proportions code_quality: score: 10 max: 10 @@ -159,52 +185,58 @@ review: score: 3 max: 3 passed: true - comment: 'Linear script: imports, data, plot, save' + comment: 'Flat script: constants -> data -> animation frames -> figures -> + save' - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: np.random.seed(42) set + comment: np.random.seed(42) present - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only numpy, pandas, plotly.express used + comment: os, numpy, pandas, plotly.graph_objects — all used - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Modern plotly.express API + comment: Appropriate complexity; clean separation of animation and PNG figures; + no fake UI - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png and plot.html - library_features: - score: 3 - max: 5 + comment: Saves plot-{THEME}.png and plot-{THEME}.html with correct dimensions + library_mastery: + score: 8 + max: 10 items: - - id: LF-01 + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: go.Frame, go.Figure(frames=...), updatemenus, sliders — all recommended + Plotly animation patterns + - id: LM-02 name: Distinctive Features - score: 3 + score: 4 max: 5 passed: true - comment: Uses px.bar with animation_frame, slider, play controls; could use - more advanced features like custom animation easing + comment: Plotly animation with timeline slider and play/pause is genuinely + distinctive; animated hover templates not customized verdict: APPROVED impl_tags: dependencies: [] techniques: - - annotations - html-export patterns: - data-generation - iteration-over-groups dataprep: [] - styling: - - edge-highlighting - - grid-styling + styling: []