Skip to content
Merged
159 changes: 159 additions & 0 deletions plots/line-stress-strain/implementations/plotnine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
""" pyplots.ai
line-stress-strain: Engineering Stress-Strain Curve
Library: plotnine 0.15.3 | Python 3.14.3
Quality: 90/100 | Created: 2026-03-20
"""

import numpy as np
import pandas as pd
from plotnine import (
aes,
annotate,
coord_cartesian,
element_blank,
element_line,
element_rect,
element_text,
geom_line,
geom_point,
geom_segment,
geom_text,
ggplot,
labs,
scale_color_identity,
scale_fill_identity,
scale_linetype_identity,
scale_size_identity,
scale_x_continuous,
scale_y_continuous,
theme,
theme_minimal,
)


# Data - Mild steel stress-strain curve
np.random.seed(42)

youngs_modulus = 210000 # MPa
yield_stress = 250 # MPa
uts = 400 # MPa
fracture_strain = 0.35
necking_strain = 0.22

# Elastic region (0 to yield)
elastic_strain = np.linspace(0, yield_stress / youngs_modulus, 40)
elastic_stress = youngs_modulus * elastic_strain

# Yield plateau (short flat region for mild steel)
plateau_strain = np.linspace(elastic_strain[-1], 0.025, 15)
plateau_stress = np.full_like(plateau_strain, yield_stress)

# Strain hardening (power law)
hardening_strain = np.linspace(0.025, necking_strain, 80)
hardening_stress = yield_stress + (uts - yield_stress) * ((hardening_strain - 0.025) / (necking_strain - 0.025)) ** 0.45

# Necking to fracture (stress decreases)
necking_strain_vals = np.linspace(necking_strain, fracture_strain, 40)
necking_stress = (
uts - (uts - 320) * ((necking_strain_vals - necking_strain) / (fracture_strain - necking_strain)) ** 1.3
)

# Combine all regions
strain = np.concatenate([elastic_strain, plateau_strain[1:], hardening_strain[1:], necking_strain_vals[1:]])
stress = np.concatenate([elastic_stress, plateau_stress[1:], hardening_stress[1:], necking_stress[1:]])

df = pd.DataFrame({"strain": strain, "stress": stress})

# 0.2% offset line data - extended for better visibility
offset = 0.002
offset_strain_start = offset
offset_strain_end = (yield_stress + 50) / youngs_modulus + offset

# Key points
yield_point_strain = yield_stress / youngs_modulus + offset
yield_point_stress = yield_stress
uts_strain = necking_strain
uts_stress = uts
fracture_strain_pt = fracture_strain
fracture_stress_pt = necking_stress[-1]

df_points = pd.DataFrame(
{
"strain": [yield_point_strain, uts_strain, fracture_strain_pt],
"stress": [yield_point_stress, uts_stress, fracture_stress_pt],
"label": ["Yield Point\n(0.2% offset)", "UTS", "Fracture"],
"color": ["#C0392B", "#C0392B", "#C0392B"],
"size": [6.0, 6.0, 6.0],
}
)

# Region labels - repositioned for clarity
df_regions = pd.DataFrame(
{
"strain": [0.005, 0.015, 0.13, 0.29],
"stress": [410, 215, 310, 370],
"label": ["Elastic", "Yield\nPlateau", "Strain\nHardening", "Necking"],
"color": ["#5D6D7E", "#5D6D7E", "#5D6D7E", "#5D6D7E"],
}
)

# Region boundary strains for shading
elastic_end = yield_stress / youngs_modulus
plateau_end = 0.025

# Plot using plotnine grammar of graphics with layered composition
plot = (
ggplot()
# Region shading using annotate("rect") - plotnine-distinctive feature
+ annotate("rect", xmin=0, xmax=elastic_end, ymin=0, ymax=440, alpha=0.15, fill="#3498DB")
+ annotate("rect", xmin=elastic_end, xmax=plateau_end, ymin=0, ymax=440, alpha=0.15, fill="#2ECC71")
+ annotate("rect", xmin=plateau_end, xmax=necking_strain, ymin=0, ymax=440, alpha=0.12, fill="#F39C12")
+ annotate("rect", xmin=necking_strain, xmax=fracture_strain, ymin=0, ymax=440, alpha=0.12, fill="#E74C3C")
# Main stress-strain curve
+ geom_line(df, aes(x="strain", y="stress"), color="#306998", size=2.8)
# 0.2% offset line using geom_segment - plotnine-distinctive
+ geom_segment(
aes(x=offset_strain_start, xend=offset_strain_end, y=0, yend=yield_stress + 50),
color="#C0392B",
size=1.2,
linetype="dashed",
)
# Offset label near the line
+ annotate("text", x=0.012, y=60, label="0.2% offset", size=11, color="#C0392B", fontstyle="italic")
# Key points with identity scales for direct aesthetic mapping
+ geom_point(df_points, aes(x="strain", y="stress", color="color", size="size"), fill="#C0392B")
+ scale_color_identity()
+ scale_size_identity()
# Point labels - larger text
+ geom_text(
df_points, aes(x="strain", y="stress", label="label"), nudge_y=32, size=15, color="#2C3E50", fontweight="bold"
)
# Region labels with identity color scale - larger text
+ geom_text(df_regions, aes(x="strain", y="stress", label="label", color="color"), size=14, fontstyle="italic")
+ scale_fill_identity()
+ scale_linetype_identity()
# Modulus annotation - larger and repositioned
+ annotate(
"text", x=0.03, y=140, label=f"E = {youngs_modulus // 1000} GPa", size=16, color="#306998", fontweight="bold"
)
+ labs(x="Engineering Strain", y="Engineering Stress (MPa)", title="line-stress-strain · plotnine · pyplots.ai")
+ scale_x_continuous(breaks=np.arange(0, 0.40, 0.05))
+ scale_y_continuous(breaks=np.arange(0, 500, 50))
# Coordinate control - plotnine-distinctive
+ coord_cartesian(xlim=(0, 0.38), ylim=(0, 460))
+ theme_minimal()
+ theme(
figure_size=(16, 9),
plot_title=element_text(size=26, weight="bold", color="#1A2530"),
axis_title=element_text(size=22, color="#2C3E50", weight="bold"),
axis_text=element_text(size=16, color="#555555"),
panel_grid_major_x=element_blank(),
panel_grid_minor=element_blank(),
panel_grid_major_y=element_line(color="#E8E8E8", size=0.4, alpha=0.5),
plot_background=element_rect(fill="white", color="white"),
panel_background=element_rect(fill="white", color="white"),
)
)

# Save
plot.save("plot.png", dpi=300, verbose=False)
229 changes: 229 additions & 0 deletions plots/line-stress-strain/metadata/plotnine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
library: plotnine
specification_id: line-stress-strain
created: '2026-03-20T21:22:28Z'
updated: '2026-03-20T21:43:25Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 23363004971
issue: 4413
python_version: 3.14.3
library_version: 0.15.3
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-stress-strain/plotnine/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-stress-strain/plotnine/plot_thumb.png
preview_html: null
quality_score: 90
review:
strengths:
- Excellent data storytelling through color-coded region shading that guides the
viewer through material behavior phases
- 'All spec-required features implemented: region labels, critical points, elastic
modulus annotation, 0.2% offset line'
- Realistic mild steel data with appropriate engineering values
- Clean, well-structured code following KISS principles
- Good use of plotnine grammar-of-graphics approach with layered composition
weaknesses:
- The elastic region is horizontally compressed, creating visual density with multiple
labels in a narrow space
- The 0.2% offset annotation text (size=11) is noticeably smaller than other text
elements
image_description: 'The plot displays an engineering stress-strain curve for mild
steel on a white background. The title "line-stress-strain · plotnine · pyplots.ai"
appears in bold dark text at the top. The x-axis is labeled "Engineering Strain"
(0.00–0.35) and the y-axis "Engineering Stress (MPa)" (0–450). A thick dark blue
line traces the characteristic stress-strain path from origin through elastic
deformation, yield plateau, strain hardening up to ~400 MPa at strain ~0.22 (UTS),
then necking down to ~320 MPa at fracture (strain ~0.35). Four color-shaded rectangular
regions mark the material behavior zones: light blue (Elastic), light green (Yield
Plateau), light yellow/orange (Strain Hardening), and light pink/red (Necking),
each with gray italic region labels. Three red dots mark critical points labeled
in bold: "Yield Point (0.2% offset)", "UTS", and "Fracture". A red dashed line
shows the 0.2% offset construction with a small red italic "0.2% offset" label
near the bottom. "E = 210 GPa" appears in bold blue text. The y-axis grid is subtle
light gray; x-axis grid is removed. Layout is 16:9 with good canvas utilization.'
criteria_checklist:
visual_quality:
score: 28
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 7
max: 8
passed: true
comment: All major text sizes explicitly set (title=26, axis_title=22, axis_text=16).
The 0.2% offset annotation at size=11 is smaller than other text.
- id: VQ-02
name: No Overlap
score: 5
max: 6
passed: true
comment: No actual text overlap, but elastic region is horizontally compressed
with multiple labels in a narrow band.
- id: VQ-03
name: Element Visibility
score: 6
max: 6
passed: true
comment: Main curve at size=2.8 clearly visible, key points at size=6 are
prominent.
- id: VQ-04
name: Color Accessibility
score: 4
max: 4
passed: true
comment: Blue curve with red accents, pastel region shading. Colorblind-safe,
no red-green dependency.
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Good 16:9 proportions with plot filling the canvas well.
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: 'Descriptive labels with units: Engineering Strain and Engineering
Stress (MPa).'
design_excellence:
score: 15
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Color-coded region shading, custom palette, intentional typographic
hierarchy. Above defaults.
- id: DE-02
name: Visual Refinement
score: 4
max: 6
passed: true
comment: X-grid removed, subtle y-only grid, clean white background. Minor
density in elastic region.
- id: DE-03
name: Data Storytelling
score: 5
max: 6
passed: true
comment: Color-coded regions guide viewer through material behavior. Key points
create clear focal points.
spec_compliance:
score: 15
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct engineering stress-strain curve as line plot.
- id: SC-02
name: Required Features
score: 4
max: 4
passed: true
comment: 'All spec features present: region labels, critical points, modulus
annotation, offset line.'
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X=strain, Y=stress correctly mapped with full data range.
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title follows exact format. No legend needed for single-series.
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: 'Shows all curve regions: elastic, yield plateau, strain hardening,
necking, fracture.'
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: Mild steel tensile test - classic, neutral engineering scenario.
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: 'Realistic values for mild steel: E=210 GPa, yield=250 MPa, UTS=400
MPa.'
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Clean linear flow: imports, data, plot, save. No functions or classes.'
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: np.random.seed(42) set at start.
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: All imports are used.
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Well-organized with clear sections. No fake UI.
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot.png with dpi=300. Current plotnine API.
library_mastery:
score: 7
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 4
max: 5
passed: true
comment: Good grammar-of-graphics with layered composition, aes mapping, identity
scales, theme customization.
- id: LM-02
name: Distinctive Features
score: 3
max: 5
passed: true
comment: Uses annotate rect for region shading, coord_cartesian, identity
scales, geom_segment.
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- annotations
- layer-composition
patterns:
- data-generation
dataprep: []
styling:
- alpha-blending
- grid-styling
Loading