diff --git a/plots/ternary-density/implementations/python/plotly.py b/plots/ternary-density/implementations/python/plotly.py index 7c6f962faf..35eafa4f0b 100644 --- a/plots/ternary-density/implementations/python/plotly.py +++ b/plots/ternary-density/implementations/python/plotly.py @@ -1,93 +1,109 @@ -""" pyplots.ai +""" anyplot.ai ternary-density: Ternary Density Plot -Library: plotly 6.5.1 | Python 3.13.11 -Quality: 91/100 | Created: 2026-01-11 +Library: plotly 6.7.0 | Python 3.13.13 +Quality: 91/100 | Updated: 2026-05-19 """ +import os +import sys + + +# Remove the script directory from sys.path so that sibling implementations +# (e.g. matplotlib.py) do not shadow installed packages. +_script_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != _script_dir] + +import matplotlib + + +matplotlib.use("Agg") +import matplotlib.pyplot as plt import numpy as np import plotly.graph_objects as go +from scipy import ndimage from scipy.stats import gaussian_kde -# Generate synthetic sediment composition data (sand/silt/clay) -np.random.seed(42) +# 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" +GRID = "rgba(26,26,23,0.12)" if THEME == "light" else "rgba(240,239,232,0.12)" -# Create clustered compositional data with 3 distinct modes +# Data — synthetic sediment composition (sand/silt/clay) +np.random.seed(42) n_samples = 500 -# Cluster 1: Sand-dominant samples (common in beaches/rivers) -cluster1_a = np.random.beta(8, 2, n_samples // 3) * 70 + 25 # Sand: 25-95% -cluster1_b = np.random.beta(2, 5, n_samples // 3) * 40 # Silt: 0-40% +# Cluster 1: Sand-dominant samples (beaches/river channels) +cluster1_a = np.random.beta(8, 2, n_samples // 3) * 70 + 25 +cluster1_b = np.random.beta(2, 5, n_samples // 3) * 40 cluster1_c = 100 - cluster1_a - cluster1_b mask1 = cluster1_c >= 0 cluster1_a, cluster1_b, cluster1_c = cluster1_a[mask1], cluster1_b[mask1], cluster1_c[mask1] -# Cluster 2: Silt-dominant samples (common in floodplains) -cluster2_b = np.random.beta(6, 2, n_samples // 3) * 60 + 30 # Silt: 30-90% -cluster2_a = np.random.beta(2, 4, n_samples // 3) * 35 # Sand: 0-35% +# Cluster 2: Silt-dominant samples (floodplains/estuaries) +cluster2_b = np.random.beta(7, 2, n_samples // 3) * 55 + 35 +cluster2_a = np.random.beta(2, 5, n_samples // 3) * 30 cluster2_c = 100 - cluster2_a - cluster2_b mask2 = cluster2_c >= 0 cluster2_a, cluster2_b, cluster2_c = cluster2_a[mask2], cluster2_b[mask2], cluster2_c[mask2] -# Cluster 3: Mixed samples (balanced composition) -cluster3_a = np.random.beta(3, 3, n_samples // 3) * 50 + 20 # Sand: 20-70% -cluster3_b = np.random.beta(3, 3, n_samples // 3) * 50 + 10 # Silt: 10-60% -cluster3_c = 100 - cluster3_a - cluster3_b -mask3 = cluster3_c >= 0 +# Cluster 3: Clay-dominant mixed samples (deep lake sediments) +cluster3_c = np.random.beta(6, 2, n_samples // 3) * 50 + 30 +cluster3_a = np.random.beta(2, 4, n_samples // 3) * 35 +cluster3_b = 100 - cluster3_a - cluster3_c +mask3 = cluster3_b >= 0 cluster3_a, cluster3_b, cluster3_c = cluster3_a[mask3], cluster3_b[mask3], cluster3_c[mask3] -# Combine all clusters +# Combine clusters sand = np.concatenate([cluster1_a, cluster2_a, cluster3_a]) silt = np.concatenate([cluster1_b, cluster2_b, cluster3_b]) clay = np.concatenate([cluster1_c, cluster2_c, cluster3_c]) -# Normalize to ensure sum = 100 +# Normalize to sum = 100 total = sand + silt + clay sand = sand / total * 100 silt = silt / total * 100 clay = clay / total * 100 -# Convert ternary coordinates to Cartesian for KDE -# Using standard ternary to Cartesian transformation +# Convert ternary → Cartesian for KDE x_cart = 0.5 * (2 * silt + clay) / 100 y_cart = (np.sqrt(3) / 2) * clay / 100 -# Perform 2D kernel density estimation +# 2D kernel density estimation coords = np.vstack([x_cart, y_cart]) kde = gaussian_kde(coords, bw_method="scott") -# Create grid for density estimation (in Cartesian space) +# Evaluation grid (Cartesian space) grid_size = 100 x_grid = np.linspace(0, 1, grid_size) y_grid = np.linspace(0, np.sqrt(3) / 2, grid_size) xx, yy = np.meshgrid(x_grid, y_grid) grid_coords = np.vstack([xx.ravel(), yy.ravel()]) -# Evaluate KDE on grid density = kde(grid_coords).reshape(xx.shape) -# Mask points outside the triangle -# Point is inside triangle if: y >= 0, y <= sqrt(3)*x, y <= sqrt(3)*(1-x) +# Mask outside the ternary triangle inside_triangle = (yy >= 0) & (yy <= np.sqrt(3) * xx) & (yy <= np.sqrt(3) * (1 - xx)) density[~inside_triangle] = np.nan -# Convert grid back to ternary coordinates for plotting -# Inverse transformation: clay = y * 2/sqrt(3) * 100, silt = (x - clay/200) * 100 +# Back to ternary coordinates for plotting clay_grid = yy * (2 / np.sqrt(3)) * 100 silt_grid = (xx - clay_grid / 200) * 100 sand_grid = 100 - silt_grid - clay_grid -# Create figure with ternary axes +# Plot fig = go.Figure() -# Add density heatmap using scatterternary with colored markers +# Density layer — Viridis-colored scatter markers valid_mask = inside_triangle & ~np.isnan(density) a_flat = sand_grid[valid_mask] b_flat = silt_grid[valid_mask] c_flat = clay_grid[valid_mask] d_flat = density[valid_mask] -# Add filled contours as a scatter ternary with colorscale fig.add_trace( go.Scatterternary( a=a_flat, @@ -100,90 +116,107 @@ "colorscale": "Viridis", "showscale": True, "colorbar": { - "title": {"text": "Density", "font": {"size": 20}}, - "tickfont": {"size": 16}, + "title": {"text": "Density", "font": {"size": 20, "color": INK}}, + "tickfont": {"size": 16, "color": INK_SOFT}, "len": 0.7, "thickness": 25, "x": 1.02, + "bgcolor": ELEVATED_BG, + "bordercolor": INK_SOFT, + "borderwidth": 1, }, - "opacity": 0.8, + "opacity": 0.85, }, hovertemplate="Sand: %{a:.1f}%
Silt: %{b:.1f}%
Clay: %{c:.1f}%", showlegend=False, ) ) -# Add contour lines for key density levels -contour_levels = np.percentile(d_flat, [25, 50, 75, 90]) -for level in contour_levels: - # Find points near this density level - level_mask = np.abs(density - level) < (np.nanmax(density) - np.nanmin(density)) * 0.02 - level_mask = level_mask & valid_mask - if np.sum(level_mask) > 10: - # Sort points by angle from centroid for contour line - a_level = sand_grid[level_mask] - b_level = silt_grid[level_mask] - c_level = clay_grid[level_mask] - - # Convert to Cartesian, compute angles, sort - x_level = 0.5 * (2 * b_level + c_level) / 100 - y_level = (np.sqrt(3) / 2) * c_level / 100 - cx, cy = np.mean(x_level), np.mean(y_level) - angles = np.arctan2(y_level - cy, x_level - cx) - sort_idx = np.argsort(angles) - - fig.add_trace( - go.Scatterternary( - a=np.append(a_level[sort_idx], a_level[sort_idx][0]), - b=np.append(b_level[sort_idx], b_level[sort_idx][0]), - c=np.append(c_level[sort_idx], c_level[sort_idx][0]), - mode="lines", - line={"color": "rgba(255, 255, 255, 0.6)", "width": 2}, - hoverinfo="skip", - showlegend=False, +# Smooth density for clean contour extraction +density_filled = density.copy() +density_filled[np.isnan(density_filled)] = 0 +smoothed = ndimage.gaussian_filter(density_filled, sigma=2) + +# Contour levels from smoothed valid region +smoothed_valid_vals = smoothed[inside_triangle] +contour_levels = np.percentile(smoothed_valid_vals[smoothed_valid_vals > 0], [25, 50, 75, 90]) + +# Extract smooth contour paths via matplotlib (data extraction only, no display) +fig_tmp, ax_tmp = plt.subplots() +CS = ax_tmp.contour(xx, yy, smoothed, levels=contour_levels) +plt.close(fig_tmp) + +contour_color = "rgba(255,255,255,0.85)" if THEME == "light" else "rgba(240,239,232,0.85)" +for segs in CS.allsegs: + for seg in segs: + if len(seg) < 5: + continue + x_c, y_c = seg[:, 0], seg[:, 1] + # Cartesian → ternary + clay_c = y_c * (2 / np.sqrt(3)) * 100 + silt_c = (x_c - clay_c / 200) * 100 + sand_c = 100 - silt_c - clay_c + # Filter to valid ternary region + valid = (sand_c >= 0) & (silt_c >= 0) & (clay_c >= 0) + if valid.sum() > 5: + fig.add_trace( + go.Scatterternary( + a=sand_c[valid], + b=silt_c[valid], + c=clay_c[valid], + mode="lines", + line={"color": contour_color, "width": 2.5}, + hoverinfo="skip", + showlegend=False, + ) ) - ) -# Update layout for ternary plot +# Style fig.update_layout( title={ - "text": "Sediment Composition Distribution · ternary-density · plotly · pyplots.ai", - "font": {"size": 28}, + "text": "Sediment Composition Distribution · ternary-density · python · plotly · anyplot.ai", + "font": {"size": 28, "color": INK}, "x": 0.5, "xanchor": "center", }, + paper_bgcolor=PAGE_BG, + plot_bgcolor=PAGE_BG, + font={"color": INK}, ternary={ "sum": 100, + "bgcolor": PAGE_BG, "aaxis": { - "title": {"text": "Sand (%)", "font": {"size": 22}}, - "tickfont": {"size": 16}, + "title": {"text": "Sand (%)", "font": {"size": 22, "color": INK}}, + "tickfont": {"size": 16, "color": INK_SOFT}, "tickangle": 0, "dtick": 20, - "gridcolor": "rgba(0, 0, 0, 0.15)", + "gridcolor": GRID, + "linecolor": INK_SOFT, "linewidth": 2, }, "baxis": { - "title": {"text": "Silt (%)", "font": {"size": 22}}, - "tickfont": {"size": 16}, + "title": {"text": "Silt (%)", "font": {"size": 22, "color": INK}}, + "tickfont": {"size": 16, "color": INK_SOFT}, "tickangle": 45, "dtick": 20, - "gridcolor": "rgba(0, 0, 0, 0.15)", + "gridcolor": GRID, + "linecolor": INK_SOFT, "linewidth": 2, }, "caxis": { - "title": {"text": "Clay (%)", "font": {"size": 22}}, - "tickfont": {"size": 16}, + "title": {"text": "Clay (%)", "font": {"size": 22, "color": INK}}, + "tickfont": {"size": 16, "color": INK_SOFT}, "tickangle": -45, "dtick": 20, - "gridcolor": "rgba(0, 0, 0, 0.15)", + "gridcolor": GRID, + "linecolor": INK_SOFT, "linewidth": 2, }, - "bgcolor": "rgba(255, 255, 255, 1)", }, - template="plotly_white", + template="none", margin={"l": 80, "r": 120, "t": 100, "b": 80}, ) -# Save outputs -fig.write_image("plot.png", width=1600, height=900, scale=3) -fig.write_html("plot.html", include_plotlyjs="cdn") +# Save +fig.write_image(f"plot-{THEME}.png", width=1600, height=900, scale=3) +fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn") diff --git a/plots/ternary-density/metadata/python/plotly.yaml b/plots/ternary-density/metadata/python/plotly.yaml index 8a1ac883a3..4f990d6c8c 100644 --- a/plots/ternary-density/metadata/python/plotly.yaml +++ b/plots/ternary-density/metadata/python/plotly.yaml @@ -1,160 +1,178 @@ library: plotly +language: python specification_id: ternary-density created: '2026-01-11T09:31:17Z' -updated: '2026-01-11T09:35:45Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20892890889 +updated: '2026-05-19T10:48:57Z' +generated_by: claude-sonnet +workflow_run: 26091196427 issue: 3696 -python_version: 3.13.11 -library_version: 6.5.1 -preview_url: https://storage.googleapis.com/anyplot-images/plots/ternary-density/plotly/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/ternary-density/plotly/plot.html +language_version: 3.13.13 +library_version: 6.7.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/plotly/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/plotly/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/plotly/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/ternary-density/python/plotly/plot-dark.html quality_score: 91 review: strengths: - - Excellent implementation of ternary density using Scatterternary with KDE-colored - markers - - Proper Viridis colormap as specified for perceptually uniform density visualization - - Grid lines visible beneath density layer as required - - Interactive hover tooltips showing Sand/Silt/Clay percentages - - Clean, well-structured code with proper seed for reproducibility - - Contour lines at percentile levels (25th, 50th, 75th, 90th) enhance interpretation - - Realistic sediment composition context with distinct clusters + - 'Perfect spec compliance: KDE, contour lines at percentile levels, vertex labels, + grid, and viridis density colormap all delivered' + - Geologically realistic three-cluster dataset (beach/floodplain/deep-lake) makes + excellent use of the sediment composition context + - 'Theme-adaptive chrome correctly applied across all elements: title, axis labels, + tick fonts, colorbar, backgrounds' + - Flat canvas size (4800x2700) and font sizes within spec + - Matplotlib used only for data extraction (contour paths) with plt.close() properly + called — no fake interactivity weaknesses: - - Contour lines (white with alpha 0.6) could be more prominent for easier density - level interpretation - - Grid at alpha 0.15 is quite subtle; alpha 0.25-0.3 might improve readability - - Silt-dominant and mixed clusters are less visually distinct than the sand-dominant - cluster - image_description: 'The plot displays a ternary density visualization for sediment - composition with three vertices labeled Sand (%), Silt (%), and Clay (%). The - density is rendered using small circular markers colored according to the Viridis - colormap, ranging from dark purple (low density ~0) to yellow (high density ~6). - The triangular plot shows multiple density hotspots: a prominent high-density - region (yellow) near the Sand vertex indicating sand-dominant samples, and additional - concentration areas visible in teal/green colors in the mid-regions. White contour - lines overlay the density field at key percentile levels. Grid lines at 20% intervals - are visible beneath the density layer. A vertical colorbar on the right shows - the density scale. The title follows the correct format with the spec-id, library, - and pyplots.ai branding.' + - Tick font size is 16px vs recommended 18px for pixel-based libraries — minor readability + reduction at full canvas + - White contour color (rgba(255,255,255,0.85)) in the light theme has variable contrast + against bright viridis regions; a dark semi-transparent color would maintain contrast + across both themes more robustly + - Scatter-marker density approach creates a slightly dotted texture rather than + a fully smooth continuous heatmap surface + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct theme surface, not pure white + Chrome: Title "Sediment Composition Distribution · ternary-density · python · plotly · anyplot.ai" in dark ink, clearly readable. Axis labels Sand (%), Silt (%), Clay (%) in dark ink at 22px, readable. Tick labels at 0/20/40/60/80/100 in secondary ink at 16px, readable though slightly small. + Data: Viridis-colored scatter markers (~10000 points) fill the ternary triangle showing three distinct density clusters — sand apex (top), silt corner (bottom-left), clay corner (bottom-right). White contour lines at 25/50/75/90th percentile density levels overlay the surface. Viridis colorbar on right labeled "Density". + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct dark theme surface, not pure black + Chrome: Title in light text (#F0EFE8), clearly readable against dark background. Axis labels and tick labels in secondary light text (#B8B7B0), readable. Colorbar fonts are light-themed. No dark-on-dark failures observed. + Data: Same viridis colorscale as light render — data colors identical, only chrome flips. Three density clusters more visually dramatic against the dark surface; yellow-green peaks contrast sharply with deep-purple low-density areas. White contour lines highly visible in dark theme. + Legibility verdict: PASS criteria_checklist: visual_quality: - score: 37 - max: 40 + score: 28 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 7 + max: 8 passed: true - comment: Title at 28pt, axis labels at 22pt, tick labels at 16pt - all clearly - readable + comment: All text readable in both themes; tick font 16px slightly below recommended + 18px for pixel-based libs - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 5 + max: 6 passed: true - comment: No overlapping text elements; labels well positioned outside triangle + comment: Minor tick label crowding on ternary axes at lower values - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Marker size of 6 works well for density visualization; slightly small - but density pattern is clear + comment: Density surface and contour lines clearly visible in both themes - 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 + name: Layout & Canvas score: 4 - max: 5 + max: 4 passed: true - comment: Good canvas utilization; triangle fills ~60% of plot area; colorbar - well positioned + comment: 4800x2700 px, good proportions, nothing cut off - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: 'All three axes labeled with units: Sand (%), Silt (%), Clay (%)' + comment: Sand (%), Silt (%), Clay (%) with units; descriptive title - id: VQ-07 - name: Grid & Legend - score: 1 + name: Palette Compliance + score: 2 max: 2 passed: true - comment: Grid visible at alpha 0.15; colorbar present but grid could be slightly - more prominent + comment: 'Viridis for continuous density data; correct backgrounds #FAF8F1/#1A1A17' + design_excellence: + score: 14 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Viridis density surface + contour lines at percentile levels + themed + colorbar show deliberate design choices + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: template=none removes defaults; styled colorbar with themed fonts/border/bg; + ternary bg matches page surface + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Three clusters map to distinct geological environments; contours + guide viewer; viridis naturally encodes density spec_compliance: - score: 24 - 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 axes - - id: SC-03 + comment: Correct ternary density chart using go.Scatterternary with KDE-colored + markers + - id: SC-02 name: Required Features score: 4 - max: 5 + max: 4 passed: true - comment: Density heatmap via KDE present; contour lines present; contours - are subtle white lines - - id: SC-04 - name: Data Range + comment: KDE via gaussian_kde, contour lines at 25/50/75/90th percentiles, + vertex labels, visible grid + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All data visible; axes span 0-100% - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: Colorbar accurately shows density scale - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: Sand/silt/clay correctly mapped; all values normalized to sum to + 100% + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: Title correctly formatted with description, spec-id, library, and - pyplots.ai + comment: Title matches required format; colorbar labeled Density data_quality: - score: 18 - 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 3 distinct clusters as intended; mixed cluster less visible - than sand-dominant + comment: Three geologically distinct clusters demonstrate sand/silt/clay dominated + sediment environments - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Sediment composition is classic geological application; neutral scientific - context + comment: Sediment composition is canonical ternary dataset from geoscience; + cluster parameters are geologically plausible - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Percentages 0-100 appropriate; cluster distributions plausible for - real sediment + comment: 500 samples within 100-5000 recommended range; values 0-100% summing + to 100% code_quality: score: 10 max: 10 @@ -164,46 +182,56 @@ review: score: 3 max: 3 passed: true - comment: 'Simple linear structure: imports, data generation, KDE, plot, save' + comment: Flat script, no functions or classes - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: np.random.seed(42) set at beginning + comment: np.random.seed(42) set - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: 'Only necessary imports: numpy, plotly.graph_objects, scipy.stats.gaussian_kde' + comment: All imports used; matplotlib for contour extraction, scipy for KDE + and smoothing - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses current Plotly API + comment: Matplotlib contour extraction is legitimate data-extraction pattern, + not fake UI - 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 (4800x2700) and plot-{THEME}.html + library_mastery: + score: 9 + max: 10 items: - - id: LF-01 - name: Distinctive Features + - id: LM-01 + name: Idiomatic Usage score: 5 max: 5 passed: true - comment: Uses Plotly Scatterternary with interactive hover tooltips; exports - both PNG and HTML + comment: go.Scatterternary is correct Plotly trace for ternary coordinates; + layout ternary dict properly configured + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Colorbar in marker dict, hovertemplate with ternary formatting, HTML + export; slight deduction for matplotlib contour workaround verdict: APPROVED impl_tags: dependencies: - scipy + - matplotlib techniques: - colorbar - hover-tooltips @@ -213,7 +241,7 @@ impl_tags: - iteration-over-groups dataprep: - kde + - normalization styling: - custom-colormap - alpha-blending - - grid-styling