{"spec_id":"ternary-density","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nternary-density: Ternary Density Plot\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 92/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.patches import Polygon\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\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nSQ3H = np.sqrt(3) / 2  # sqrt(3)/2, reused throughout\n\n# Data: sediment composition (sand/silt/clay) with three cluster types\nnp.random.seed(42)\n\nn1 = 300\nsand1 = np.random.beta(5, 2, n1) * 60 + 35\nsilt1 = np.random.beta(2, 3, n1) * (100 - sand1) * 0.6\nclay1 = 100 - sand1 - silt1\n\nn2 = 250\nsilt2 = np.random.beta(5, 2, n2) * 50 + 40\nsand2 = np.random.beta(2, 4, n2) * (100 - silt2) * 0.5\nclay2 = 100 - sand2 - silt2\n\nn3 = 250\nclay3 = np.random.beta(4, 2, n3) * 45 + 40\nsand3 = np.random.beta(2, 5, n3) * (100 - clay3) * 0.4\nsilt3 = 100 - sand3 - clay3\n\nsand = np.clip(np.concatenate([sand1, sand2, sand3]), 0, 100)\nsilt = np.clip(np.concatenate([silt1, silt2, silt3]), 0, 100)\nclay = np.clip(np.concatenate([clay1, clay2, clay3]), 0, 100)\ntotal = sand + silt + clay\nsand, silt, clay = sand / total, silt / total, clay / total\n\n# Ternary → Cartesian: Sand vertex (0,0), Silt vertex (1,0), Clay vertex (0.5, SQ3H)\n# x = silt + 0.5 * clay,  y = SQ3H * clay\nx_data = silt + 0.5 * clay\ny_data = SQ3H * clay\n\n# KDE density grid\ngrid_res = 200\nxi = np.linspace(0, 1, grid_res)\nyi = np.linspace(0, SQ3H, grid_res)\nXi, Yi = np.meshgrid(xi, yi)\nCi = Yi / SQ3H\nBi = Xi - 0.5 * Ci\nAi = 1 - Bi - Ci\nmask = (Ai >= 0) & (Bi >= 0) & (Ci >= 0)\n\nkde = gaussian_kde(np.vstack([x_data, y_data]), bw_method=\"silverman\")\nZ = kde(np.vstack([Xi.ravel(), Yi.ravel()])).reshape(Xi.shape)\nZ = np.where(mask, Z, np.nan)\n\n# Plot (12×12 → 3600×3600 px at 300 dpi, valid 1:1 format for symmetric plot)\nfig, ax = plt.subplots(figsize=(12, 12), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Triangle boundary\nax.add_patch(Polygon([[0, 0], [1, 0], [0.5, SQ3H]], fill=False, edgecolor=INK_SOFT, linewidth=2.5, zorder=10))\n\n# Grid lines — three families, one per component (20/40/60/80%)\nfor lv in [0.2, 0.4, 0.6, 0.8]:\n    # Constant Clay% — parallel to bottom Sand–Silt edge\n    ax.plot(\n        [0.5 * lv, 1 - 0.5 * lv],\n        [SQ3H * lv, SQ3H * lv],\n        color=INK_MUTED,\n        linewidth=0.8,\n        alpha=0.35,\n        linestyle=\"--\",\n        zorder=1,\n    )\n    # Constant Silt% — parallel to left Sand–Clay edge\n    ax.plot(\n        [lv, 0.5 + 0.5 * lv], [0, SQ3H * (1 - lv)], color=INK_MUTED, linewidth=0.8, alpha=0.35, linestyle=\"--\", zorder=1\n    )\n    # Constant Sand% — parallel to right Silt–Clay edge\n    ax.plot(\n        [0.5 * (1 - lv), 1 - lv],\n        [SQ3H * (1 - lv), 0],\n        color=INK_MUTED,\n        linewidth=0.8,\n        alpha=0.35,\n        linestyle=\"--\",\n        zorder=1,\n    )\n\n# Density heatmap and contour lines\ndensity_plot = ax.contourf(Xi, Yi, Z, levels=20, cmap=\"viridis\", alpha=0.85, zorder=2)\nax.contour(Xi, Yi, Z, levels=6, colors=\"white\", linewidths=1.5, alpha=0.6, zorder=3)\n\n# Colorbar\ncbar = plt.colorbar(density_plot, ax=ax, shrink=0.65, pad=0.02)\ncbar.set_label(\"Density\", fontsize=20, color=INK)\ncbar.ax.tick_params(labelsize=16, colors=INK_SOFT)\ncbar.ax.yaxis.label.set_color(INK)\n\n# Vertex labels\nax.text(0, -0.06, \"Sand\", fontsize=22, ha=\"center\", va=\"top\", fontweight=\"bold\", color=INK)\nax.text(1, -0.06, \"Silt\", fontsize=22, ha=\"center\", va=\"top\", fontweight=\"bold\", color=INK)\nax.text(0.5, SQ3H + 0.05, \"Clay\", fontsize=22, ha=\"center\", va=\"bottom\", fontweight=\"bold\", color=INK)\n\n# Edge tick labels — each edge shows a DIFFERENT component's % scale\nfor pct in [20, 40, 60, 80]:\n    f = pct / 100\n    # Bottom edge: Sand% decreasing left→right (100% at Sand vertex, 0% at Silt vertex)\n    # At Sand=f, Silt=1-f, Clay=0 → x = 1-f, y = 0\n    ax.text(1 - f, -0.03, f\"{pct}\", fontsize=13, ha=\"center\", va=\"top\", color=INK_MUTED)\n    # Left edge: Clay% increasing bottom→top (0% at Sand vertex, 100% at Clay vertex)\n    # At Clay=f, Sand=1-f, Silt=0 → x = 0.5·f, y = SQ3H·f\n    ax.text(0.5 * f - 0.04, SQ3H * f, f\"{pct}\", fontsize=13, ha=\"right\", va=\"center\", color=INK_MUTED)\n    # Right edge: Silt% decreasing bottom→top (100% at Silt vertex, 0% at Clay vertex)\n    # At Silt=f, Sand=0, Clay=1-f → x = 0.5+0.5·f, y = SQ3H·(1-f)\n    ax.text(0.5 + 0.5 * f + 0.04, SQ3H * (1 - f), f\"{pct}\", fontsize=13, ha=\"left\", va=\"center\", color=INK_MUTED)\n\n# Axis direction labels outside each edge (identify which component the ticks measure)\nax.text(0.5, -0.14, \"Sand (%)\", fontsize=14, ha=\"center\", va=\"top\", color=INK_SOFT, style=\"italic\")\nax.text(\n    0.19, SQ3H * 0.54, \"Clay (%)\", fontsize=14, ha=\"center\", va=\"center\", color=INK_SOFT, style=\"italic\", rotation=60\n)\nax.text(\n    0.81, SQ3H * 0.54, \"Silt (%)\", fontsize=14, ha=\"center\", va=\"center\", color=INK_SOFT, style=\"italic\", rotation=-60\n)\n\n# Title\nax.set_title(\n    \"Sediment Composition Analysis\\nternary-density · python · matplotlib · anyplot.ai\", fontsize=24, pad=20, color=INK\n)\n\nax.set_xlim(-0.15, 1.2)\nax.set_ylim(-0.22, SQ3H + 0.2)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=300, bbox_inches=\"tight\", facecolor=PAGE_BG)\n"}