{"spec_id":"ternary-density","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nternary-density: Ternary Density Plot\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script directory from sys.path so that sibling implementations\n# (e.g. matplotlib.py) do not shadow installed packages.\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != _script_dir]\n\nimport matplotlib\n\n\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport plotly.graph_objects as go\nfrom scipy import ndimage\nfrom scipy.stats import gaussian_kde\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.12)\" if THEME == \"light\" else \"rgba(240,239,232,0.12)\"\n\n# Data — synthetic sediment composition (sand/silt/clay)\nnp.random.seed(42)\nn_samples = 500\n\n# Cluster 1: Sand-dominant samples (beaches/river channels)\ncluster1_a = np.random.beta(8, 2, n_samples // 3) * 70 + 25\ncluster1_b = np.random.beta(2, 5, n_samples // 3) * 40\ncluster1_c = 100 - cluster1_a - cluster1_b\nmask1 = cluster1_c >= 0\ncluster1_a, cluster1_b, cluster1_c = cluster1_a[mask1], cluster1_b[mask1], cluster1_c[mask1]\n\n# Cluster 2: Silt-dominant samples (floodplains/estuaries)\ncluster2_b = np.random.beta(7, 2, n_samples // 3) * 55 + 35\ncluster2_a = np.random.beta(2, 5, n_samples // 3) * 30\ncluster2_c = 100 - cluster2_a - cluster2_b\nmask2 = cluster2_c >= 0\ncluster2_a, cluster2_b, cluster2_c = cluster2_a[mask2], cluster2_b[mask2], cluster2_c[mask2]\n\n# Cluster 3: Clay-dominant mixed samples (deep lake sediments)\ncluster3_c = np.random.beta(6, 2, n_samples // 3) * 50 + 30\ncluster3_a = np.random.beta(2, 4, n_samples // 3) * 35\ncluster3_b = 100 - cluster3_a - cluster3_c\nmask3 = cluster3_b >= 0\ncluster3_a, cluster3_b, cluster3_c = cluster3_a[mask3], cluster3_b[mask3], cluster3_c[mask3]\n\n# Combine clusters\nsand = np.concatenate([cluster1_a, cluster2_a, cluster3_a])\nsilt = np.concatenate([cluster1_b, cluster2_b, cluster3_b])\nclay = np.concatenate([cluster1_c, cluster2_c, cluster3_c])\n\n# Normalize to sum = 100\ntotal = sand + silt + clay\nsand = sand / total * 100\nsilt = silt / total * 100\nclay = clay / total * 100\n\n# Convert ternary → Cartesian for KDE\nx_cart = 0.5 * (2 * silt + clay) / 100\ny_cart = (np.sqrt(3) / 2) * clay / 100\n\n# 2D kernel density estimation\ncoords = np.vstack([x_cart, y_cart])\nkde = gaussian_kde(coords, bw_method=\"scott\")\n\n# Evaluation grid (Cartesian space)\ngrid_size = 100\nx_grid = np.linspace(0, 1, grid_size)\ny_grid = np.linspace(0, np.sqrt(3) / 2, grid_size)\nxx, yy = np.meshgrid(x_grid, y_grid)\ngrid_coords = np.vstack([xx.ravel(), yy.ravel()])\n\ndensity = kde(grid_coords).reshape(xx.shape)\n\n# Mask outside the ternary triangle\ninside_triangle = (yy >= 0) & (yy <= np.sqrt(3) * xx) & (yy <= np.sqrt(3) * (1 - xx))\ndensity[~inside_triangle] = np.nan\n\n# Back to ternary coordinates for plotting\nclay_grid = yy * (2 / np.sqrt(3)) * 100\nsilt_grid = (xx - clay_grid / 200) * 100\nsand_grid = 100 - silt_grid - clay_grid\n\n# Plot\nfig = go.Figure()\n\n# Density layer — Viridis-colored scatter markers\nvalid_mask = inside_triangle & ~np.isnan(density)\na_flat = sand_grid[valid_mask]\nb_flat = silt_grid[valid_mask]\nc_flat = clay_grid[valid_mask]\nd_flat = density[valid_mask]\n\nfig.add_trace(\n    go.Scatterternary(\n        a=a_flat,\n        b=b_flat,\n        c=c_flat,\n        mode=\"markers\",\n        marker={\n            \"size\": 6,\n            \"color\": d_flat,\n            \"colorscale\": \"Viridis\",\n            \"showscale\": True,\n            \"colorbar\": {\n                \"title\": {\"text\": \"Density\", \"font\": {\"size\": 20, \"color\": INK}},\n                \"tickfont\": {\"size\": 16, \"color\": INK_SOFT},\n                \"len\": 0.7,\n                \"thickness\": 25,\n                \"x\": 1.02,\n                \"bgcolor\": ELEVATED_BG,\n                \"bordercolor\": INK_SOFT,\n                \"borderwidth\": 1,\n            },\n            \"opacity\": 0.85,\n        },\n        hovertemplate=\"Sand: %{a:.1f}%<br>Silt: %{b:.1f}%<br>Clay: %{c:.1f}%<extra></extra>\",\n        showlegend=False,\n    )\n)\n\n# Smooth density for clean contour extraction\ndensity_filled = density.copy()\ndensity_filled[np.isnan(density_filled)] = 0\nsmoothed = ndimage.gaussian_filter(density_filled, sigma=2)\n\n# Contour levels from smoothed valid region\nsmoothed_valid_vals = smoothed[inside_triangle]\ncontour_levels = np.percentile(smoothed_valid_vals[smoothed_valid_vals > 0], [25, 50, 75, 90])\n\n# Extract smooth contour paths via matplotlib (data extraction only, no display)\nfig_tmp, ax_tmp = plt.subplots()\nCS = ax_tmp.contour(xx, yy, smoothed, levels=contour_levels)\nplt.close(fig_tmp)\n\ncontour_color = \"rgba(255,255,255,0.85)\" if THEME == \"light\" else \"rgba(240,239,232,0.85)\"\nfor segs in CS.allsegs:\n    for seg in segs:\n        if len(seg) < 5:\n            continue\n        x_c, y_c = seg[:, 0], seg[:, 1]\n        # Cartesian → ternary\n        clay_c = y_c * (2 / np.sqrt(3)) * 100\n        silt_c = (x_c - clay_c / 200) * 100\n        sand_c = 100 - silt_c - clay_c\n        # Filter to valid ternary region\n        valid = (sand_c >= 0) & (silt_c >= 0) & (clay_c >= 0)\n        if valid.sum() > 5:\n            fig.add_trace(\n                go.Scatterternary(\n                    a=sand_c[valid],\n                    b=silt_c[valid],\n                    c=clay_c[valid],\n                    mode=\"lines\",\n                    line={\"color\": contour_color, \"width\": 2.5},\n                    hoverinfo=\"skip\",\n                    showlegend=False,\n                )\n            )\n\n# Style\nfig.update_layout(\n    title={\n        \"text\": \"Sediment Composition Distribution · ternary-density · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 28, \"color\": INK},\n        \"x\": 0.5,\n        \"xanchor\": \"center\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    ternary={\n        \"sum\": 100,\n        \"bgcolor\": PAGE_BG,\n        \"aaxis\": {\n            \"title\": {\"text\": \"Sand (%)\", \"font\": {\"size\": 22, \"color\": INK}},\n            \"tickfont\": {\"size\": 16, \"color\": INK_SOFT},\n            \"tickangle\": 0,\n            \"dtick\": 20,\n            \"gridcolor\": GRID,\n            \"linecolor\": INK_SOFT,\n            \"linewidth\": 2,\n        },\n        \"baxis\": {\n            \"title\": {\"text\": \"Silt (%)\", \"font\": {\"size\": 22, \"color\": INK}},\n            \"tickfont\": {\"size\": 16, \"color\": INK_SOFT},\n            \"tickangle\": 45,\n            \"dtick\": 20,\n            \"gridcolor\": GRID,\n            \"linecolor\": INK_SOFT,\n            \"linewidth\": 2,\n        },\n        \"caxis\": {\n            \"title\": {\"text\": \"Clay (%)\", \"font\": {\"size\": 22, \"color\": INK}},\n            \"tickfont\": {\"size\": 16, \"color\": INK_SOFT},\n            \"tickangle\": -45,\n            \"dtick\": 20,\n            \"gridcolor\": GRID,\n            \"linecolor\": INK_SOFT,\n            \"linewidth\": 2,\n        },\n    },\n    template=\"none\",\n    margin={\"l\": 80, \"r\": 120, \"t\": 100, \"b\": 80},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=1600, height=900, scale=3)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}