diff --git a/plots/slope-basic/implementations/python/altair.py b/plots/slope-basic/implementations/python/altair.py index 2275dc0e8b..f98a827598 100644 --- a/plots/slope-basic/implementations/python/altair.py +++ b/plots/slope-basic/implementations/python/altair.py @@ -1,14 +1,34 @@ -""" pyplots.ai +""" anyplot.ai slope-basic: Basic Slope Chart (Slopegraph) -Library: altair 6.0.0 | Python 3.13.11 -Quality: 94/100 | Created: 2025-12-17 +Library: altair 6.1.0 | Python 3.13.13 +Quality: 86/100 | Created: 2026-04-30 """ -import altair as alt +import os +import sys + import pandas as pd -# Data - Product sales comparing Q1 vs Q4 (10 products) +_script_dir = os.path.dirname(os.path.abspath(__file__)) +if _script_dir in sys.path: + sys.path.remove(_script_dir) + +import altair as alt # noqa: E402 + + +# 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: position 1 = Increase, position 2 = Decrease +COLOR_INCREASE = "#009E73" +COLOR_DECREASE = "#D55E00" + +# Data data = pd.DataFrame( { "Product": [ @@ -28,84 +48,67 @@ } ) -# Reshape data to long format for slope chart df_long = pd.melt(data, id_vars=["Product"], value_vars=["Q1 Sales", "Q4 Sales"], var_name="Period", value_name="Sales") - -# Determine direction of change for color coding data["Direction"] = data.apply(lambda row: "Increase" if row["Q4 Sales"] > row["Q1 Sales"] else "Decrease", axis=1) df_long = df_long.merge(data[["Product", "Direction"]], on="Product") -# Create slope chart +color_scale = alt.Scale(domain=["Increase", "Decrease"], range=[COLOR_INCREASE, COLOR_DECREASE]) + +# Plot lines = ( alt.Chart(df_long) .mark_line(strokeWidth=3, opacity=0.8) .encode( - x=alt.X("Period:N", axis=alt.Axis(labelFontSize=20, titleFontSize=24, title=None, labelAngle=0)), + x=alt.X("Period:N", axis=alt.Axis(labelFontSize=20, title=None, labelAngle=0)), y=alt.Y( "Sales:Q", axis=alt.Axis(labelFontSize=18, titleFontSize=22, title="Sales (units)"), scale=alt.Scale(zero=False), ), color=alt.Color( - "Direction:N", - scale=alt.Scale(domain=["Increase", "Decrease"], range=["#306998", "#FFD43B"]), - legend=alt.Legend(titleFontSize=20, labelFontSize=18, orient="top-right"), + "Direction:N", scale=color_scale, legend=alt.Legend(titleFontSize=20, labelFontSize=18, orient="top-right") ), detail="Product:N", ) ) -# Add points at endpoints points = ( alt.Chart(df_long) .mark_circle(size=200, opacity=0.9) - .encode( - x="Period:N", - y="Sales:Q", - color=alt.Color( - "Direction:N", scale=alt.Scale(domain=["Increase", "Decrease"], range=["#306998", "#FFD43B"]), legend=None - ), - ) + .encode(x="Period:N", y="Sales:Q", color=alt.Color("Direction:N", scale=color_scale, legend=None)) ) -# Add labels at left endpoint (Q1) labels_left = ( alt.Chart(df_long[df_long["Period"] == "Q1 Sales"]) .mark_text(align="right", dx=-15, fontSize=16) - .encode( - x="Period:N", - y="Sales:Q", - text="Product:N", - color=alt.Color( - "Direction:N", scale=alt.Scale(domain=["Increase", "Decrease"], range=["#306998", "#FFD43B"]), legend=None - ), - ) + .encode(x="Period:N", y="Sales:Q", text="Product:N", color=alt.Color("Direction:N", scale=color_scale, legend=None)) ) -# Add labels at right endpoint (Q4) labels_right = ( alt.Chart(df_long[df_long["Period"] == "Q4 Sales"]) .mark_text(align="left", dx=15, fontSize=16) - .encode( - x="Period:N", - y="Sales:Q", - text="Product:N", - color=alt.Color( - "Direction:N", scale=alt.Scale(domain=["Increase", "Decrease"], range=["#306998", "#FFD43B"]), legend=None - ), - ) + .encode(x="Period:N", y="Sales:Q", text="Product:N", color=alt.Color("Direction:N", scale=color_scale, legend=None)) ) -# Combine layers +# Style chart = ( (lines + points + labels_left + labels_right) - .properties( - width=1400, height=850, title=alt.Title(text="slope-basic · altair · pyplots.ai", fontSize=28, anchor="middle") + .properties(width=1400, height=850, background=PAGE_BG, title="slope-basic · altair · anyplot.ai") + .configure_title(color=INK, fontSize=28, anchor="middle") + .configure_axis( + domainColor=INK_SOFT, + tickColor=INK_SOFT, + grid=True, + gridColor=INK, + gridOpacity=0.10, + gridDash=[4, 4], + labelColor=INK_SOFT, + titleColor=INK, ) - .configure_axis(grid=True, gridOpacity=0.3, gridDash=[4, 4]) - .configure_view(strokeWidth=0) + .configure_view(fill=PAGE_BG, stroke=INK_SOFT) + .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK) ) -# Save as PNG (4800 × 2700 px) and HTML -chart.save("plot.png", scale_factor=3.0) -chart.save("plot.html") +# Save +chart.save(f"plot-{THEME}.png", scale_factor=3.0) +chart.save(f"plot-{THEME}.html") diff --git a/plots/slope-basic/metadata/python/altair.yaml b/plots/slope-basic/metadata/python/altair.yaml index 0aee8df7d4..5b4ad12c43 100644 --- a/plots/slope-basic/metadata/python/altair.yaml +++ b/plots/slope-basic/metadata/python/altair.yaml @@ -1,38 +1,230 @@ library: altair +language: python specification_id: slope-basic created: 2025-12-17 02:32:26+00:00 -updated: 2025-12-17 02:32:26+00:00 -generated_by: claude-opus-4-5-20251101 -workflow_run: 20289431993 +updated: '2026-04-30T17:06:43Z' +generated_by: claude-sonnet +workflow_run: 25177483606 issue: 981 -python_version: 3.13.11 -library_version: 6.0.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/slope-basic/altair/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/slope-basic/altair/plot.html -quality_score: 94 +python_version: 3.13.13 +library_version: 6.1.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/slope-basic/python/altair/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/slope-basic/python/altair/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/slope-basic/python/altair/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/slope-basic/python/altair/plot-dark.html +quality_score: 86 +review: + strengths: + - Semantic directional color encoding (green=increase, orange=decrease) creates + immediate visual clarity and meaningful hierarchy + - Full spec compliance — all required features present and correctly implemented + - Excellent data quality with realistic business context and visible rank changes + - Clean, idiomatic Altair code using layer composition and detail encoding correctly + - Correct Okabe-Ito palette with proper theme adaptation in both light and dark + renders + weaknesses: + - Label crowding on left side around the 390-440 sales range (Charger/Tablet/Headphones + too close together); consider adjusting dx offsets or filtering to reduce cluster + density + - Canvas dimensions 1400x850 are below the recommended 1600x900 target + - Full rectangular view border instead of recommended L-shaped frame (remove top/right + spines via configure_view stroke removal or configure_axis settings) + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct theme surface, not pure white + Chrome: Title "slope-basic · altair · anyplot.ai" in dark ink, clearly readable. Y-axis label "Sales (units)" in dark ink, readable. Tick labels in soft dark (#4A4A44), readable. X-axis period labels "Q1 Sales" / "Q4 Sales" in soft dark, readable. Legend box with elevated background (#FFFDF6) and soft border. + Data: Ten slope lines — green (#009E73) for increases, orange (#D55E00) for decreases. Circle markers at endpoints. Entity labels on both sides in matching directional colors. Left-side cluster around 390-440 (Charger/Tablet/Headphones) shows some crowding. + Legibility verdict: PASS (minor label crowding but no unreadable text) + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) — correct dark theme surface, not pure black + Chrome: Title in light cream (#F0EFE8), clearly readable against dark background. Axis labels and tick labels in light grey (#B8B7B0), all readable. No dark-on-dark failures detected. Legend box uses elevated dark (#242420). + Data: Colors are identical to light render — same green (#009E73) for increases, same orange (#D55E00) for decreases. Data identity maintained across themes. + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 25 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: Font sizes explicitly set throughout; data labels at fontSize=16 + slightly small + - id: VQ-02 + name: No Overlap + score: 3 + max: 6 + passed: false + comment: Label crowding on left side at 390-440 range (Charger/Tablet/Headphones) + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Lines strokeWidth=3 and markers size=200 well-sized + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Green vs orange CVD-safe contrast + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Canvas 1400x850 slightly undersized vs recommended 1600x900 + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Y-axis Sales (units) with units; period names descriptive + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Correct Okabe-Ito #009E73 first, #D55E00 second; correct backgrounds + both themes' + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Semantic directional coloring above defaults; thoughtful design not + publication-ready + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Subtle dashed grid at 10% opacity; full view border instead of L-frame + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Directional colors create immediate visual hierarchy; viewer instantly + sees decliners vs growers + spec_compliance: + score: 15 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct slopegraph + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: All spec features present + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Correct X/Y/color/detail mapping + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title format correct; legend labels match + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Both increases and decreases with rank changes visible + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Tech product sales Q1 vs Q4 — neutral business scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Values 180-1200 units with factually plausible variation + 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, no functions or classes + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Hardcoded deterministic data + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only os, sys, pandas, altair — all used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean Altair layer composition with pd.melt for reshaping + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-{THEME}.png and .html with current API + library_mastery: + score: 8 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Expert use of layer composition, detail encoding, alt.Scale, configure_* + system + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: detail encoding for per-product lines grouped by color is distinctly + Altair; layer composition strength used well + verdict: APPROVED impl_tags: dependencies: [] techniques: - layer-composition - html-export patterns: - - data-generation - wide-to-long dataprep: [] styling: + - alpha-blending - grid-styling -review: - strengths: [] - weaknesses: [] - improvements: [] - image_description: 'The plot shows a slope chart (slopegraph) comparing product - sales between Q1 and Q4. Ten products are displayed with lines connecting their - Q1 values (left) to Q4 values (right). Lines are color-coded: **blue (#306998)** - for products that increased (Laptop, Tablet, Monitor, Mouse, Headphones, Webcam) - and **yellow (#FFD43B)** for products that decreased (Phone, Keyboard, Charger, - Speaker). Each endpoint has a circular marker and labels on both sides identifying - the product. The Y-axis shows "Sales (units)" ranging from ~100 to 1200. The X-axis - displays "Q1 Sales" and "Q4 Sales" as period labels. A legend in the top-right - shows "Direction" with Increase/Decrease categories. The title reads "slope-basic - · altair · pyplots.ai". A subtle dashed grid helps with reading values.' - verdict: APPROVED