diff --git a/plots/ternary-basic/implementations/python/pygal.py b/plots/ternary-basic/implementations/python/pygal.py
index 8c3b87200c..da0e238739 100644
--- a/plots/ternary-basic/implementations/python/pygal.py
+++ b/plots/ternary-basic/implementations/python/pygal.py
@@ -1,10 +1,11 @@
-""" pyplots.ai
+""" anyplot.ai
ternary-basic: Basic Ternary Plot
-Library: pygal 3.1.0 | Python 3.13.11
-Quality: 78/100 | Created: 2025-12-24
+Library: pygal 3.1.0 | Python 3.13.13
+Quality: 92/100 | Updated: 2026-05-06
"""
import math
+import os
import cairosvg
import numpy as np
@@ -12,87 +13,79 @@
from pygal.style import Style
+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"
+BRAND = "#009E73"
+
np.random.seed(42)
-# Triangle height for equilateral triangle with base = 1
H = math.sqrt(3) / 2
-# Sample soil composition data (Sand%, Silt%, Clay%) - all sum to 100
-# Ternary coordinates: Top=Sand, Bottom-right=Silt, Bottom-left=Clay
compositions = [
- (65, 25, 10), # Sandy Loam
- (10, 45, 45), # Silty Clay
- (30, 35, 35), # Clay Loam
- (40, 40, 20), # Loam
- (50, 10, 40), # Sandy Clay
- (20, 65, 15), # Silt Loam
- (90, 5, 5), # Pure Sand
- (5, 90, 5), # Pure Silt
- (5, 5, 90), # Pure Clay
- (33, 34, 33), # Balanced
- (45, 45, 10), # Sandy Silt
- (55, 35, 10), # Silty Sand
- (15, 20, 65), # Clay
- (10, 15, 75), # Heavy Clay
- (50, 30, 20), # Light Loam
+ (65, 25, 10),
+ (10, 45, 45),
+ (30, 35, 35),
+ (40, 40, 20),
+ (50, 10, 40),
+ (20, 65, 15),
+ (90, 5, 5),
+ (5, 90, 5),
+ (5, 5, 90),
+ (33, 34, 33),
+ (45, 45, 10),
+ (55, 35, 10),
+ (15, 20, 65),
+ (10, 15, 75),
+ (50, 30, 20),
]
-# Convert ternary (sand, silt, clay) to cartesian (x, y)
-# Formula: x = 0.5 * (2 * silt/100 + sand/100), y = H * sand/100
data_points = [(0.5 * (2 * s[1] + s[0]) / 100, H * s[0] / 100) for s in compositions]
-# Triangle vertices
-vertex_sand = (0.5, H) # Top - 100% Sand
-vertex_silt = (1.0, 0.0) # Bottom-right - 100% Silt
-vertex_clay = (0.0, 0.0) # Bottom-left - 100% Clay
+vertex_sand = (0.5, H)
+vertex_silt = (1.0, 0.0)
+vertex_clay = (0.0, 0.0)
-# Grid lines at 20%, 40%, 60%, 80% intervals
-# Each line connects two edges of the triangle
grid_lines = []
for pct in [0.2, 0.4, 0.6, 0.8]:
- # Parallel to base (constant Sand %) - from left edge to right edge
p1 = (0.5 * (2 * (1 - pct) + pct), H * pct)
p2 = (0.5 * pct, H * pct)
grid_lines.extend([p1, p2, (None, None)])
- # Parallel to left side (constant Silt %) - from base to right edge
p1 = (0.5 * (2 * pct + (1 - pct)), H * (1 - pct))
p2 = (pct, 0.0)
grid_lines.extend([p1, p2, (None, None)])
- # Parallel to right side (constant Clay %) - from base to left edge
p1 = (0.5 * (1 - pct), H * (1 - pct))
p2 = (0.5 * (2 * (1 - pct)), 0.0)
grid_lines.extend([p1, p2, (None, None)])
-# Tick marks along each edge at 20% intervals
tick_marks = []
-tick_len = 0.03 # Length of tick marks
+tick_len = 0.03
for pct in [0.2, 0.4, 0.6, 0.8]:
- # Ticks on left edge (Clay-Sand): perpendicular outward
x_left = 0.5 * pct
y_left = H * pct
tick_marks.extend([(x_left, y_left), (x_left - tick_len, y_left), (None, None)])
- # Ticks on right edge (Sand-Silt): perpendicular outward
x_right = 0.5 * (2 - pct)
y_right = H * pct
tick_marks.extend([(x_right, y_right), (x_right + tick_len, y_right), (None, None)])
- # Ticks on base (Clay-Silt): perpendicular downward
x_base = pct
y_base = 0.0
tick_marks.extend([(x_base, y_base), (x_base, y_base - tick_len), (None, None)])
-# Custom style for 3600x3600 px canvas
custom_style = Style(
- background="white",
- plot_background="white",
- foreground="#333333",
- foreground_strong="#333333",
- foreground_subtle="#666666",
- colors=("#333333", "#AAAAAA", "#306998", "#333333"), # Boundary, Grid, Data, Ticks
+ background=PAGE_BG,
+ plot_background=PAGE_BG,
+ foreground=INK,
+ foreground_strong=INK,
+ foreground_subtle=INK_MUTED,
+ colors=(INK, INK_MUTED, BRAND, INK),
title_font_size=80,
label_font_size=48,
major_label_font_size=44,
@@ -101,7 +94,6 @@
opacity=0.85,
)
-# Create XY chart (square format for ternary plot)
chart = pygal.XY(
width=3600,
height=3600,
@@ -115,7 +107,7 @@
show_y_labels=False,
x_title="",
y_title="",
- title="Soil Composition · ternary-basic · pygal · pyplots.ai",
+ title="Soil Composition · ternary-basic · pygal · anyplot.ai",
dots_size=20,
stroke=False,
include_x_axis=False,
@@ -126,26 +118,18 @@
margin_bottom=120,
)
-# Triangle outline (no legend entry - structural element)
chart.add(
None, [vertex_clay, vertex_silt, vertex_sand, vertex_clay], stroke=True, show_dots=False, stroke_style={"width": 5}
)
-# Grid lines at 20% intervals (no legend entry - structural element)
chart.add(None, grid_lines, stroke=True, show_dots=False, stroke_style={"width": 2, "dasharray": "8,5"})
-# Data points (soil samples) - the only legend-worthy series
chart.add("Soil Samples", data_points, stroke=False, dots_size=22)
-# Tick marks along edges (no legend entry - structural element)
chart.add(None, tick_marks, stroke=True, show_dots=False, stroke_style={"width": 3})
-# Render to SVG string first
svg_content = chart.render().decode("utf-8")
-# Calculate pixel positions for labels (inline conversion - KISS principle)
-# The chart has xrange=(-0.15, 1.15) = 1.30 range and yrange=(-0.20, 1.05) = 1.25 range
-# Approximate: plot area starts after margin and title
plot_x_start = 150
plot_x_end = 3450
plot_y_start = 250
@@ -153,7 +137,6 @@
x_range = 1.30
y_range = 1.25
-# Vertex label positions (offset from triangle vertices)
sand_px = plot_x_start + (0.5 + 0.15) / x_range * (plot_x_end - plot_x_start)
sand_py = plot_y_start + (1.05 - (H + 0.06)) / y_range * (plot_y_end - plot_y_start)
silt_px = plot_x_start + (1.07 + 0.15) / x_range * (plot_x_end - plot_x_start)
@@ -161,48 +144,40 @@
clay_px = plot_x_start + (-0.07 + 0.15) / x_range * (plot_x_end - plot_x_start)
clay_py = plot_y_start + (1.05 - (-0.03)) / y_range * (plot_y_end - plot_y_start)
-# Build SVG text elements for vertex labels
vertex_labels_svg = f"""
- SAND
- SILT
- CLAY
+ SAND
+ SILT
+ CLAY
"""
-# Percentage labels along each edge at 20%, 40%, 60%, 80%
pct_labels_svg = ""
pct_font_size = 36
for pct in [20, 40, 60, 80]:
frac = pct / 100.0
- # Left edge (Clay-Sand): Sand % increases going up, Clay % decreases
left_x = 0.5 * frac
left_y = H * frac
left_px = plot_x_start + (left_x - 0.06 + 0.15) / x_range * (plot_x_end - plot_x_start)
left_py = plot_y_start + (1.05 - left_y) / y_range * (plot_y_end - plot_y_start)
- pct_labels_svg += f' {pct}\n'
+ pct_labels_svg += f' {pct}\n'
- # Right edge (Sand-Silt): Sand % increases going up, Silt % increases going down-right
right_x = 0.5 * (2 - frac)
right_y = H * frac
right_px = plot_x_start + (right_x + 0.04 + 0.15) / x_range * (plot_x_end - plot_x_start)
right_py = plot_y_start + (1.05 - right_y) / y_range * (plot_y_end - plot_y_start)
- pct_labels_svg += f' {pct}\n'
+ pct_labels_svg += f' {pct}\n'
- # Bottom edge (Clay-Silt): Clay % decreases left-to-right, Silt % increases
base_x = frac
base_y = -0.05
base_px = plot_x_start + (base_x + 0.15) / x_range * (plot_x_end - plot_x_start)
base_py = plot_y_start + (1.05 - base_y) / y_range * (plot_y_end - plot_y_start)
- pct_labels_svg += f' {pct}\n'
+ pct_labels_svg += f' {pct}\n'
-# Insert all labels before the closing tag
all_labels_svg = vertex_labels_svg + pct_labels_svg
svg_content = svg_content.replace("", all_labels_svg + "")
-# Save as SVG for HTML output
-with open("plot.html", "w") as f:
+with open(f"plot-{THEME}.html", "w") as f:
f.write(svg_content)
-# Convert to PNG
-cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to="plot.png")
+cairosvg.svg2png(bytestring=svg_content.encode("utf-8"), write_to=f"plot-{THEME}.png")
diff --git a/plots/ternary-basic/metadata/python/pygal.yaml b/plots/ternary-basic/metadata/python/pygal.yaml
index 3cbe96e916..815bfbbc11 100644
--- a/plots/ternary-basic/metadata/python/pygal.yaml
+++ b/plots/ternary-basic/metadata/python/pygal.yaml
@@ -1,40 +1,193 @@
library: pygal
+language: python
specification_id: ternary-basic
created: '2025-12-24T09:54:54Z'
-updated: '2025-12-24T10:23:09Z'
-generated_by: claude-opus-4-5-20251101
-workflow_run: 20483448048
-issue: 0
-python_version: 3.13.11
+updated: '2026-05-06T03:54:39Z'
+generated_by: claude-haiku
+workflow_run: 25414898498
+issue: 1001
+python_version: 3.13.13
library_version: 3.1.0
-preview_url: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/pygal/plot.png
-preview_html: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/pygal/plot.html
-quality_score: 78
+preview_url_light: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/python/pygal/plot-light.png
+preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/python/pygal/plot-dark.png
+preview_html_light: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/python/pygal/plot-light.html
+preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-basic/python/pygal/plot-dark.html
+quality_score: 92
+review:
+ strengths:
+ - 'Palette compliance fixed: data points now correctly render as #009E73 brand green'
+ - Perfect theme adaptation with proper chrome switching
+ - Clean, professional layout with no text overlaps
+ - Complete specification implementation with all ternary features
+ - Strong code quality with proper reproducibility and structure
+ weaknesses: []
+ image_description: |-
+ Light render (plot-light.png):
+ Background: Warm off-white #FAF8F1 - correct
+ Chrome: Title in dark text #1A1A17, vertex labels bold and clear, percentage labels in muted ink #6B6A63, grid lines subtle dashed pattern
+ Data: 15 data points rendered as brand green circles #009E73, clearly visible and well-positioned
+ Legibility verdict: PASS
+
+ Dark render (plot-dark.png):
+ Background: Warm near-black #1A1A17 - correct
+ Chrome: Title in light text #F0EFE8, vertex labels visible in light text, percentage labels in light muted #A8A79F, grid lines subtle
+ Data: 15 data points remain as brand green #009E73 - identical to light render (only chrome flipped)
+ Legibility verdict: PASS
+ criteria_checklist:
+ visual_quality:
+ score: 30
+ max: 30
+ items:
+ - id: VQ-01
+ name: Text Legibility
+ score: 8
+ max: 8
+ passed: true
+ - id: VQ-02
+ name: No Overlap
+ score: 6
+ max: 6
+ passed: true
+ - id: VQ-03
+ name: Element Visibility
+ score: 6
+ max: 6
+ passed: true
+ - id: VQ-04
+ name: Color Accessibility
+ score: 2
+ max: 2
+ passed: true
+ - id: VQ-05
+ name: Layout & Canvas
+ score: 4
+ max: 4
+ passed: true
+ - id: VQ-06
+ name: Axis Labels & Title
+ score: 2
+ max: 2
+ passed: true
+ - id: VQ-07
+ name: Palette Compliance
+ score: 2
+ max: 2
+ passed: true
+ design_excellence:
+ score: 14
+ max: 20
+ items:
+ - id: DE-01
+ name: Aesthetic Sophistication
+ score: 6
+ max: 8
+ passed: true
+ - id: DE-02
+ name: Visual Refinement
+ score: 4
+ max: 6
+ passed: true
+ - id: DE-03
+ name: Data Storytelling
+ score: 4
+ max: 6
+ passed: true
+ spec_compliance:
+ score: 15
+ max: 15
+ items:
+ - id: SC-01
+ name: Plot Type
+ score: 5
+ max: 5
+ passed: true
+ - id: SC-02
+ name: Required Features
+ score: 4
+ max: 4
+ passed: true
+ - id: SC-03
+ name: Data Mapping
+ score: 3
+ max: 3
+ passed: true
+ - id: SC-04
+ name: Title & Legend
+ score: 3
+ max: 3
+ passed: true
+ data_quality:
+ score: 15
+ max: 15
+ items:
+ - id: DQ-01
+ name: Feature Coverage
+ score: 6
+ max: 6
+ passed: true
+ - id: DQ-02
+ name: Realistic Context
+ score: 5
+ max: 5
+ passed: true
+ - id: DQ-03
+ name: Appropriate Scale
+ score: 4
+ max: 4
+ passed: true
+ code_quality:
+ score: 10
+ max: 10
+ items:
+ - id: CQ-01
+ name: KISS Structure
+ score: 3
+ max: 3
+ passed: true
+ - id: CQ-02
+ name: Reproducibility
+ score: 2
+ max: 2
+ passed: true
+ - id: CQ-03
+ name: Clean Imports
+ score: 2
+ max: 2
+ passed: true
+ - id: CQ-04
+ name: Code Elegance
+ score: 2
+ max: 2
+ passed: true
+ - id: CQ-05
+ name: Output & API
+ score: 1
+ max: 1
+ passed: true
+ library_mastery:
+ score: 8
+ max: 10
+ items:
+ - id: LM-01
+ name: Idiomatic Usage
+ score: 5
+ max: 5
+ passed: true
+ - id: LM-02
+ name: Distinctive Features
+ score: 3
+ max: 5
+ passed: true
+ verdict: APPROVED
impl_tags:
dependencies:
- cairosvg
techniques:
- - manual-ticks
- annotations
- html-export
patterns:
+ - data-generation
- matrix-construction
- - iteration-over-groups
dataprep: []
styling:
- - grid-styling
-review:
- strengths:
- - Creative implementation of ternary plot using pygal XY chart type since pygal
- lacks native ternary support
- - Excellent use of SVG manipulation to add vertex and percentage labels that pygal
- cannot natively produce
- - Realistic soil composition data with meaningful variation across the ternary space
- - Proper ternary coordinate transformation from compositional data to Cartesian
- coordinates
- - Clean grid lines at 20% intervals with appropriate styling
- - Correct title format following the spec-id · library · pyplots.ai convention
- weaknesses:
- - Legend shows only Soil Samples which adds little value for a single-series plot
- - Some whitespace below the plot area could be better utilized
- - Percentage labels along edges are relatively small compared to vertex labels
+ - custom-colormap