diff --git a/plots/ternary-density/implementations/python/altair.py b/plots/ternary-density/implementations/python/altair.py index a0c62b6987..2dc01c628c 100644 --- a/plots/ternary-density/implementations/python/altair.py +++ b/plots/ternary-density/implementations/python/altair.py @@ -1,108 +1,108 @@ -""" pyplots.ai +""" anyplot.ai ternary-density: Ternary Density Plot -Library: altair 6.0.0 | Python 3.13.11 -Quality: 91/100 | Created: 2026-01-11 +Library: altair 6.1.0 | Python 3.13.13 +Quality: 90/100 | Updated: 2026-05-19 """ +import os + import altair as alt import numpy as np import pandas as pd from scipy.stats import gaussian_kde -# Data - Generate clustered compositional data (sand/silt/clay for sediment analysis) +# 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" + +# Data — clustered sediment compositions (sand/silt/clay) np.random.seed(42) n_per_cluster = 400 # Cluster 1: Sandy sediment (high sand, low clay) -c1_sand = np.random.beta(12, 2, n_per_cluster) * 80 + 15 # 15-95% sand +c1_sand = np.random.beta(12, 2, n_per_cluster) * 80 + 15 c1_clay = np.random.beta(2, 8, n_per_cluster) * (100 - c1_sand) * 0.3 c1_silt = 100 - c1_sand - c1_clay # Cluster 2: Silty sediment (high silt) -c2_silt = np.random.beta(10, 2, n_per_cluster) * 70 + 25 # 25-95% silt +c2_silt = np.random.beta(10, 2, n_per_cluster) * 70 + 25 c2_sand = np.random.beta(3, 6, n_per_cluster) * (100 - c2_silt) * 0.7 c2_clay = 100 - c2_silt - c2_sand # Cluster 3: Clay-rich sediment (high clay) -c3_clay = np.random.beta(8, 3, n_per_cluster) * 60 + 30 # 30-90% clay +c3_clay = np.random.beta(8, 3, n_per_cluster) * 60 + 30 c3_sand = np.random.beta(2, 5, n_per_cluster) * (100 - c3_clay) * 0.5 c3_silt = 100 - c3_clay - c3_sand -# Combine all clusters +# Combine and normalize to exact 100% sand = np.concatenate([c1_sand, c2_sand, c3_sand]) silt = np.concatenate([c1_silt, c2_silt, c3_silt]) clay = np.concatenate([c1_clay, c2_clay, c3_clay]) - -# Clip and normalize to ensure valid compositions sand = np.clip(sand, 0.1, 99.8) silt = np.clip(silt, 0.1, 99.8) clay = np.clip(clay, 0.1, 99.8) total = sand + silt + clay sand, silt, clay = sand / total * 100, silt / total * 100, clay / total * 100 -# Convert ternary to Cartesian coordinates -# Triangle vertices: Sand=(0,0), Silt=(1,0), Clay=(0.5, sqrt(3)/2) +# Ternary → Cartesian: Sand=(0,0), Silt=(1,0), Clay=(0.5, √3/2) sqrt3_2 = np.sqrt(3) / 2 -x_points = 0.5 * (2 * silt + clay) / 100 -y_points = sqrt3_2 * clay / 100 +x_pts = 0.5 * (2 * silt + clay) / 100 +y_pts = sqrt3_2 * clay / 100 -# Create grid for density estimation -grid_res = 80 +# KDE on the transformed coordinates +grid_res = 100 x_grid = np.linspace(0, 1, grid_res) y_grid = np.linspace(0, sqrt3_2, grid_res) xx, yy = np.meshgrid(x_grid, y_grid) - -# Perform KDE on the transformed coordinates -coords = np.vstack([x_points, y_points]) -kde = gaussian_kde(coords, bw_method="scott") +kde = gaussian_kde(np.vstack([x_pts, y_pts]), bw_method="scott") density = kde(np.vstack([xx.ravel(), yy.ravel()])).reshape(xx.shape) -# Create mask for points inside the triangle (half-plane method) -# Triangle: A=(0,0), B=(1,0), C=(0.5, sqrt3_2) +# Triangle mask (half-plane method) margin = 0.005 -inside_bottom = yy >= -margin -inside_right = np.sqrt(3) * xx + yy <= np.sqrt(3) + margin -inside_left = yy - np.sqrt(3) * xx <= margin -mask = inside_bottom & inside_right & inside_left - -# Create DataFrame for heatmap (only points inside triangle) -density_data = [] -for i in range(grid_res): - for j in range(grid_res): - if mask[i, j]: - density_data.append({"x": xx[i, j], "y": yy[i, j], "density": density[i, j]}) -density_df = pd.DataFrame(density_data) +inside = (yy >= -margin) & (np.sqrt(3) * xx + yy <= np.sqrt(3) + margin) & (yy - np.sqrt(3) * xx <= margin) + +# Cell half-widths for mark_rect (pixel-perfect coverage) +dx = (x_grid[1] - x_grid[0]) / 2 +dy = (y_grid[1] - y_grid[0]) / 2 + +# Density DataFrame — explicit cell bounds for clean edges +density_rows = [ + {"x1": xx[i, j] - dx, "x2": xx[i, j] + dx, "y1": yy[i, j] - dy, "y2": yy[i, j] + dy, "density": density[i, j]} + for i in range(grid_res) + for j in range(grid_res) + if inside[i, j] +] +density_df = pd.DataFrame(density_rows) # Triangle outline vertices triangle_df = pd.DataFrame({"x": [0, 1, 0.5, 0], "y": [0, 0, sqrt3_2, 0], "order": [0, 1, 2, 3]}) -# Grid lines for ternary diagram (all lines stay inside triangle) +# Ternary grid lines (10% intervals for all three axes) grid_lines = [] -n_lines = 10 -for i in range(1, n_lines): - frac = i / n_lines - # Horizontal lines (constant clay %) +for i in range(1, 10): + frac = i / 10 + # Constant clay (horizontal) y_val = frac * sqrt3_2 x_left = y_val / np.sqrt(3) x_right = 1 - y_val / np.sqrt(3) - grid_lines.extend( - [{"x": x_left, "y": y_val, "line": f"h{i}", "o": 0}, {"x": x_right, "y": y_val, "line": f"h{i}", "o": 1}] - ) - # Lines from bottom to left edge (constant sand %) - x_bottom = frac - x_top = frac / 2 - y_top = frac * sqrt3_2 - grid_lines.extend( - [{"x": x_bottom, "y": 0, "line": f"l{i}", "o": 0}, {"x": x_top, "y": y_top, "line": f"l{i}", "o": 1}] - ) - # Lines from bottom to right edge (constant silt %) - x_bottom = 1 - frac - x_top = 1 - frac / 2 - y_top = frac * sqrt3_2 - grid_lines.extend( - [{"x": x_bottom, "y": 0, "line": f"r{i}", "o": 0}, {"x": x_top, "y": y_top, "line": f"r{i}", "o": 1}] - ) + grid_lines += [ + {"x": x_left, "y": y_val, "line": f"h{i}", "o": 0}, + {"x": x_right, "y": y_val, "line": f"h{i}", "o": 1}, + ] + # Constant sand + grid_lines += [ + {"x": frac, "y": 0, "line": f"s{i}", "o": 0}, + {"x": frac / 2, "y": frac * sqrt3_2, "line": f"s{i}", "o": 1}, + ] + # Constant silt + grid_lines += [ + {"x": 1 - frac, "y": 0, "line": f"t{i}", "o": 0}, + {"x": 1 - frac / 2, "y": frac * sqrt3_2, "line": f"t{i}", "o": 1}, + ] grid_df = pd.DataFrame(grid_lines) # Vertex labels @@ -110,17 +110,19 @@ {"x": [-0.02, 1.02, 0.5], "y": [-0.05, -0.05, sqrt3_2 + 0.05], "label": ["Sand (%)", "Silt (%)", "Clay (%)"]} ) -# Shared axis config (no visible axes for ternary diagram) -x_enc = alt.X("x:Q", scale=alt.Scale(domain=[-0.12, 1.12]), axis=None) -y_enc = alt.Y("y:Q", scale=alt.Scale(domain=[-0.12, sqrt3_2 + 0.12]), axis=None) +# Shared scale domains for all layers +X_DOMAIN = [-0.12, 1.12] +Y_DOMAIN = [-0.12, sqrt3_2 + 0.12] -# Density heatmap layer +# Density heatmap — mark_rect with explicit cell bounds heatmap = ( alt.Chart(density_df) - .mark_square(size=250, opacity=0.9) + .mark_rect(opacity=0.92) .encode( - x=x_enc, - y=y_enc, + x=alt.X("x1:Q", scale=alt.Scale(domain=X_DOMAIN), axis=None), + x2=alt.X2("x2:Q"), + y=alt.Y("y1:Q", scale=alt.Scale(domain=Y_DOMAIN), axis=None), + y2=alt.Y2("y2:Q"), color=alt.Color( "density:Q", scale=alt.Scale(scheme="viridis"), @@ -130,33 +132,54 @@ ) # Triangle outline -triangle = alt.Chart(triangle_df).mark_line(color="#222", strokeWidth=3).encode(x=x_enc, y=y_enc, order="order:O") +triangle = ( + alt.Chart(triangle_df) + .mark_line(color=INK, strokeWidth=3) + .encode( + x=alt.X("x:Q", scale=alt.Scale(domain=X_DOMAIN), axis=None), + y=alt.Y("y:Q", scale=alt.Scale(domain=Y_DOMAIN), axis=None), + order="order:O", + ) +) # Grid lines grid = ( alt.Chart(grid_df) - .mark_line(color="#888", strokeWidth=1, opacity=0.4) - .encode(x=x_enc, y=y_enc, detail="line:N", order="o:O") + .mark_line(color=INK_SOFT, strokeWidth=1, opacity=0.35) + .encode( + x=alt.X("x:Q", scale=alt.Scale(domain=X_DOMAIN), axis=None), + y=alt.Y("y:Q", scale=alt.Scale(domain=Y_DOMAIN), axis=None), + detail="line:N", + order="o:O", + ) ) # Vertex labels labels = ( alt.Chart(labels_df) - .mark_text(fontSize=24, fontWeight="bold", color="#222") - .encode(x=x_enc, y=y_enc, text="label:N") + .mark_text(fontSize=24, fontWeight="bold", color=INK) + .encode( + x=alt.X("x:Q", scale=alt.Scale(domain=X_DOMAIN), axis=None), + y=alt.Y("y:Q", scale=alt.Scale(domain=Y_DOMAIN), axis=None), + text="label:N", + ) ) -# Combine layers +# Compose layers chart = ( alt.layer(grid, heatmap, triangle, labels) .properties( + background=PAGE_BG, width=1600, height=900, - title=alt.Title("Sediment Composition · ternary-density · altair · pyplots.ai", fontSize=28), + title=alt.Title( + "Sediment Composition · ternary-density · python · altair · anyplot.ai", fontSize=28, color=INK + ), ) - .configure_view(strokeWidth=0) + .configure_view(fill=PAGE_BG, strokeWidth=0) + .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK) ) # Save -chart.save("plot.png", scale_factor=3.0) -chart.save("plot.html") +chart.save(f"plot-{THEME}.png", scale_factor=3.0) +chart.save(f"plot-{THEME}.html") diff --git a/plots/ternary-density/metadata/python/altair.yaml b/plots/ternary-density/metadata/python/altair.yaml index bc255aec76..161041ef20 100644 --- a/plots/ternary-density/metadata/python/altair.yaml +++ b/plots/ternary-density/metadata/python/altair.yaml @@ -1,154 +1,181 @@ library: altair +language: python specification_id: ternary-density created: '2026-01-11T09:32:43Z' -updated: '2026-01-11T09:36:56Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20892892485 +updated: '2026-05-19T10:41:19Z' +generated_by: claude-sonnet +workflow_run: 26091386646 issue: 3696 -python_version: 3.13.11 -library_version: 6.0.0 -preview_url: https://storage.googleapis.com/anyplot-images/plots/ternary-density/altair/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/ternary-density/altair/plot.html -quality_score: 91 +language_version: 3.13.13 +library_version: 6.1.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/altair/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/altair/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/altair/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/altair/plot-dark.html +quality_score: 90 review: strengths: - - Excellent implementation of ternary coordinates with proper Cartesian transformation - - Clean layer composition with grid → heatmap → triangle → labels ordering - - Sophisticated KDE calculation using scipy.stats.gaussian_kde with Scott bandwidth - - Proper triangle masking using half-plane method - - Good use of Altair declarative style for building complex visualizations - - Generates both PNG and HTML output for interactivity - - Realistic geological sediment data with three distinct composition clusters + - Perfect ternary-to-Cartesian coordinate transformation with correct half-plane + triangle masking + - All theme tokens (PAGE_BG, ELEVATED_BG, INK, INK_SOFT) correctly applied to every + chrome element in both renders + - 'Full spec compliance: KDE with auto bandwidth (Scott''s rule), viridis colormap, + ternary grid lines, vertex labels with units' + - Geologically realistic three-cluster sediment dataset (sandy/silty/clay-rich) + that tells a clear compositional story + - Expert Altair layer composition using idiomatic mark_rect bounds, detail encoding, + and X2/Y2 secondary position encodings weaknesses: - - Minor jagged edges at triangle boundary due to square marks not perfectly aligned - with diagonal edges - - Spec mentions contour lines at key density levels - not implemented (optional) - image_description: 'The plot displays a ternary density diagram representing sediment - composition with three components: Sand (%), Silt (%), and Clay (%). The density - is visualized using the viridis colormap, with yellow indicating high density - (~5) and dark purple indicating low density (~1). Three distinct density clusters - are clearly visible: (1) a sandy sediment cluster in the lower-left corner, (2) - a silty sediment cluster in the lower-right corner, and (3) a clay-rich sediment - cluster in the upper region. The equilateral triangle has a solid black outline - (3px stroke), subtle grid lines (10% intervals) visible beneath the density layer - with 40% opacity, and vertex labels positioned clearly outside the triangle boundary. - The title follows the correct format "Sediment Composition · ternary-density · - altair · pyplots.ai". The density legend is positioned on the right side with - appropriate font sizes (title 20pt, labels 16pt).' + - 'DE-01 moderate: design is professional but lacks final aesthetic flourish — consider + adding sparse contour lines at key density thresholds or cluster region labels + near each density peak' + - 'VQ-05 minor: some wasted space below the triangle base; tightening Y domain or + switching to square canvas format could improve utilization for this symmetric + shape' + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct theme surface, not pure white + Chrome: Title "Sediment Composition · ternary-density · python · altair · anyplot.ai" at 28px in dark ink (#1A1A17) — clearly readable. Vertex labels "Sand (%)", "Silt (%)", "Clay (%)" at 24px bold dark ink — fully readable. Density legend title/labels in INK_SOFT tokens — readable. No text readability issues. + Data: Viridis colormap density heatmap inside equilateral triangle; three distinct density peaks (yellow-green hotspots) near Sand vertex (bottom-left), Silt vertex (bottom-right), and Clay vertex (top); low-density areas in deep purple; subtle ternary grid lines at 0.35 opacity visible within triangle; dark triangle outline at strokeWidth=3 + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Near-black #1A1A17 — correct dark theme surface, not pure black + Chrome: Title and vertex labels rendered in light ink (#F0EFE8) — clearly readable against dark background. Triangle outline flips to light (#F0EFE8). Legend text in INK_SOFT dark tokens (#B8B7B0) — readable. No dark-on-dark text failures detected. + Data: Viridis colormap pattern and three density clusters are visually identical to light render — same yellow-green hotspots at Sandy/Silty/Clay-rich peaks, same purple low-density field. Only chrome elements (background, text, outline) changed between themes. + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 37 - max: 40 + score: 29 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 8 + max: 8 passed: true - comment: Title at 28pt, vertex labels at 24pt bold, legend text at 16-20pt - - all perfectly readable + comment: Title 28px, vertex labels 24px, legend title 20px, legend labels + 16px — all explicitly set and readable in both themes - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text elements; labels positioned outside triangle + comment: No overlapping text; vertex labels cleanly positioned outside triangle - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Density squares (size=250) well-suited for the grid resolution; very - minor jagged edges at triangle boundary + comment: Density heatmap with three distinct clusters clearly visible; viridis + contrast excellent - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Viridis colormap is perceptually uniform and colorblind-safe + comment: Viridis is perceptually uniform and colorblind-safe - id: VQ-05 - name: Layout Balance - score: 5 - max: 5 + name: Layout & Canvas + score: 3 + max: 4 passed: true - comment: Plot fills canvas well, balanced margins, legend near plot + comment: Good layout; triangle fills ~65% of canvas with balanced margins; + some inherent empty space below triangular base - id: VQ-06 - name: Axis Labels - score: 0 + name: Axis Labels & Title + score: 2 max: 2 - passed: false - comment: N/A for ternary plots (no traditional axes), vertex labels have units - (%) + passed: true + comment: Vertex labels include units (%) and legend labeled Density - id: VQ-07 - name: Grid & Legend + name: Palette Compliance score: 2 max: 2 passed: true - comment: Grid at 40% opacity is subtle, legend well-positioned + comment: 'Viridis for continuous density data; correct backgrounds #FAF8F1/#1A1A17; + all chrome tokens theme-adaptive' + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: Strong and professional — appropriate viridis, clean triangular boundary, + all theme tokens — above default but not FiveThirtyEight level + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Grid lines at 0.35 opacity, no axes, styled legend with custom fill/stroke, + view strokeWidth=0 + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Three sediment regimes immediately visible via viridis color hierarchy; + viewer sees compositional separation without annotations spec_compliance: - score: 25 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct ternary density plot with KDE overlay - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Three components correctly mapped to ternary coordinates - - id: SC-03 + comment: Correct ternary density plot with KDE heatmap overlay + - id: SC-02 name: Required Features - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Density overlay, grid lines, vertex labels, viridis colormap all - present - - id: SC-04 - name: Data Range + comment: KDE with Scott bandwidth, viridis, ternary grid lines, vertex labels, + triangle outline — all spec requirements met + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All data visible within triangle bounds - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Density legend correctly labeled - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Sand/Silt/Clay correctly mapped to ternary Cartesian; components + sum to 100% + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Uses spec-id · library · pyplots.ai format with meaningful prefix + comment: Title follows {Descriptive Title} · {spec-id} · python · altair · + anyplot.ai format; density legend correct data_quality: - score: 19 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows three distinct clusters demonstrating density estimation well; - could show more overlap between clusters + comment: Three distinct clusters (sandy, silty, clay-rich) demonstrate all + aspects of multi-modal compositional density - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Sediment composition (sand/silt/clay) is a real geological application + comment: Sediment composition (sand/silt/clay) is a well-established geological + measurement domain; completely neutral - id: DQ-03 name: Appropriate Scale - score: 5 - max: 5 + score: 4 + max: 4 passed: true - comment: Values sum to 100%, realistic beta distributions for sediment types + comment: Beta distributions produce geologically plausible proportions; normalization + ensures exact 100% constraint code_quality: score: 10 max: 10 @@ -158,43 +185,53 @@ review: score: 3 max: 3 passed: true - comment: 'Linear structure: imports → data → coordinates → KDE → layers → - save' + comment: 'Linear: imports → data generation → coordinate transform → KDE → + DataFrame → chart layers → save' - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Uses np.random.seed(42) + comment: np.random.seed(42) set - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only necessary imports (altair, numpy, pandas, scipy.stats) + comment: All five imports (os, altair, numpy, pandas, scipy.stats.gaussian_kde) + are used - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current Altair API + comment: Clean, Pythonic; list comprehension for density grid; appropriate + complexity - 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: 5 - max: 5 + comment: Saves plot-{THEME}.png and plot-{THEME}.html; Altair 6.0.0 API used + correctly + library_mastery: + score: 8 + max: 10 items: - - id: LF-01 - name: Distinctive Features + - id: LM-01 + name: Idiomatic Usage score: 5 max: 5 passed: true - comment: Excellent use of Altair's declarative layer composition, mark_square - for heatmap, proper encoding types, HTML export capability + comment: 'Expert Altair: correct encoding suffixes, alt.layer() composition, + configure_* chaining, alt.Scale(domain=) for axis alignment' + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: mark_rect with x1/x2/y1/y2 bounds for pixel-perfect cells, detail + channel for grid grouping, alt.X2/Y2 secondary encodings verdict: APPROVED impl_tags: dependencies: @@ -205,10 +242,8 @@ impl_tags: patterns: - data-generation - matrix-construction - - iteration-over-groups dataprep: - kde styling: - custom-colormap - alpha-blending - - grid-styling