{"spec_id":"ternary-density","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nternary-density: Ternary Density Plot\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport matplotlib.cm as cm\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport seaborn as sns\nfrom matplotlib.colors import Normalize\nfrom matplotlib.patches import Polygon\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\"\nACCENT = \"#4467A3\"  # Okabe-Ito blue — structural elements\n\nsns.set_theme(style=\"ticks\", rc={\"figure.facecolor\": PAGE_BG, \"axes.facecolor\": PAGE_BG, \"text.color\": INK})\n\n# Data - Soil composition samples (sand/silt/clay percentages)\nnp.random.seed(42)\n\n# Cluster 1: Sandy soils (high sand content)\nn1 = 200\nsand1 = np.random.beta(5, 2, n1) * 70 + 25\nsilt1 = np.random.beta(2, 3, n1) * (100 - sand1) * 0.6\nclay1 = 100 - sand1 - silt1\n\n# Cluster 2: Silty soils (high silt content)\nn2 = 150\nsilt2 = np.random.beta(5, 2, n2) * 60 + 30\nsand2 = np.random.beta(2, 3, n2) * (100 - silt2) * 0.5\nclay2 = 100 - sand2 - silt2\n\n# Cluster 3: Clay-rich soils\nn3 = 150\nclay3 = np.random.beta(4, 2, n3) * 50 + 30\nsand3 = np.random.beta(2, 3, n3) * (100 - clay3) * 0.4\nsilt3 = 100 - clay3 - sand3\n\nsand = np.concatenate([sand1, sand2, sand3])\nsilt = np.concatenate([silt1, silt2, silt3])\nclay = np.concatenate([clay1, clay2, clay3])\n\n# Transform ternary to Cartesian: Sand → bottom-left, Silt → bottom-right, Clay → top\ntotal = sand + silt + clay\nsand_norm = sand / total\nsilt_norm = silt / total\nclay_norm = clay / total\nx = 0.5 * (2 * silt_norm + clay_norm)\ny = (np.sqrt(3) / 2) * clay_norm\n\nsqrt3_2 = np.sqrt(3) / 2\nvertices = np.array([[0, 0], [1, 0], [0.5, sqrt3_2]])\n\n# Plot\nfig, ax = plt.subplots(figsize=(12, 12), facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\ntriangle_clip = Polygon(vertices, transform=ax.transData)\n\n# Grid lines at 10% intervals (all three ternary families)\nfor i in range(1, 10):\n    frac = i / 10\n    gkw = {\"color\": INK_SOFT, \"alpha\": 0.20, \"linewidth\": 0.8, \"zorder\": 1}\n    # Constant clay (horizontal, parallel to base)\n    ax.plot([0.5 * frac, 1 - 0.5 * frac], [sqrt3_2 * frac, sqrt3_2 * frac], **gkw)\n    # Constant silt (parallel to Sand-Clay left edge, slope +√3)\n    ax.plot([frac, 0.5 * (1 + frac)], [0, sqrt3_2 * (1 - frac)], **gkw)\n    # Constant sand (parallel to Silt-Clay right edge, slope -√3)\n    ax.plot([1 - frac, 0.5 * (1 - frac)], [0, sqrt3_2 * (1 - frac)], **gkw)\n\n# KDE density fill\nsns.kdeplot(x=x, y=y, fill=True, cmap=\"viridis\", levels=20, alpha=0.85, ax=ax, thresh=0.02, zorder=5)\n\nfor collection in ax.collections:\n    collection.set_clip_path(triangle_clip)\n\n# KDE contour lines — thicker for visibility at full resolution\nsns.kdeplot(x=x, y=y, levels=10, color=ACCENT, linewidths=3.0, ax=ax, zorder=6)\n\nfor collection in ax.collections:\n    collection.set_clip_path(triangle_clip)\n\n# Triangle boundary\nax.add_patch(Polygon(vertices, fill=False, edgecolor=ACCENT, linewidth=4, zorder=15))\n\n# Colorbar showing relative density scale\nsm = cm.ScalarMappable(cmap=\"viridis\", norm=Normalize(vmin=0, vmax=1))\nsm.set_array([])\ncbar = plt.colorbar(sm, ax=ax, fraction=0.025, pad=0.04, aspect=20, shrink=0.55)\ncbar.set_label(\"Relative Density\", fontsize=18, labelpad=12)\ncbar.ax.yaxis.label.set_color(INK)\ncbar.set_ticks([0, 0.5, 1.0])\ncbar.ax.set_yticklabels([\"Low\", \"Medium\", \"High\"])\nfor lbl in cbar.ax.get_yticklabels():\n    lbl.set_color(INK_SOFT)\n    lbl.set_fontsize(16)\ncbar.ax.tick_params(colors=INK_SOFT)\ncbar.outline.set_edgecolor(INK_SOFT)\n\n# Vertex labels (component names + unit)\nax.text(0, -0.08, \"Sand (%)\", ha=\"center\", va=\"top\", fontsize=22, fontweight=\"bold\", color=INK)\nax.text(1, -0.08, \"Silt (%)\", ha=\"center\", va=\"top\", fontsize=22, fontweight=\"bold\", color=INK)\nax.text(0.5, sqrt3_2 + 0.08, \"Clay (%)\", ha=\"center\", va=\"bottom\", fontsize=22, fontweight=\"bold\", color=INK)\n\n# Percentage tick labels along each edge\nfor i in [2, 4, 6, 8]:\n    frac = i / 10\n    ax.text(frac, -0.04, f\"{int(frac * 100)}\", ha=\"center\", va=\"top\", fontsize=16, color=INK_SOFT)\n    ax.text(\n        0.5 * frac - 0.04, sqrt3_2 * frac, f\"{int(frac * 100)}\", ha=\"right\", va=\"center\", fontsize=16, color=INK_SOFT\n    )\n    ax.text(\n        1 - 0.5 * frac + 0.04, sqrt3_2 * frac, f\"{int(frac * 100)}\", ha=\"left\", va=\"center\", fontsize=16, color=INK_SOFT\n    )\n\n# Cluster annotations — label each density peak for data storytelling\nann_kw = {\n    \"fontsize\": 15,\n    \"fontweight\": \"bold\",\n    \"color\": INK,\n    \"bbox\": {\"boxstyle\": \"round,pad=0.3\", \"facecolor\": PAGE_BG, \"edgecolor\": INK_SOFT, \"alpha\": 0.75},\n}\nax.text(0.13, 0.04, \"Sandy\\nsoils\", ha=\"center\", va=\"center\", **ann_kw)\nax.text(0.80, 0.11, \"Silty\\nsoils\", ha=\"center\", va=\"center\", **ann_kw)\nax.text(0.53, 0.50, \"Clay-rich\\nsoils\", ha=\"center\", va=\"center\", **ann_kw)\n\n# Style\nax.set_title(\n    \"Soil Composition · ternary-density · python · seaborn · anyplot.ai\",\n    fontsize=24,\n    fontweight=\"medium\",\n    color=INK,\n    pad=25,\n)\nax.set_xlim(-0.15, 1.15)\nax.set_ylim(-0.15, 1.05)\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"}