diff --git a/plots/sankey-basic/implementations/python/letsplot.py b/plots/sankey-basic/implementations/python/letsplot.py index 76d5d0c647..c0a2005e72 100644 --- a/plots/sankey-basic/implementations/python/letsplot.py +++ b/plots/sankey-basic/implementations/python/letsplot.py @@ -1,14 +1,17 @@ -""" pyplots.ai +""" anyplot.ai sankey-basic: Basic Sankey Diagram -Library: letsplot 4.8.2 | Python 3.13.11 -Quality: 92/100 | Created: 2025-12-23 +Library: letsplot 4.9.0 | Python 3.13.13 +Quality: 85/100 | Updated: 2026-04-30 """ +import os + import pandas as pd from lets_plot import ( LetsPlot, aes, element_blank, + element_rect, element_text, geom_polygon, geom_rect, @@ -27,6 +30,16 @@ LetsPlot.setup_html() +# 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 source categories (canonical order, first = #009E73) +OKABE_ITO = ["#009E73", "#D55E00", "#0072B2", "#CC79A7"] + # Energy flow data: sources -> sectors (realistic energy distribution) flows = [ ("Coal", "Industrial", 28), @@ -41,9 +54,9 @@ ("Renewable", "Industrial", 6), ] -# Define node ordering sources = ["Coal", "Natural Gas", "Nuclear", "Renewable"] targets = ["Industrial", "Residential", "Commercial"] +source_color_map = dict(zip(sources, OKABE_ITO, strict=True)) # Calculate totals for each node source_totals = {} @@ -54,7 +67,7 @@ for _, tgt, val in flows: target_totals[tgt] = target_totals.get(tgt, 0) + val -# Normalize positions +# Layout parameters total_flow = sum(v for _, _, v in flows) node_gap = 0.04 x_left = 0.18 @@ -80,43 +93,33 @@ source_offsets = dict.fromkeys(sources, 0) target_offsets = dict.fromkeys(targets, 0) -# Build flow polygons with smooth bezier curves +# Build flow polygons with smooth cubic bezier curves flow_data = [] for src, tgt, val in flows: flow_height = val / total_flow * 0.85 - # Source connection points src_y0 = source_positions[src]["y0"] + source_offsets[src] src_y1 = src_y0 + flow_height source_offsets[src] += flow_height - # Target connection points tgt_y0 = target_positions[tgt]["y0"] + target_offsets[tgt] tgt_y1 = tgt_y0 + flow_height target_offsets[tgt] += flow_height - # Create smooth bezier polygon for flow n_points = 40 - x_vals_top = [] - y_vals_top = [] - x_vals_bottom = [] - y_vals_bottom = [] + x_vals_top, y_vals_top = [], [] + x_vals_bottom, y_vals_bottom = [], [] for i in range(n_points + 1): t = i / n_points x = x_left + t * (x_right - x_left) - # Smooth cubic bezier easing ease = t * t * (3 - 2 * t) - y_top = src_y1 + ease * (tgt_y1 - src_y1) - y_bottom = src_y0 + ease * (tgt_y0 - src_y0) - x_vals_top.append(x) - y_vals_top.append(y_top) + y_vals_top.append(src_y1 + ease * (tgt_y1 - src_y1)) x_vals_bottom.append(x) - y_vals_bottom.append(y_bottom) + y_vals_bottom.append(src_y0 + ease * (tgt_y0 - src_y0)) - # Combine into closed polygon x_polygon = x_vals_top + x_vals_bottom[::-1] y_polygon = y_vals_top + y_vals_bottom[::-1] @@ -132,32 +135,18 @@ for src in sources: pos = source_positions[src] node_rects.append( - { - "xmin": pos["x"] - node_width / 2, - "xmax": pos["x"] + node_width / 2, - "ymin": pos["y0"], - "ymax": pos["y1"], - "label": src, - "side": "source", - } + {"xmin": pos["x"] - node_width / 2, "xmax": pos["x"] + node_width / 2, "ymin": pos["y0"], "ymax": pos["y1"]} ) for tgt in targets: pos = target_positions[tgt] node_rects.append( - { - "xmin": pos["x"] - node_width / 2, - "xmax": pos["x"] + node_width / 2, - "ymin": pos["y0"], - "ymax": pos["y1"], - "label": tgt, - "side": "target", - } + {"xmin": pos["x"] - node_width / 2, "xmax": pos["x"] + node_width / 2, "ymin": pos["y0"], "ymax": pos["y1"]} ) df_nodes = pd.DataFrame(node_rects) -# Build labels with flow values +# Build labels with flow totals labels = [] for src in sources: pos = source_positions[src] @@ -183,27 +172,19 @@ df_labels = pd.DataFrame(labels) -# Colors for each energy source -source_colors = {"Coal": "#4A4A4A", "Natural Gas": "#306998", "Nuclear": "#9B59B6", "Renewable": "#27AE60"} - -# Create the plot +# Plot plot = ( ggplot() + geom_polygon( - aes(x="x", y="y", group="flow_id", fill="source"), data=df_flows, alpha=0.65, color="white", size=0.2 - ) - + geom_rect( - aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), - data=df_nodes, - fill="#2C3E50", - color="#1A252F", - size=1.5, + aes(x="x", y="y", group="flow_id", fill="source"), data=df_flows, alpha=0.65, color=PAGE_BG, size=0.2 ) + + geom_rect(aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), data=df_nodes, fill=INK, color=INK, size=1.5) + geom_text( aes(x="x", y="y", label="label"), data=df_labels[df_labels["side"] == "left"], size=14, hjust=1, + color=INK_SOFT, family="sans-serif", ) + geom_text( @@ -211,28 +192,30 @@ data=df_labels[df_labels["side"] == "right"], size=14, hjust=0, + color=INK_SOFT, family="sans-serif", ) - + scale_fill_manual(values=[source_colors[s] for s in sources], name="Energy Source") - + labs(title="Energy Flow · sankey-basic · letsplot · pyplots.ai") + + scale_fill_manual(values=[source_color_map[s] for s in sources], name="Energy Source") + + labs(title="Energy Flow · sankey-basic · letsplot · anyplot.ai") + theme_minimal() + theme( - plot_title=element_text(size=30, face="bold"), + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), + panel_background=element_rect(fill=PAGE_BG), + plot_title=element_text(size=30, face="bold", color=INK), axis_title=element_blank(), axis_text=element_blank(), axis_ticks=element_blank(), panel_grid=element_blank(), - legend_text=element_text(size=18), - legend_title=element_text(size=20, face="bold"), + legend_text=element_text(size=18, color=INK_SOFT), + legend_title=element_text(size=20, face="bold", color=INK), legend_position="bottom", + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), ) + scale_x_continuous(limits=[-0.02, 1.02]) + scale_y_continuous(limits=[-0.02, 1.02]) + ggsize(1600, 900) ) -# Save as PNG (scale 3x for 4800 × 2700 px) -ggsave(plot, "plot.png", path=".", scale=3) - -# Save as HTML for interactivity -ggsave(plot, "plot.html", path=".") +# Save PNG (scale 3x for 4800 × 2700 px) and HTML +ggsave(plot, f"plot-{THEME}.png", path=".", scale=3) +ggsave(plot, f"plot-{THEME}.html", path=".") diff --git a/plots/sankey-basic/metadata/python/letsplot.yaml b/plots/sankey-basic/metadata/python/letsplot.yaml index 0e02d860bc..81564a3251 100644 --- a/plots/sankey-basic/metadata/python/letsplot.yaml +++ b/plots/sankey-basic/metadata/python/letsplot.yaml @@ -1,170 +1,203 @@ library: letsplot +language: python specification_id: sankey-basic created: '2025-12-23T19:47:34Z' -updated: '2025-12-23T19:57:27Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20469999343 -issue: 0 -python_version: 3.13.11 -library_version: 4.8.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/letsplot/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/letsplot/plot.html -quality_score: 92 -impl_tags: - dependencies: [] - techniques: - - bezier-curves - - layer-composition - - html-export - patterns: - - iteration-over-groups - dataprep: [] - styling: - - alpha-blending +updated: '2026-04-30T09:26:16Z' +generated_by: claude-sonnet +workflow_run: 25156796686 +issue: 810 +python_version: 3.13.13 +library_version: 4.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/letsplot/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/letsplot/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/letsplot/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/sankey-basic/python/letsplot/plot-dark.html +quality_score: 85 review: strengths: - - Excellent manual Sankey implementation using geom_polygon for smooth bezier flows - - Clear visual hierarchy with distinct node bars and semi-transparent flows - - Good color palette that is colorblind-accessible - - Informative labels showing both names and total values (TWh units) - - Proper canvas utilization with balanced layout - - Correct title format following pyplots.ai conventions - - Clean separation between flow calculation and visualization code + - Full Sankey diagram built from scratch using letsplot primitives (geom_polygon + for flow bands, geom_rect for nodes, geom_text for labels) — correct approach + given no native Sankey geom + - Smooth bezier-like flow curves using smoothstep interpolation (ease = t*t*(3-2*t)) + produce polished, publication-quality ribbon shapes + - 'Okabe-Ito palette applied in correct canonical order: Coal=#009E73 (first), Natural + Gas=#D55E00, Nuclear=#0072B2, Renewable=#CC79A7' + - 'Theme-adaptive chrome correctly implemented: PAGE_BG, INK, INK_SOFT all flip + between light (#FAF8F1) and dark (#1A1A17) themes' + - Realistic energy flow domain with sensible TWh values; node labels include totals + (e.g., 'Natural Gas (75 TWh)') aiding interpretation + - 'All chrome tokens applied throughout: plot_background, panel_background, legend_background, + axis_text (blanked), plot_title, legend_text all reference theme variables' + - Code is deterministic (hardcoded data), clean imports, linear KISS structure weaknesses: - - Node labels could be slightly larger for better readability at full resolution - - Some minor whitespace imbalance (more space at bottom due to legend placement) - image_description: 'The plot displays a Sankey diagram visualizing energy flow from - four sources (Coal, Natural Gas, Nuclear, Renewable) on the left to three consumption - sectors (Industrial, Residential, Commercial) on the right. Each source node has - a dark navy rectangular bar with white labels showing the source name and total - TWh (e.g., "Coal (36 TWh)"). The flows are smooth bezier curves colored by energy - source: Coal is dark gray (#4A4A4A), Natural Gas is blue (#306998), Nuclear is - purple (#9B59B6), and Renewable is green (#27AE60). Flows have ~65% opacity with - thin white edges, allowing overlapping flows to remain distinguishable. Target - nodes on the right display sector names with totals (e.g., "Industrial (72 TWh)"). - The title "Energy Flow · sankey-basic · letsplot · pyplots.ai" appears at the - top left in bold. A horizontal legend at the bottom shows all four energy source - colors. The plot fills approximately 70% of the canvas with balanced margins.' + - Title in code is 'Energy Flow · sankey-basic · letsplot · anyplot.ai' — required + format is '{spec-id} · {library} · anyplot.ai' (i.e., 'sankey-basic · letsplot + · anyplot.ai'), the 'Energy Flow ·' prefix deviates from spec + - Node label text size=14 is slightly below the recommended 16pt minimum for secondary + text at 4800×2700px — increase to 16 or 18 for better legibility at full resolution + - 'No visual hierarchy or data storytelling: the dominant flow (Natural Gas → Residential, + 35 TWh) is not visually emphasized beyond its natural proportional width — a subtle + annotation or slightly different visual treatment would improve storytelling' + - 'LM-02 is low: the implementation uses only generic letsplot/ggplot primitives; + no distinctively letsplot-only features leveraged (e.g., interactive tooltips + via ggsave HTML could be improved with letsplot''s built-in interactivity)' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) — correct theme surface, not pure white. + Chrome: Title visible in bold dark INK (#1A1A17) text at top-left; node labels on left (Renewable 30 TWh, Nuclear 28 TWh, Natural Gas 75 TWh, Coal 36 TWh) and right (Commercial 40 TWh, Residential 57 TWh, Industrial 72 TWh) rendered in INK_SOFT — all readable. Legend at bottom shows "Energy Source" with four colored swatches; legible. No axis text (correctly blanked for Sankey). + Data: Four source flow bands — Coal in #009E73 (green, first/correct), Natural Gas in #D55E00 (orange), Nuclear in #0072B2 (blue), Renewable in #CC79A7 (reddish purple) — correct Okabe-Ito canonical order. Flow bands rendered at alpha=0.65 with smooth bezier-like curves. Black node rectangles clearly visible. + Legibility verdict: PASS — all text readable against warm off-white background. + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) — correct dark surface, not pure black. + Chrome: Title rendered in light INK (#F0EFE8) text — clearly readable. Node labels in INK_SOFT (#B8B7B0) — readable against dark background, no dark-on-dark failure observed. Legend text is light-colored. No axis text. + Data: Flow band colors are IDENTICAL to light render — Coal #009E73, Natural Gas #D55E00, Nuclear #0072B2, Renewable #CC79A7 — correct (only chrome flips, not data colors). Node rectangles rendered in light INK color consistent with dark theme. + Legibility verdict: PASS — all text readable against near-black background; no dark-on-dark failures detected. 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 is large and bold (~30pt), node labels are clear and readable - (~14pt scaled) + comment: Title size=30 and legend 18/20 explicitly set; node labels size=14 + is slightly below the 16pt recommended minimum for secondary text — all + readable but marginal - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text; labels are positioned outside the flow area + comment: No text or element overlap visible; source and target labels well-separated + from flow bands - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Flows are clearly visible with good alpha (0.65); white edges help - distinguish overlapping flows + comment: Flow bands at alpha=0.65 clearly visible; node rectangles prominent; + all elements distinguishable - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Four distinct colors that work well for colorblind users (gray, blue, - purple, green) + comment: Okabe-Ito palette is CVD-safe; adequate luminance differences between + all four flow colors - id: VQ-05 - name: Layout Balance + name: Layout & Canvas score: 4 - max: 5 + max: 4 passed: true - comment: Good canvas utilization (~70%), slight imbalance with more whitespace - at top/bottom than needed + comment: Sankey fills canvas well; nodes at x=0.18/0.82 with y spanning ~0.05-0.95; + good proportions for 16:9 - id: VQ-06 - name: Axis Labels - score: 0 + name: Axis Labels & Title + score: 2 max: 2 passed: true - comment: N/A for Sankey diagrams (no traditional axes) - deducting as per - criteria + comment: No traditional axis labels (correct for Sankey); node labels with + TWh values serve as data labels; title present - id: VQ-07 - name: Grid & Legend + name: Palette Compliance score: 2 max: 2 passed: true - comment: No grid (appropriate for Sankey), legend well-placed at bottom + comment: 'First series (Coal) = #009E73 correct; Okabe-Ito canonical order + maintained; backgrounds #FAF8F1 / #1A1A17 correct; chrome adaptive in both + themes' + design_excellence: + score: 12 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Well above defaults — custom-built Sankey with smooth curves, professional + color palette, clean typography; not quite FiveThirtyEight-level polish + but clearly intentional design + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Grid removed, axes blanked, warm theme-appropriate background; legend + with border adds some chrome; good whitespace around nodes + - id: DE-03 + name: Data Storytelling + score: 3 + max: 6 + passed: false + comment: Data displayed with proportional flow widths telling a basic story; + dominant flow (Natural Gas 75 TWh) is visually prominent by size but not + explicitly emphasized; no annotations or visual hierarchy highlighting key + insight spec_compliance: - score: 25 - max: 25 + score: 14 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct Sankey diagram implementation - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Source→target flows correctly mapped with proportional widths - - id: SC-03 + comment: Correct Sankey diagram with proportional flow widths, source/target + nodes, and multi-node connections + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: 'All spec features present: flow visualization, node labels, distinct - colors, link opacity' - - id: SC-04 - name: Data Range + comment: Source/target/value all mapped; node labels visible; distinct colors + per source; link opacity (alpha=0.65); no circular flows + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All flows visible, no data cut off - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Legend correctly identifies all four energy sources - - id: SC-06 - name: Title Format + comment: Flow widths correctly proportional to TWh values; all 10 flows correctly + rendered + - id: SC-04 + name: Title & Legend score: 2 - max: 2 - passed: true - comment: 'Title follows exact format: "Energy Flow · sankey-basic · letsplot - · pyplots.ai"' + max: 3 + passed: false + comment: Code has title='Energy Flow · sankey-basic · letsplot · anyplot.ai' + — extra 'Energy Flow ·' prefix deviates from required format '{spec-id} + · {library} · anyplot.ai'; legend correctly shows Energy Source with four + labeled colors data_quality: - score: 19 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows multiple sources, multiple targets, varying flow magnitudes, - crossing flows + comment: Shows multiple sources (4), multiple targets (3), varying flow magnitudes + (6-35 TWh), all inter-connections represented - id: DQ-02 name: Realistic Context - score: 6 - max: 7 + score: 5 + max: 5 passed: true - comment: Plausible energy distribution scenario with realistic proportions - (gas dominates, coal to industrial) + comment: Realistic energy sector flow (Coal/Gas/Nuclear/Renewable → Industrial/Residential/Commercial) + — neutral, real-world scenario - id: DQ-03 name: Appropriate Scale - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: TWh values are realistic for a regional energy system + comment: TWh values are plausible for national-scale energy distribution; + relative proportions (Gas > Coal > Nuclear > Renewable) are realistic code_quality: - score: 9 + score: 10 max: 10 items: - id: CQ-01 @@ -172,43 +205,63 @@ review: score: 3 max: 3 passed: true - comment: 'Linear flow: imports → data → calculations → plot → save' + comment: 'Linear structure: imports → tokens → data → layout → plot → save; + no functions or classes despite complexity' - id: CQ-02 name: Reproducibility score: 2 - max: 3 - passed: false - comment: Deterministic data (hardcoded values), but no explicit random seed - statement + max: 2 + passed: true + comment: Fully deterministic — all data hardcoded, no random generation - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: All imports are used + comment: All imported symbols are used; no unused imports - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current lets-plot API + comment: Appropriate complexity for building a Sankey from primitives; smooth + curve computation is clear and correct - 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 (scale=3) and plot-{THEME}.html correctly + library_mastery: + score: 5 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features + - id: LM-01 + name: Idiomatic Usage score: 3 max: 5 passed: true - comment: Uses ggplot grammar effectively with geom_polygon, geom_rect, geom_text, - theme_minimal, but Sankey is manually constructed rather than using a native - Sankey geom (lets-plot lacks native Sankey support, so this manual approach - is appropriate) + comment: Correct ggplot grammar with layer composition (geom_polygon + geom_rect + + geom_text), scale_fill_manual, theme system, labs — idiomatic but at primitive + level + - id: LM-02 + name: Distinctive Features + score: 2 + max: 5 + passed: false + comment: HTML export via ggsave is somewhat letsplot-distinctive; however, + the full Sankey implementation uses generic primitives that could be replicated + in plotnine with minor syntax changes verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - layer-composition + - bezier-curves + - html-export + patterns: + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending + - minimal-chrome