{"spec_id":"ternary-basic","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\nternary-basic: Basic Ternary Plot\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove script directory from sys.path to avoid local matplotlib.py shadow\nscript_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != script_dir]\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\nfrom matplotlib.colors import LinearSegmentedColormap\nfrom matplotlib.patches import Polygon\n\n\n# Theme tokens (see prompts/default-style-guide.md)\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\"\nBRAND = \"#009E73\"  # Okabe-Ito position 1\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Set seaborn/matplotlib theme\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n    },\n)\n\n# Data - Soil texture samples (Sand, Silt, Clay), USDA-style ternary domain\n# alpha=1 draws uniformly over the simplex, giving realistic coverage all the\n# way to near-pure single-component corners (unlike a peaked Dirichlet).\nnp.random.seed(42)\nn_points = 50\nraw = np.random.dirichlet(alpha=[1.0, 1.0, 1.0], size=n_points) * 100\ndf = pd.DataFrame({\"Sand\": raw[:, 0], \"Silt\": raw[:, 1], \"Clay\": raw[:, 2]})\n\n# Ternary coordinates transformation (vectorized)\n# Convert (a, b, c) to Cartesian (x, y) where a + b + c = 100\n# Triangle vertices: Bottom-left (0,0)=Sand, Bottom-right (1,0)=Silt, Top (0.5, sqrt(3)/2)=Clay\nsqrt3_2 = np.sqrt(3) / 2\nsand_norm = df[\"Sand\"].values / 100\nsilt_norm = df[\"Silt\"].values / 100\nclay_norm = df[\"Clay\"].values / 100\nx = 0.5 * (2 * silt_norm + clay_norm)\ny = sqrt3_2 * clay_norm\nscatter_df = pd.DataFrame({\"x\": x, \"y\": y})\n\n# Create plot — canonical square canvas: figsize(6,6) x dpi=400 -> 2400x2400 px\nfig, ax = plt.subplots(figsize=(6, 6), dpi=400, facecolor=PAGE_BG)\n\n# Triangle outline (used both for drawing and as a density clip mask)\ntriangle = np.array([[0, 0], [1, 0], [0.5, sqrt3_2], [0, 0]])\n\n# Density backdrop — genuine seaborn statistical estimator (KDE), clipped to\n# the simplex, to give the point cloud a visual hierarchy instead of a flat\n# scatter (addresses DE-03 storytelling + LM-02 distinctive-feature usage).\nexisting_collections = set(ax.collections)\nsns.kdeplot(\n    x=scatter_df[\"x\"],\n    y=scatter_df[\"y\"],\n    ax=ax,\n    fill=True,\n    cmap=imprint_seq,\n    alpha=0.35,\n    levels=6,\n    thresh=0.15,\n    zorder=1,\n)\nclip_patch = Polygon(triangle[:-1], transform=ax.transData)\nfor collection in ax.collections:\n    if collection not in existing_collections:\n        collection.set_clip_path(clip_patch)\n\n# Draw triangle outline\nax.plot(triangle[:, 0], triangle[:, 1], color=INK_SOFT, linewidth=1, zorder=5)\n\n# Draw grid lines at 10% intervals\ngrid_lw = 0.5\n\nfor level in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:\n    # Lines parallel to bottom (constant Clay)\n    x1, y1 = 0.5 * level, sqrt3_2 * level\n    x2, y2 = 1 - 0.5 * level, sqrt3_2 * level\n    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.15, linewidth=grid_lw, zorder=2)\n\n    # Lines parallel to left edge (constant Silt)\n    x1, y1 = level, 0\n    x2, y2 = 0.5 + 0.5 * level, sqrt3_2 * (1 - level)\n    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.15, linewidth=grid_lw, zorder=2)\n\n    # Lines parallel to right edge (constant Sand)\n    x1, y1 = 0.5 * (1 - level), sqrt3_2 * (1 - level)\n    x2, y2 = 1 - level, 0\n    ax.plot([x1, x2], [y1, y2], color=INK, alpha=0.15, linewidth=grid_lw, zorder=2)\n\n# Add tick marks along edges (at 20% intervals)\ntick_length = 0.02\n\nfor level in [0.2, 0.4, 0.6, 0.8]:\n    # Bottom edge ticks (Silt percentage increasing left to right)\n    ax.plot([level, level], [-tick_length, 0], color=INK_SOFT, linewidth=0.75, zorder=5)\n    ax.text(level, -0.05, f\"{int(level * 100)}%\", ha=\"center\", va=\"top\", fontsize=8, color=INK_SOFT)\n\n    # Left edge ticks (Clay percentage)\n    x_tick = 0.5 * level\n    y_tick = sqrt3_2 * level\n    dx, dy = -tick_length * np.cos(np.pi / 6), -tick_length * np.sin(np.pi / 6)\n    ax.plot([x_tick, x_tick + dx], [y_tick, y_tick + dy], color=INK_SOFT, linewidth=0.75, zorder=5)\n    ax.text(\n        x_tick + dx - 0.03,\n        y_tick + dy + 0.01,\n        f\"{int(level * 100)}%\",\n        ha=\"right\",\n        va=\"center\",\n        fontsize=8,\n        color=INK_SOFT,\n    )\n\n    # Right edge ticks (Clay percentage from right side)\n    x_tick = 1 - 0.5 * level\n    y_tick = sqrt3_2 * level\n    dx, dy = tick_length * np.cos(np.pi / 6), -tick_length * np.sin(np.pi / 6)\n    ax.plot([x_tick, x_tick + dx], [y_tick, y_tick + dy], color=INK_SOFT, linewidth=0.75, zorder=5)\n    ax.text(\n        x_tick + dx + 0.03,\n        y_tick + dy + 0.01,\n        f\"{int(level * 100)}%\",\n        ha=\"left\",\n        va=\"center\",\n        fontsize=8,\n        color=INK_SOFT,\n    )\n\n# Data points — routed through seaborn's own plotting API (not raw ax.scatter)\nsns.scatterplot(\n    data=scatter_df,\n    x=\"x\",\n    y=\"y\",\n    ax=ax,\n    color=BRAND,\n    s=110,\n    alpha=0.75,\n    edgecolor=PAGE_BG,\n    linewidth=0.75,\n    zorder=10,\n    legend=False,\n)\n\n# Centroid marker — highlights the dataset's average composition for a\n# guided reading instead of a flat, unannotated point cloud.\ncentroid_x, centroid_y = scatter_df[\"x\"].mean(), scatter_df[\"y\"].mean()\nax.scatter([centroid_x], [centroid_y], marker=\"D\", s=130, facecolor=INK, edgecolor=PAGE_BG, linewidth=1.2, zorder=11)\nax.annotate(\n    \"Mean composition\",\n    xy=(centroid_x, centroid_y),\n    xytext=(0.83, 0.46),\n    fontsize=8,\n    color=INK,\n    ha=\"left\",\n    va=\"center\",\n    zorder=12,\n    arrowprops={\"arrowstyle\": \"-\", \"color\": INK_SOFT, \"linewidth\": 0.75},\n)\n\n# Vertex labels\nlabel_offset = 0.07\nax.text(0, -label_offset, \"Sand (100%)\", ha=\"center\", va=\"top\", fontsize=10, fontweight=\"bold\", color=INK)\nax.text(1, -label_offset, \"Silt (100%)\", ha=\"center\", va=\"top\", fontsize=10, fontweight=\"bold\", color=INK)\nax.text(0.5, sqrt3_2 + label_offset, \"Clay (100%)\", ha=\"center\", va=\"bottom\", fontsize=10, fontweight=\"bold\", color=INK)\n\n# Title — mandated format: {Descriptive Title} · {spec-id} · {language} · {library} · anyplot.ai\nax.set_title(\n    \"Soil Texture Classification · ternary-basic · python · seaborn · anyplot.ai\",\n    fontsize=12,\n    pad=8,\n    color=INK,\n    fontweight=\"medium\",\n)\n\n# Clean up axes — tighter top margin than the previous revision to balance\n# the title-to-apex gap against the base-to-bottom-edge gap (VQ-05).\nax.set_xlim(-0.15, 1.15)\nax.set_ylim(-0.18, 1.0)\nax.set_aspect(\"equal\")\nax.axis(\"off\")\n\nfig.subplots_adjust(left=0.04, right=0.96, top=0.90, bottom=0.06)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)  # bbox_inches MUST stay default (None) — see canvas rule\n"}