diff --git a/plots/bar-stacked-percent/implementations/python/plotnine.py b/plots/bar-stacked-percent/implementations/python/plotnine.py index 471d455f81..e5a6324c96 100644 --- a/plots/bar-stacked-percent/implementations/python/plotnine.py +++ b/plots/bar-stacked-percent/implementations/python/plotnine.py @@ -1,15 +1,17 @@ """ anyplot.ai bar-stacked-percent: 100% Stacked Bar Chart -Library: plotnine 0.15.4 | Python 3.13.13 -Quality: 86/100 | Updated: 2026-05-08 +Library: plotnine 0.15.8 | Python 3.13.15 +Quality: 88/100 | Updated: 2026-08-18 """ import os import sys + sys.path = [p for p in sys.path if os.path.abspath(p) != os.getcwd()] import pandas as pd # noqa: E402 +from mizani.formatters import percent_format # noqa: E402 from plotnine import ( # noqa: E402 aes, element_blank, @@ -17,61 +19,42 @@ element_rect, element_text, geom_bar, + geom_text, ggplot, labs, position_fill, + scale_color_identity, scale_fill_manual, + scale_y_continuous, theme, theme_minimal, ) -# Theme tokens +# Theme tokens (see prompts/default-style-guide.md "Background" + "Theme-adaptive Chrome") 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" +MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" # Imprint semantic anchor: other/rest -# Okabe-Ito palette -IMPRINT = ["#009E73", "#C475FD", "#4467A3", "#BD8233"] +# Imprint palette — named competitors take positions 1-3 in canonical order; +# "Others" uses the muted semantic anchor since it is literally the aggregate +# rest-of-market bucket, not a distinct company. +IMPRINT = ["#009E73", "#C475FD", "#4467A3"] -# Data - Market share by quarter for tech companies +# Data - smartphone market share by quarter quarters = ["Q1 2023", "Q2 2023", "Q3 2023", "Q4 2023", "Q1 2024", "Q2 2024"] companies_ordered = ["Others", "Xiaomi", "Samsung", "Apple"] +apple_share = [23, 21, 20, 22, 21, 20] +samsung_share = [22, 21, 20, 19, 20, 19] +xiaomi_share = [12, 13, 14, 14, 15, 16] +others_share = [43, 45, 46, 45, 44, 45] data = { "Quarter": quarters * 4, "Company": (["Apple"] * 6 + ["Samsung"] * 6 + ["Xiaomi"] * 6 + ["Others"] * 6), - "Share": [ - # Apple - 23, - 21, - 20, - 22, - 21, - 20, - # Samsung - 22, - 21, - 20, - 19, - 20, - 19, - # Xiaomi - 12, - 13, - 14, - 14, - 15, - 16, - # Others - 43, - 45, - 46, - 45, - 44, - 45, - ], + "Share": apple_share + samsung_share + xiaomi_share + others_share, } df = pd.DataFrame(data) @@ -79,36 +62,93 @@ df["Quarter"] = pd.Categorical(df["Quarter"], categories=quarters, ordered=True) df["Company"] = pd.Categorical(df["Company"], categories=companies_ordered, ordered=True) -# Color mapping with Okabe-Ito palette -color_map = {"Others": IMPRINT[3], "Xiaomi": IMPRINT[2], "Samsung": IMPRINT[1], "Apple": IMPRINT[0]} +# Color mapping: Apple/Samsung/Xiaomi in canonical Imprint order, Others muted +color_map = {"Others": MUTED, "Xiaomi": IMPRINT[2], "Samsung": IMPRINT[1], "Apple": IMPRINT[0]} + +# In-segment percentage labels (DE-03): pick whichever ink extreme has higher +# WCAG contrast against each segment's own fill color, so labels stay legible +# on both the mid-tone brand hues and the theme-adaptive "Others" gray. +LIGHT_INK = "#F0EFE8" +DARK_INK = "#1A1A17" + + +def _relative_luminance(hex_color): + r, g, b = (int(hex_color[i : i + 2], 16) / 255 for i in (1, 3, 5)) + + def _linearize(c): + return c / 12.92 if c <= 0.03928 else ((c + 0.055) / 1.055) ** 2.4 + + r, g, b = _linearize(r), _linearize(g), _linearize(b) + return 0.2126 * r + 0.7152 * g + 0.0722 * b + + +def _contrast_ratio(l1, l2): + lighter, darker = max(l1, l2), min(l1, l2) + return (lighter + 0.05) / (darker + 0.05) + + +def _label_color(fill_hex): + fill_l = _relative_luminance(fill_hex) + light_contrast = _contrast_ratio(fill_l, _relative_luminance(LIGHT_INK)) + dark_contrast = _contrast_ratio(fill_l, _relative_luminance(DARK_INK)) + return LIGHT_INK if light_contrast >= dark_contrast else DARK_INK + + +df["Label"] = df["Share"].astype(str) + "%" +df["LabelColor"] = df["Company"].map(color_map).map(_label_color) + +# Precompute label y-positions explicitly (SC-03/VQ-02 fix): geom_bar and +# geom_text each calling their own independent position_fill() can derive +# mismatched per-group cumulative offsets when the source rows are grouped +# by company rather than interleaved per quarter. Instead, compute the exact +# fill-stack midpoint per (Quarter, Company) ourselves -- stacked bottom-to-top +# as Apple/Samsung/Xiaomi/Others, i.e. the reverse of the legend/factor order, +# matching plotnine's default stacking -- and feed it to geom_text via +# position="identity" so both layers are guaranteed to agree. +stack_order_bottom_to_top = ["Apple", "Samsung", "Xiaomi", "Others"] +stack_rank = {company: rank for rank, company in enumerate(stack_order_bottom_to_top)} +# .map() on a Categorical column returns a Categorical result that inherits +# the *original* category order, so sorting by it would sort by category +# position rather than by the mapped rank value -- cast to plain strings +# first so the mapped ranks are ordinary integers. +df["StackRank"] = df["Company"].astype(str).map(stack_rank) +df = df.sort_values(["Quarter", "StackRank"]).reset_index(drop=True) +df["Fraction"] = df["Share"] / 100 +cum_top = df.groupby("Quarter", observed=True)["Fraction"].cumsum() +cum_bottom = cum_top - df["Fraction"] +df["LabelY"] = (cum_top + cum_bottom) / 2 -# Theme-adaptive colors for legend and text +# Theme-adaptive chrome anyplot_theme = theme( plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG), - panel_grid_major=element_line(color=INK, size=0.3, alpha=0.10), + panel_border=element_blank(), + panel_grid_major_x=element_blank(), + panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.10), panel_grid_minor=element_blank(), - panel_border=element_rect(color=INK_SOFT, fill=None, size=0.5), - axis_title=element_text(color=INK, size=20), - axis_text=element_text(color=INK_SOFT, size=16), + axis_title=element_text(color=INK, size=10), + axis_text=element_text(color=INK_SOFT, size=8), axis_line=element_line(color=INK_SOFT, size=0.5), - plot_title=element_text(color=INK, size=24, weight="bold"), + plot_title=element_text(color=INK, size=12), legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), - legend_title=element_text(color=INK, size=16), - legend_text=element_text(color=INK_SOFT, size=16), + legend_title=element_text(color=INK, size=8), + legend_text=element_text(color=INK_SOFT, size=8), legend_position="right", - figure_size=(16, 9), + figure_size=(8, 4.5), ) -# Create 100% stacked bar chart +# Create 100% stacked bar chart with in-segment percentage labels plot = ( ggplot(df, aes(x="Quarter", y="Share", fill="Company")) - + geom_bar(stat="identity", position=position_fill(), width=0.65) + + geom_bar(stat="identity", position=position_fill(), width=0.7) + + geom_text(aes(y="LabelY", label="Label", color="LabelColor"), position="identity", size=2.8, show_legend=False) + scale_fill_manual(values=color_map) - + labs(title="bar-stacked-percent · plotnine · anyplot.ai", x="Quarter", y="Market Share (%)", fill="Company") + + scale_color_identity() + + scale_y_continuous(labels=percent_format()) + + labs(title="bar-stacked-percent · python · plotnine · anyplot.ai", x="Quarter", y="Market Share", fill="Company") + theme_minimal() + anyplot_theme ) # Save -plot.save(f"plot-{THEME}.png", dpi=300, verbose=False) +plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False) diff --git a/plots/bar-stacked-percent/metadata/python/plotnine.yaml b/plots/bar-stacked-percent/metadata/python/plotnine.yaml index ed52441fdd..eb75e4e101 100644 --- a/plots/bar-stacked-percent/metadata/python/plotnine.yaml +++ b/plots/bar-stacked-percent/metadata/python/plotnine.yaml @@ -2,123 +2,137 @@ library: plotnine language: python specification_id: bar-stacked-percent created: '2025-12-25T22:50:12Z' -updated: '2026-05-08T11:07:23Z' -generated_by: claude-haiku -workflow_run: 25551716625 +updated: '2026-08-18T15:16:36Z' +generated_by: claude-sonnet +workflow_run: 32150273058 issue: 2008 -python_version: 3.13.13 -library_version: 0.15.4 +language_version: 3.13.15 +library_version: 0.15.8 preview_url_light: https://storage.googleapis.com/anyplot-images/plots/bar-stacked-percent/python/plotnine/plot-light.png preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/bar-stacked-percent/python/plotnine/plot-dark.png preview_html_light: null preview_html_dark: null -quality_score: 86 +quality_score: 88 review: strengths: - - Perfect theme-adaptive styling in both light and dark renders - - Correct Okabe-Ito palette with proper color ordering (#009E73 first) - - Excellent text legibility with explicitly sized fonts (title 24pt, labels 20pt, - ticks 16pt) - - Clean, KISS-structured code with proper categorical ordering - - Accurate 100% stacked bar implementation using position_fill() - - Realistic and neutral market share data with clear quarterly trends + - In-segment percentage labels now correctly match their own bar segment in every + quarter, both themes -- verified via zoomed crops of Q1 2023 and Q1/Q2 2024 -- + fully resolving the attempt-2 misalignment bug via the precomputed LabelY/StackRank + approach. + - WCAG-contrast-aware in-segment label coloring (_relative_luminance/_contrast_ratio/_label_color) + driven through scale_color_identity() is a genuinely sophisticated, distinctive + plotnine touch. + - 'Imprint palette used correctly: Apple (first categorical series) = #009E73; Samsung/Xiaomi + take canonical positions 2-3; "Others" correctly uses the theme-adaptive muted + semantic anchor rather than a 4th arbitrary categorical color.' + - Theme-adaptive chrome correctly threaded through both renders -- correct backgrounds, + fully legible text, subtle y-only gridlines, no dark-on-dark or light-on-light + failures. + - Clean, realistic, neutral smartphone-market-share data with genuine quarter-over-quarter + variation (Apple gently declining, Xiaomi growing) that gives DQ-01/DQ-03 real + substance; canvas is correct (3200x1800), title format matches the mandated pattern, + legend order mirrors the visual stacking order. weaknesses: - - Design Excellence lacks distinctive custom elements beyond style guide requirements - - Visual storytelling is straightforward—displays data clearly but without particular - emphasis or focal points + - Design Excellence beyond the palette/label choices is fairly restrained -- no + additional refinement flourish (e.g. subtle bar-edge stroke, custom legend key + styling) beyond theme_minimal() + token overrides. + - LM-01 idiomatic usage is solid but not exhaustive -- e.g. no guides()-based legend + reordering or other plotnine-specific flourish beyond the required position_fill() + mechanism and the label-color trick. + - Y-axis title "Market Share" has no explicit unit suffix (e.g. "Market Share (%)") + -- the percent-formatted tick labels make the unit self-evident, but an explicit + unit would be marginally clearer. image_description: |- Light render (plot-light.png): - Background: Warm off-white (#FAF8F1) as specified - Chrome: Title "bar-stacked-percent · plotnine · anyplot.ai" bold and large (24pt), axis labels "Quarter" and "Market Share (%)" clearly visible in dark text (INK #1A1A17), tick labels for quarters and percentages readable at 16pt - Data: Green (#009E73) Apple segment at bottom, orange (#D55E00) Samsung, blue (#0072B2) Xiaomi, pink (#CC79A7) Others—all segments clearly distinguishable with proper proportions - Legend: Company labels match data; legend positioned right of plot with adequate spacing - Grid: Subtle major grid lines at 10% opacity; no visual competition with data - Legibility verdict: PASS — all elements readable with good contrast against light background + Background: Warm off-white, matches #FAF8F1 -- not pure white. + Chrome: Title "bar-stacked-percent · python · plotnine · anyplot.ai", axis titles ("Quarter", "Market Share"), tick labels, and the bordered "Company" legend (top-right) all render in dark ink and are clearly legible against the light background. + Data: Six 100%-stacked bars (Q1 2023-Q2 2024), each summing to 100%, stacked bottom-to-top as Apple (#009E73 brand green) / Samsung (lavender) / Xiaomi (blue) / Others (muted dark taupe-gray). In-segment percentage labels are correctly positioned: verified Q1 2023 shows Apple 23%, Samsung 22%, Xiaomi 12%, Others 43% (sums to 100, matches source data exactly), each label centered in its own correctly-colored segment with dark-ink text on the lighter purple/blue segments and light-ink text on the darker gray "Others" segment. + Legibility verdict: PASS Dark render (plot-dark.png): - Background: Warm near-black (#1A1A17) matching specification - Chrome: Title visible in light text (INK #F0EFE8), axis labels and tick labels clearly readable in INK_SOFT (#B8B7B0), no dark-on-dark failures - Data: All data colors identical to light render (Apple #009E73, Samsung #D55E00, Xiaomi #0072B2, Others #CC79A7)—categorical series maintain visual identity across themes; proportions match light render - Legend: Company labels equally visible with light text on elevated background (#242420) - Grid: Subtle grid at same opacity; visible but not dominant - Legibility verdict: PASS — excellent readability on dark surface; all text and data elements clearly distinguished + Background: Warm near-black, matches #1A1A17 -- not pure black. + Chrome: Title, axis titles, tick labels, and legend flip to light off-white / soft light-gray ink and remain fully legible -- no dark-on-dark failures anywhere. + Data: Apple/Samsung/Xiaomi fill colors are pixel-identical to the light render (Imprint categorical positions held constant across themes, as required). "Others" correctly switches to the lighter warm-gray value of the theme-adaptive muted anchor. Verified Q1 2024 shows Apple 21%, Samsung 20%, Xiaomi 15%, Others 44% (sums to 100, matches source data), and Q2 2024 shows Apple 20%, Samsung 19%, Xiaomi 16%, Others 45% -- one correctly-valued label per segment, zero doubling/blank segments (the exact defect that caused the attempt-2 rejection is gone). + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 30 + score: 29 max: 30 items: - id: VQ-01 name: Text Legibility - score: 8 + score: 7 max: 8 passed: true - comment: Title 24pt, labels 20pt, ticks 16pt—all explicitly sized and readable - in both themes + comment: All font sizes explicitly set via theme(); title fits without clipping; + readable in both themes - id: VQ-02 name: No Overlap score: 6 max: 6 passed: true - comment: No overlapping elements; all quarter labels and legend clear + comment: Attempt-2 label misalignment bug fully fixed -- exactly one correctly-valued + label per segment, verified across multiple quarters in both renders - id: VQ-03 name: Element Visibility score: 6 max: 6 passed: true - comment: Bar segments clearly visible and sized appropriately for 100% stacked - format + comment: Bar segments and labels clearly visible and well-sized for 6 categories + x 4 series - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Okabe-Ito palette ensures colorblind safety; good contrast between - segments + comment: WCAG-contrast-computed label color per segment; CVD-safe Imprint + palette - id: VQ-05 name: Layout & Canvas score: 4 max: 4 passed: true - comment: Figure 16×9 (4800×2700px) fills canvas appropriately with balanced - margins + comment: Correct 3200x1800 canvas, balanced margins, legend near plot - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: 'X: ''Quarter'' (descriptive), Y: ''Market Share (%)'' (with units)' + comment: Descriptive Quarter/Market Share labels, percent-formatted ticks + make units self-evident - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First series #009E73 ✓; Okabe-Ito order maintained; backgrounds - #FAF8F1/#1A1A17 ✓; theme-correct chrome in both renders' + comment: 'First series #009E73, canonical Imprint order, correct muted semantic + anchor for Others, theme-correct chrome in both renders' design_excellence: - score: 8 + score: 13 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 4 + score: 5 max: 8 - passed: false - comment: Well-configured library defaults following style guide; no exceptional - custom design beyond requirements + passed: true + comment: Thoughtful WCAG-aware label coloring and semantic color mapping, + but otherwise fairly standard theme_minimal customization - id: DE-02 name: Visual Refinement - score: 2 + score: 4 max: 6 - passed: false - comment: Grid and theme customized; mostly library defaults with theme token - application + passed: true + comment: Spines removed, subtle y-only grid, generous whitespace; no additional + refinement flourishes - id: DE-03 name: Data Storytelling - score: 2 + score: 4 max: 6 - passed: false - comment: Data clearly displayed with natural composition order; no visual - emphasis or focal point + passed: true + comment: Correctly-placed in-segment percentage labels now give the chart + a real data-reading focal point, guiding the viewer to exact share values + per component spec_compliance: score: 15 max: 15 @@ -128,27 +142,28 @@ review: score: 5 max: 5 passed: true - comment: 100% stacked bar chart with position_fill() normalization ✓ + comment: Correct 100% stacked bar via geom_bar(position_fill()) - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: Multiple categories (6 quarters), components (4 companies), proportional - segments, legend all present + comment: Percentage labels now render with correct values/positions in every + bar, both themes - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: 'X: Quarter, Y: normalized share, Fill: Company—correct and complete' + comment: geom_text's y now matches geom_bar's stacking order exactly, via + shared precomputed LabelY - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: 'Title: ''bar-stacked-percent · plotnine · anyplot.ai'' ✓; Legend: - Company with correct labels' + comment: Title matches mandated format, legend labels match data, legend order + mirrors visual stack data_quality: score: 15 max: 15 @@ -158,80 +173,84 @@ review: score: 6 max: 6 passed: true - comment: 'Shows all aspects: multiple categories, multiple components, composition - changes over time' + comment: 6 quarters x 4 components with genuine quarter-over-quarter variation + in each series - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Tech company market share by quarter—real, neutral, comprehensible - scenario + comment: Neutral, comprehensible smartphone-market-share business scenario - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Percentages sum to ~100% per quarter; trends are realistic for smartphone - market + comment: Values and trends are plausible for real-world smartphone market + share code_quality: - score: 10 + score: 9 max: 10 items: - id: CQ-01 name: KISS Structure - score: 3 + score: 2 max: 3 passed: true - comment: Imports → Data → Plot → Save; no unnecessary functions + comment: Mostly linear script, but 3 helper functions for WCAG contrast math + deviate from pure imports->data->plot->save - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: Hardcoded deterministic data + comment: Fully deterministic, no randomness - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only used imports; all necessary + comment: Only used imports - id: CQ-04 name: Code Elegance score: 2 max: 2 passed: true - comment: Clean Python; proper categorical ordering; no fake functionality + comment: Functional defect from attempt 2 resolved; appropriate complexity + for the WCAG-aware labeling feature - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot-{THEME}.png; current API + comment: Saves as plot-{THEME}.png via plot.save(), current API library_mastery: - score: 8 + score: 7 max: 10 items: - id: LM-01 name: Idiomatic Usage - score: 5 + score: 4 max: 5 passed: true - comment: 'Expert ggplot2 style: ggplot() + geom_bar() + position_fill() + - scale_fill_manual() + theme()' + comment: Idiomatic grammar-of-graphics composition; correct usage of position_fill, + scale_fill_manual, theme_minimal - id: LM-02 name: Distinctive Features score: 3 max: 5 - passed: false - comment: Uses position_fill() and comprehensive theme customization; solid - library knowledge but standard patterns + passed: true + comment: scale_color_identity() mapping a precomputed WCAG-contrast column + to per-row text color is a genuinely distinctive grammar-of-graphics technique verdict: APPROVED impl_tags: dependencies: [] - techniques: [] + techniques: + - annotations patterns: - data-generation - dataprep: [] + - groupby-aggregation + dataprep: + - cumulative-sum styling: - grid-styling