diff --git a/plots/sankey-basic/implementations/python/altair.py b/plots/sankey-basic/implementations/python/altair.py index bfdc7c776f..412a9311a4 100644 --- a/plots/sankey-basic/implementations/python/altair.py +++ b/plots/sankey-basic/implementations/python/altair.py @@ -1,13 +1,22 @@ -""" pyplots.ai +""" anyplot.ai sankey-basic: Basic Sankey Diagram -Library: altair 6.0.0 | Python 3.13.11 -Quality: 91/100 | Created: 2025-12-23 +Library: altair 6.1.0 | Python 3.13.13 +Quality: 82/100 | Updated: 2026-04-30 """ +import os + import altair as alt import pandas as pd +# 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" + # Data - Energy flow from sources to sectors flows = [ {"source": "Coal", "target": "Residential", "value": 20}, @@ -26,8 +35,7 @@ df = pd.DataFrame(flows) -# Target output: 4800x2700 px (16:9 aspect ratio) with scale_factor=3.0 -# Internal canvas: 1600x900 pixels +# Canvas dimensions: 1600x900 internal → 4800x2700 px at scale_factor=3.0 width = 1600 height = 900 node_width = 80 @@ -37,12 +45,10 @@ sources = df["source"].unique().tolist() targets = df["target"].unique().tolist() -# Calculate totals for positioning source_totals = df.groupby("source")["value"].sum().to_dict() target_totals = df.groupby("target")["value"].sum().to_dict() total_flow = df["value"].sum() -# Available height for nodes - reserve space for title (top) and margins top_margin = 100 bottom_margin = 60 available_height = height - top_margin - bottom_margin @@ -71,14 +77,19 @@ target_positions[tgt] = {"y": current_y, "height": node_height} current_y += node_height + node_padding -# Color palettes - Python Blue (#306998) as primary, Yellow (#FFD43B) for accent -source_colors = {"Coal": "#306998", "Gas": "#4A8BC6", "Nuclear": "#2D5986", "Renewable": "#FFD43B"} +# Okabe-Ito palette for source colors — distinct, colorblind-safe +source_colors = { + "Coal": "#009E73", # Okabe-Ito #1 (brand green) + "Gas": "#D55E00", # Okabe-Ito #2 (vermillion) + "Nuclear": "#0072B2", # Okabe-Ito #3 (blue) + "Renewable": "#CC79A7", # Okabe-Ito #4 (reddish purple) +} -target_colors = {"Residential": "#4ECDC4", "Commercial": "#95E1D3", "Industrial": "#FF6B6B", "Transport": "#FFA07A"} +# Target node colors — muted, distinct from source palette +target_colors = {"Residential": "#7EC8C8", "Commercial": "#A8D8A8", "Industrial": "#E8C07A", "Transport": "#C8A8E8"} -# Create node rectangles data +# Build node rectangles data nodes_data = [] - for src in sources: pos = source_positions[src] nodes_data.append( @@ -115,12 +126,10 @@ nodes_df = pd.DataFrame(nodes_data) -# Create flow paths using polygons -# Track current position within each node for stacking flows +# Generate smoothstep S-curve polygon points for each flow band source_y_offsets = {src: source_positions[src]["y"] for src in sources} target_y_offsets = {tgt: target_positions[tgt]["y"] for tgt in targets} -# Generate polygon points for each flow (closed path) all_flow_data = [] num_curve_points = 40 @@ -129,34 +138,28 @@ tgt = row["target"] val = row["value"] - # Flow height proportional to value within each node src_height = (val / source_totals[src]) * source_positions[src]["height"] tgt_height = (val / target_totals[tgt]) * target_positions[tgt]["height"] - # Start and end Y positions for this flow band src_y_top = source_y_offsets[src] src_y_bottom = src_y_top + src_height tgt_y_top = target_y_offsets[tgt] tgt_y_bottom = tgt_y_top + tgt_height - # Update offsets for stacking next flow from same source/target source_y_offsets[src] += src_height target_y_offsets[tgt] += tgt_height x_start = node_width x_end = width - node_width - # Generate top curve points (left to right) using smoothstep interpolation top_points = [] for i in range(num_curve_points): t = i / (num_curve_points - 1) x = x_start + t * (x_end - x_start) - # Smoothstep creates smooth S-curve for natural flow appearance bezier_t = t * t * (3 - 2 * t) y = src_y_top + bezier_t * (tgt_y_top - src_y_top) top_points.append((x, y)) - # Generate bottom curve points (right to left to close the polygon) bottom_points = [] for i in range(num_curve_points - 1, -1, -1): t = i / (num_curve_points - 1) @@ -165,7 +168,6 @@ y = src_y_bottom + bezier_t * (tgt_y_bottom - src_y_bottom) bottom_points.append((x, y)) - # Combine top + bottom into closed polygon for filled area rendering all_points = top_points + bottom_points for pt_idx, (x, y) in enumerate(all_points): all_flow_data.append( @@ -174,7 +176,7 @@ flows_df = pd.DataFrame(all_flow_data) -# Create flow polygons using mark_line with filled=True +# Flow polygons colored by source links_chart = ( alt.Chart(flows_df) .mark_line(filled=True, opacity=0.55, strokeWidth=0) @@ -184,24 +186,17 @@ color=alt.Color( "source:N", scale=alt.Scale(domain=list(source_colors.keys()), range=list(source_colors.values())), - legend=alt.Legend( - title="Energy Source", - titleFontSize=18, - labelFontSize=16, - orient="bottom-right", - titleColor="#333333", - labelColor="#333333", - ), + legend=alt.Legend(title="Energy Source", titleFontSize=18, labelFontSize=16, orient="bottom-right"), ), detail="flow_id:N", order="order:Q", ) ) -# Create node rectangles +# Node rectangles nodes_chart = ( alt.Chart(nodes_df) - .mark_rect(stroke="#333333", strokeWidth=2) + .mark_rect(stroke=INK_SOFT, strokeWidth=2) .encode( x=alt.X("x:Q", scale=alt.Scale(domain=[0, width])), y=alt.Y("y:Q", scale=alt.Scale(domain=[0, height])), @@ -212,51 +207,55 @@ ) ) -# Create source labels (right-aligned to the left of nodes) +# Source labels (right of left nodes) source_labels_df = nodes_df[nodes_df["side"] == "source"] source_labels = ( alt.Chart(source_labels_df) - .mark_text(fontSize=20, fontWeight="bold", color="#333333", align="left", baseline="middle") + .mark_text(fontSize=20, fontWeight="bold", align="left", baseline="middle") .encode( x=alt.X("label_x:Q", scale=alt.Scale(domain=[0, width])), y=alt.Y("label_y:Q", scale=alt.Scale(domain=[0, height])), text="name:N", + color=alt.value(INK), ) ) -# Create target labels (left-aligned to the right of nodes) +# Target labels (left of right nodes) target_labels_df = nodes_df[nodes_df["side"] == "target"] target_labels = ( alt.Chart(target_labels_df) - .mark_text(fontSize=20, fontWeight="bold", color="#333333", align="right", baseline="middle") + .mark_text(fontSize=20, fontWeight="bold", align="right", baseline="middle") .encode( x=alt.X("label_x:Q", scale=alt.Scale(domain=[0, width])), y=alt.Y("label_y:Q", scale=alt.Scale(domain=[0, height])), text="name:N", + color=alt.value(INK), ) ) -# Combine all layers +# Compose all layers with theme-adaptive chrome chart = ( alt.layer(links_chart, nodes_chart, source_labels, target_labels) .properties( width=width, height=height, + background=PAGE_BG, title=alt.Title( - text="sankey-basic · altair · pyplots.ai", + text="sankey-basic · altair · anyplot.ai", subtitle="Energy Flow from Sources to Sectors", fontSize=28, subtitleFontSize=20, anchor="middle", - color="#333333", - subtitleColor="#666666", + color=INK, + subtitleColor=INK_SOFT, ), - autosize=alt.AutoSizeParams(type="fit", contains="padding"), ) - .configure_view(strokeWidth=0) - .configure_legend(padding=15, cornerRadius=5, fillColor="#FFFFFF", strokeColor="#DDDDDD") + .configure_view(strokeWidth=0, fill=PAGE_BG) + .configure_legend( + padding=15, cornerRadius=5, fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK + ) ) -# Save as PNG (4800x2700 px with scale_factor=3.0) and HTML -chart.save("plot.png", scale_factor=3.0) -chart.save("plot.html") +# Save outputs (PNG at 4800×2700 px + HTML for interactivity) +chart.save(f"plot-{THEME}.png", scale_factor=3.0) +chart.save(f"plot-{THEME}.html") diff --git a/plots/sankey-basic/metadata/python/altair.yaml b/plots/sankey-basic/metadata/python/altair.yaml index 81e0d05aaa..402e5316b0 100644 --- a/plots/sankey-basic/metadata/python/altair.yaml +++ b/plots/sankey-basic/metadata/python/altair.yaml @@ -1,206 +1,242 @@ library: altair +language: python specification_id: sankey-basic created: '2025-12-23T19:50:52Z' -updated: '2025-12-23T19:57:56Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20470109773 -issue: 0 -python_version: 3.13.11 -library_version: 6.0.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/altair/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/altair/plot.html -quality_score: 91 -impl_tags: - dependencies: [] - techniques: - - bezier-curves - - layer-composition - - hover-tooltips - - html-export - patterns: - - iteration-over-groups - dataprep: [] - styling: - - alpha-blending +updated: '2026-04-30T09:24:49Z' +generated_by: claude-sonnet +workflow_run: 25156438466 +issue: 810 +python_version: 3.13.13 +library_version: 6.1.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/altair/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/altair/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/altair/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/altair/plot-dark.html +quality_score: 82 review: strengths: - - Excellent manual implementation of Sankey diagram using Altair primitive marks - (rectangles + filled polygons) - - Smooth S-curve interpolation for flow bands using smoothstep function creates - natural-looking flows - - Well-organized color scheme with Python Blue (#306998) as primary source color - - Clean layer composition (links → nodes → source labels → target labels) - - Good use of tooltips for interactivity on node hover - - Proper title format with informative subtitle + - Correct Okabe-Ito palette (positions 1-4) for source node and flow colors + - Smooth smoothstep bezier S-curves create a professional Sankey appearance + - Theme-adaptive chrome (background, text, legend) works correctly in both light + and dark renders with no dark-on-dark failures + - Complete spec compliance including correct title format and interactive HTML export + - Clean, deterministic, readable code with no functions or classes + - Tooltips on nodes provide interactive value information weaknesses: - - The three blue shades for Coal, Gas, and Nuclear sources are visually similar; - wider color variation would improve distinguishability - - Code complexity is high due to manual polygon generation; this is necessary since - Altair lacks native Sankey support, but adds maintenance burden - image_description: 'The plot displays a Sankey diagram showing energy flow from - four source nodes (Renewable, Nuclear, Gas, Coal) positioned on the left side - to four target sector nodes (Transport, Industrial, Commercial, Residential) on - the right. The flows are rendered as smooth S-curved bands with semi-transparent - fill (opacity ~0.55), colored by source category. Source nodes use a blue palette - (Coal: #306998, Gas: #4A8BC6, Nuclear: #2D5986) with Renewable in yellow (#FFD43B). - Target nodes are colored distinctly (teal shades for Residential/Commercial, coral/red - for Industrial/Transport). Node labels are positioned adjacent to their respective - rectangles in bold black text. The title "sankey-basic · altair · pyplots.ai" - appears centered at top with subtitle "Energy Flow from Sources to Sectors". A - legend in the bottom-right corner shows the "Energy Source" color mapping.' + - Transport label clipped at bottom canvas edge — only 'T' visible; increase bottom_margin + from 60 to 100 or reduce height scale factor from 0.85 to 0.80 + - 'Target node colors are custom hex values (#7EC8C8, #A8D8A8, #E8C07A, #C8A8E8) + not from the Okabe-Ito palette; replace with positions 5-7 (#E69F00, #56B4E9, + #F0E442) plus adaptive neutral' + - Legend shows line symbols (colored dashes) for what are actually filled area marks; + consider a custom legend solution + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) - correct + Chrome: Title "sankey-basic · altair · anyplot.ai" (28px, dark ink) and subtitle "Energy Flow from Sources to Sectors" (20px) are clearly readable. Node labels for Coal, Nuclear, Gas, Renewable (left), Industrial, Commercial, Residential (right) are readable in dark ink at 20px bold. Legend text readable at 16px. ISSUE: Transport target label is clipped - only "T" is visible at the bottom-right canvas edge. + Data: Four sources use Okabe-Ito positions 1-4: Coal=#009E73 (green), Gas=#D55E00 (vermillion), Nuclear=#0072B2 (blue), Renewable=#CC79A7 (pink-purple). Flow bands are semi-transparent (opacity=0.55) colored by source. Target node colors (#7EC8C8, #A8D8A8, #E8C07A, #C8A8E8) are custom non-Okabe-Ito values. Smooth S-curves connect sources to targets. + Legibility verdict: PASS (with caveat: Transport label clipped to "T") + + Dark render (plot-dark.png): + Background: Near-black (#1A1A17) - correct + Chrome: Title and subtitle rendered in light ink (#F0EFE8), clearly readable against dark background. Node labels rendered in INK token (light) - readable for all visible nodes. Legend box uses elevated dark fill (#242420) with subtle border. No dark-on-dark failures for any visible text elements. + Data: Flow band colors are identical to light render - Coal=#009E73, Gas=#D55E00, Nuclear=#0072B2, Renewable=#CC79A7. Data colors unchanged, only chrome flips between themes. Same Transport label clipping issue persists. + Legibility verdict: PASS (with caveat: Transport label clipped to "T", same as light render) criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 24 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 7 + max: 8 passed: true - comment: Title at 28pt, labels at 20pt bold, all clearly readable + comment: Font sizes excellent (title 28, labels 20, legend 16); Transport + label clipped to 'T' - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 5 + max: 6 passed: true - comment: No overlapping text, all labels positioned outside nodes + comment: Labels well-spaced; Transport truncation counts as partial failure - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 5 + max: 6 passed: true - comment: Flow bands well-sized, nodes visible with stroke, slight deduction - as some crossing flows can be hard to trace + comment: All flows visible at opacity=0.55; Transport node very small but + present - id: VQ-04 name: Color Accessibility - score: 4 - max: 5 + score: 2 + max: 2 passed: true - comment: Good color distinction overall, though blue shades for Coal/Gas/Nuclear - are somewhat similar + comment: Okabe-Ito for sources; labels provide text identification - id: VQ-05 - name: Layout Balance - score: 5 - max: 5 + name: Layout & Canvas + score: 2 + max: 4 + passed: false + comment: Transport node partially clipped at bottom canvas edge; label shows + only 'T' + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 passed: true - comment: Excellent use of canvas, nodes well-spaced, balanced margins + comment: Correct title format; descriptive subtitle; axes hidden appropriately - id: VQ-07 - name: Grid & Legend - score: 2 + name: Palette Compliance + score: 1 max: 2 + passed: false + comment: 'Source colors use Okabe-Ito positions 1-4 correctly. Target node + colors (#7EC8C8, #A8D8A8, #E8C07A, #C8A8E8) are custom hex values not from + Okabe-Ito' + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Professional manual Sankey via Altair primitives; smooth bezier S-curves; + styled legend + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: No axes/spines appropriate; no grid; clean background; good node + padding + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 passed: true - comment: Legend well-placed in bottom-right, no grid needed for Sankey + comment: Clear energy source->sector flow narrative; width encoding communicates + magnitudes; no explicit emphasis on dominant pathways 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 Sankey diagram with nodes and flow links - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Source→Target flows with proportional widths correctly implemented - - id: SC-03 + comment: Correct Sankey diagram with nodes and proportional flow bands + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: 'All spec features present: categorical nodes, flow values, distinct - colors' - - id: SC-04 - name: Data Range + comment: Source nodes, target nodes, proportional widths, distinct source + colors, link opacity, no circular flows + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All flows visible, node heights proportional to totals - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Legend correctly shows energy source colors - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Energy flow sources to sectors; values mapped to band widths + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Exact format "sankey-basic · altair · pyplots.ai" used + comment: Title matches spec-id·library·anyplot.ai format; legend labels match + source categories data_quality: - score: 18 - max: 20 + score: 14 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 5 + max: 6 passed: true - comment: Shows multiple sources flowing to multiple targets with varying magnitudes; - slight deduction as no extremely dominant or extremely minor flows shown + comment: 12 flows across 4x4 source/target grid; Transport only receives one + flow limiting full coverage - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Energy flow from sources to sectors is a classic, realistic Sankey - use case + comment: Energy distribution domain realistic and neutral; Coal->Industrial + largest is plausible - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Values (10-45 units) are plausible; could benefit from showing more - extreme differences + comment: 12 flows within 5-50 recommended range; values 10-45 sensible code_quality: - score: 8 + score: 10 max: 10 items: - id: CQ-01 name: KISS Structure - score: 2 + score: 3 max: 3 passed: true - comment: Linear flow structure, but complex manual polygon calculation; no - functions/classes used though + comment: No functions or classes; clean procedural flow - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Deterministic data (hardcoded flows), no random elements + comment: No random elements; fully deterministic - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only altair and pandas imported, both used + comment: Only os, altair, pandas - all used - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current Altair API + comment: Complexity justified by manual Sankey construction; no fake UI - id: CQ-05 - name: Output Correct - score: 0 + name: Output & API + score: 1 max: 1 passed: true - comment: Saves as "plot.png" but also saves "plot.html" which is fine - library_features: - score: 4 - max: 5 + comment: Saves plot-{THEME}.png (scale_factor=3.0) and plot-{THEME}.html; + current Altair 6.x API + library_mastery: + score: 7 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features + - id: LM-01 + name: Idiomatic Usage score: 4 max: 5 passed: true - comment: Excellent use of Altair's declarative layering, mark_line with filled - polygons, detailed encoding with tooltips, and legend configuration + comment: Correct alt.layer() composition, proper encoding types (Q/N), alt.Title + with subtitle, configure_legend/configure_view + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Interactive HTML export, node tooltips, layer composition of primitive + marks to build custom chart type verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - layer-composition + - bezier-curves + - hover-tooltips + - html-export + patterns: + - groupby-aggregation + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending