diff --git a/plots/line-stress-strain/implementations/python/pygal.py b/plots/line-stress-strain/implementations/python/pygal.py new file mode 100644 index 0000000000..8ed72dc197 --- /dev/null +++ b/plots/line-stress-strain/implementations/python/pygal.py @@ -0,0 +1,169 @@ +""" anyplot.ai +line-stress-strain: Engineering Stress-Strain Curve +Library: pygal 3.1.3 | Python 3.13.14 +Quality: 79/100 | Created: 2026-06-21 +""" + +import os +import sys + + +# Prevent this file (pygal.py) from shadowing the installed pygal package +_this_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path = [p for p in sys.path if os.path.abspath(p or ".") != _this_dir] + +import numpy as np +import pygal +from pygal.style import Style + + +# Theme tokens — Imprint palette, 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" +INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F" + +IMPRINT_PALETTE = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314") + +# Data — engineering stress-strain curves for two classic materials +np.random.seed(42) + +# ── ASTM A36 Mild Steel: distinct Lüders plateau and ductile failure ── +E_MILD = 200_000.0 # Young's modulus, MPa +SY_MILD = 250.0 # Yield strength, MPa +eps_y_mild = SY_MILD / E_MILD # elastic-limit strain ~0.00125 + +# Elastic region (linear) +s_e_mild = np.linspace(0.0, eps_y_mild, 30) +sig_e_mild = E_MILD * s_e_mild + +# Lüders plateau (slight hardening before macroscopic work hardening) +s_pl_mild = np.linspace(eps_y_mild, 0.013, 18) +sig_pl_mild = SY_MILD + 22.0 * ((s_pl_mild - eps_y_mild) / (0.013 - eps_y_mild)) ** 2 + +# Strain hardening (power-law) +s_sh_mild = np.linspace(0.013, 0.210, 88) +sig_sh_mild = 272.0 + 178.0 * ((s_sh_mild - 0.013) / (0.210 - 0.013)) ** 0.52 + +# Necking to fracture (engineering stress drops as cross-section shrinks) +SU_MILD = float(sig_sh_mild[-1]) # UTS ≈ 450 MPa +s_nk_mild = np.linspace(0.210, 0.270, 22) +sig_nk_mild = SU_MILD * (1.0 - 0.27 * ((s_nk_mild - 0.210) / 0.060) ** 0.45) + +strain_mild = np.concatenate([s_e_mild, s_pl_mild[1:], s_sh_mild[1:], s_nk_mild[1:]]) +stress_mild = np.concatenate([sig_e_mild, sig_pl_mild[1:], sig_sh_mild[1:], sig_nk_mild[1:]]) +stress_mild += np.random.normal(0, 1.2, len(stress_mild)) +stress_mild = np.clip(stress_mild, 0.0, None) + +n_e_mild = len(s_e_mild) # elastic region length +uts_idx_mild = int(np.argmax(stress_mild)) + +# ── 6061-T6 Aluminum Alloy: smooth curve, no yield plateau ────────── +E_AL = 68_500.0 # Young's modulus, MPa +SY_AL = 276.0 # 0.2% offset yield strength, MPa +SU_AL = 310.0 # UTS, MPa +eps_y_al = SY_AL / E_AL # ~0.00403 + +# Elastic region +s_e_al = np.linspace(0.0, eps_y_al, 25) +sig_e_al = E_AL * s_e_al + +# Continuous strain hardening (saturating exponential — no Lüders band) +s_p_al = np.linspace(eps_y_al, 0.115, 130) +t_al = (s_p_al - eps_y_al) / (0.115 - eps_y_al) +sig_p_al = SY_AL + (SU_AL - SY_AL) * (1.0 - np.exp(-5.5 * t_al)) + +# Necking onset at ~0.095 strain +nk_start = int(np.argmin(np.abs(s_p_al - 0.095))) +sig_p_al[nk_start:] = sig_p_al[nk_start] * (1.0 - 0.21 * np.linspace(0.0, 1.0, len(sig_p_al) - nk_start) ** 0.5) + +strain_al = np.concatenate([s_e_al, s_p_al[1:]]) +stress_al = np.concatenate([sig_e_al, sig_p_al[1:]]) +stress_al += np.random.normal(0, 0.7, len(strain_al)) +stress_al = np.clip(stress_al, 0.0, None) + +n_e_al = len(s_e_al) +uts_idx_al = int(np.argmax(stress_al)) + +# ── 0.2% Offset Line — illustrates yield determination for aluminum ── +OFFSET_STRAIN = 0.002 # 0.2% = 0.002 in strain units +s_off = np.linspace(OFFSET_STRAIN, eps_y_al + 0.003, 30) +sig_off = E_AL * (s_off - OFFSET_STRAIN) # parallel to elastic, shifted right + +# ── Convert to pygal XY format (key points get tooltip labels) ────── +mild_key_labels = { + n_e_mild - 1: f"Yield Point — {stress_mild[n_e_mild - 1]:.0f} MPa", + uts_idx_mild: f"UTS — {stress_mild[uts_idx_mild]:.0f} MPa", + len(strain_mild) - 1: f"Fracture — {stress_mild[-1]:.0f} MPa", +} +mild_data = [] +for i, (s, sig) in enumerate(zip(strain_mild, stress_mild, strict=True)): + pt = (round(float(s), 6), round(float(sig), 2)) + if i in mild_key_labels: + mild_data.append({"value": pt, "label": mild_key_labels[i]}) + else: + mild_data.append(pt) + +al_key_labels = { + n_e_al - 1: f"0.2% Offset Yield — {stress_al[n_e_al - 1]:.0f} MPa", + uts_idx_al: f"UTS — {stress_al[uts_idx_al]:.0f} MPa", + len(strain_al) - 1: f"Fracture — {stress_al[-1]:.0f} MPa", +} +al_data = [] +for i, (s, sig) in enumerate(zip(strain_al, stress_al, strict=True)): + pt = (round(float(s), 6), round(float(sig), 2)) + if i in al_key_labels: + al_data.append({"value": pt, "label": al_key_labels[i]}) + else: + al_data.append(pt) + +offset_data = [(round(float(s), 6), round(float(sig), 2)) for s, sig in zip(s_off, sig_off, strict=True)] + +# ── Chart style ────────────────────────────────────────────────────── +title = "line-stress-strain · python · pygal · anyplot.ai" +n_chars = len(title) +title_fs = round(66 * (67 / n_chars)) if n_chars > 67 else 66 + +custom_style = Style( + background=PAGE_BG, + plot_background=PAGE_BG, + foreground=INK, + foreground_strong=INK, + foreground_subtle=INK_MUTED, + colors=IMPRINT_PALETTE, + title_font_size=title_fs, + label_font_size=56, + major_label_font_size=44, + legend_font_size=44, + value_font_size=36, + stroke_width=3.5, +) + +# ── Build chart ────────────────────────────────────────────────────── +chart = pygal.XY( + style=custom_style, + width=3200, + height=1800, + title=title, + x_title="Engineering Strain (dimensionless)", + y_title="Engineering Stress (MPa)", + stroke=True, + show_dots=False, + fill=False, + show_x_guides=False, + show_y_guides=True, + legend_at_bottom=True, + legend_at_bottom_columns=3, + truncate_legend=40, +) + +chart.add("ASTM A36 Mild Steel", mild_data) +chart.add("6061-T6 Aluminum", al_data) +chart.add("0.2% Offset Line (Al)", offset_data, stroke_style={"width": 2, "dasharray": "8, 6", "linecap": "round"}) + +# Save PNG and HTML (pygal is interactive) +chart.render_to_png(f"plot-{THEME}.png") +with open(f"plot-{THEME}.html", "wb") as f: + f.write(chart.render()) diff --git a/plots/line-stress-strain/metadata/python/pygal.yaml b/plots/line-stress-strain/metadata/python/pygal.yaml new file mode 100644 index 0000000000..8a87e25351 --- /dev/null +++ b/plots/line-stress-strain/metadata/python/pygal.yaml @@ -0,0 +1,266 @@ +library: pygal +language: python +specification_id: line-stress-strain +created: '2026-06-21T09:41:50Z' +updated: '2026-06-21T09:48:31Z' +generated_by: claude-sonnet +workflow_run: 27900138421 +issue: 4413 +language_version: 3.13.14 +library_version: 3.1.3 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/pygal/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/pygal/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/pygal/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/line-stress-strain/python/pygal/plot-dark.html +quality_score: 79 +review: + strengths: + - Physically accurate dual-material stress-strain dataset with correct ASTM A36 + and 6061-T6 aluminum parameters (E, yield, UTS) + - 'Excellent data quality: covers elastic, Luders plateau, strain hardening, and + necking regions for both materials' + - 'Perfect code quality: KISS structure, reproducible (seed=42), clean imports, + saves PNG + HTML correctly' + - 'Correct use of Imprint palette in canonical order -- first series is #009E73, + theme-adaptive chrome in both renders' + - 'Both renders are fully readable: no dark-on-dark in dark theme, no light-on-light + in light theme' + - Idiomatic pygal.XY usage with per-point tooltip label dicts and stroke_style for + dashed offset line + weaknesses: + - 'Missing visible region labels on the PNG: the spec requires labeling key curve + regions (elastic, plastic/strain hardening, necking) directly on the chart -- + these are absent in the static render (tooltip-only labels do not appear in the + PNG)' + - 'Missing visible critical-point markers in PNG: yield point, UTS, and fracture + point are only accessible via pygal hover tooltips (HTML) -- the spec requires + them to be marked in the static output' + - 'Missing elastic modulus slope annotation: the spec explicitly requires annotating + Young''s modulus as a slope line or text annotation in the elastic region -- this + is entirely absent' + - The 0.2% offset line is too thin (stroke width=2 vs data series width=3.5) and + barely distinguishable, especially in the dark render -- increase to width=4 and + consider a more distinct dasharray + - Design is a well-configured library default with no visual refinement beyond the + style object -- all spines present, no emphasis elements, chart reads as functional + rather than polished + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1) -- correct, not pure white + Chrome: Title "line-stress-strain · python · pygal · anyplot.ai" in dark ink spans ~65% of width, clearly readable. Y-axis label "Engineering Stress (MPa)" rotated vertically, readable. X-axis label "Engineering Strain (dimensionless)" clearly readable. Tick labels (0, 0.02, 0.04...0.26 on X; 0, 100, 200...400 on Y) all readable in dark ink. + Data: Green (#009E73) curve for ASTM A36 Mild Steel shows clear Luders plateau and strain hardening to UTS ~450 MPa at strain ~0.21, then necking to fracture at ~0.27. Lavender (#C475FD) curve for 6061-T6 Aluminum shows smooth hardening to UTS ~310 MPa at ~0.095 strain then necking to fracture at ~0.12. Thin dashed blue (#4467A3) offset line visible but thin near x=0.002-0.007. No visible point markers or region labels in the PNG -- these are tooltip-only. + Legibility verdict: PASS -- all chrome readable on light background + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17) -- correct, not pure black + Chrome: Title, axis labels, and tick labels all render in light-colored text (#F0EFE8 range), clearly readable against dark background. No dark-on-dark failures observed. Legend at bottom shows light text for all three series labels. + Data: Data colors IDENTICAL to light render -- green (#009E73) steel curve, lavender (#C475FD) aluminum curve, thin dashed blue (#4467A3) offset line. Curves are clearly distinguishable against the dark background. No tooltip/annotation markers visible in static PNG. + Legibility verdict: PASS -- all chrome readable on dark background, no dark-on-dark failures + criteria_checklist: + visual_quality: + score: 26 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set via Style object. Title, axis labels, + ticks all readable in both themes. Proportions good. + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text overlap. Legend at bottom clean. Line series overlap in elastic + region is acceptable data behavior. + - id: VQ-03 + name: Element Visibility + score: 4 + max: 6 + passed: true + comment: Main curves well visible. The 0.2% offset line is thin (width=2) + and barely distinguishable. Critical point markers absent from PNG (tooltip-only). + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Green/purple/blue combination is CVD-safe. Distinguishable without + relying on hue alone. + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: Plot fills canvas well. Steel data goes to x=0.27, aluminum stops + at x=0.12 leaving lower-right area sparse -- data-driven, acceptable. Legend + at bottom is clean. + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: 'X-axis: ''Engineering Strain (dimensionless)'', Y-axis: ''Engineering + Stress (MPa)'' -- both descriptive with units.' + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Imprint palette in canonical order: #009E73 (steel), #C475FD (aluminum), + #4467A3 (offset line). Backgrounds correct (#FAF8F1 light, #1A1A17 dark). + Data colors identical across themes.' + design_excellence: + score: 9 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 4 + max: 8 + passed: false + comment: Well-configured library default. Custom style with Imprint palette + and theme tokens. No visual hierarchy beyond color. No emphasis or focal + points. + - id: DE-02 + name: Visual Refinement + score: 3 + max: 6 + passed: false + comment: Y-axis-only grid is a good choice. Legend at bottom is clean. But + all spines present (pygal constraint), no additional refinement beyond default. + - id: DE-03 + name: Data Storytelling + score: 2 + max: 6 + passed: false + comment: Two-material comparison tells a basic story. But region labels, critical + point annotations, and elastic modulus annotation -- all spec-required storytelling + elements -- are absent from the static PNG. + spec_compliance: + score: 12 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: 'Correct chart type: XY line chart for stress-strain curve.' + - id: SC-02 + name: Required Features + score: 1 + max: 4 + passed: false + comment: 'Spec requires: (1) region labels -- absent in PNG; (2) critical + point markers (yield, UTS, fracture) -- tooltip-only, absent in PNG; (3) + elastic modulus slope annotation -- absent; (4) 0.2% offset line -- present + but thin. Multi-material overlay done. 3 of 4 required annotation features + missing from static render.' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Strain on X, stress (MPa) on Y. Both materials fully displayed. + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title 'line-stress-strain · python · pygal · anyplot.ai' is correct + format. Legend labels 'ASTM A36 Mild Steel', '6061-T6 Aluminum', '0.2% Offset + Line (Al)' are descriptive. + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: 'Covers all stress-strain regions: elastic, Luders plateau (mild + steel), strain hardening, necking, fracture. Both materials show different + behaviors (Luders band vs smooth hardening).' + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Real engineering materials (ASTM A36 mild steel, 6061-T6 aluminum) + with real designations. Neutral scientific context. + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: 'ASTM A36: E=200 GPa, yield=250 MPa, UTS~450 MPa -- correct. 6061-T6: + E=68.5 GPa, yield=276 MPa, UTS=310 MPa -- correct. Noise levels realistic.' + code_quality: + score: 10 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: Clean imports -> data -> chart -> save. No functions or classes. + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: np.random.seed(42) set. + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: os, sys, numpy, pygal, pygal.style.Style -- all used. + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: Clean, Pythonic. Dict format for key point tooltips is appropriate. + No over-engineering. + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-{THEME}.png and plot-{THEME}.html correctly for interactive + pygal. + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 4 + max: 5 + passed: true + comment: Good use of pygal.XY for continuous coordinate data. Style object + used correctly. stroke_style with dasharray for dashed line. Correct HTML + export pattern. + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: false + comment: Uses per-point label dict format for tooltip annotations (pygal-specific). + HTML export with interactive tooltips. stroke_style dasharray is pygal-distinctive. + verdict: REJECTED +impl_tags: + dependencies: [] + techniques: + - hover-tooltips + - html-export + patterns: + - data-generation + dataprep: [] + styling: []