{"spec_id":"ternary-basic","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nternary-basic: Basic Ternary Plot\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 90/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\nimport sys\n\n\nsys.path.pop(0)\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    coord_fixed,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_polygon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_gradient,\n    theme,\n    theme_void,\n)\n\n\n# Theme tokens (Imprint)\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\"\nBRAND = \"#009E73\"  # Imprint position 1\nBLUE = \"#4467A3\"  # Imprint position 3 — far end of imprint_seq\n\n# Data - Soil composition samples (sand, silt, clay), USDA-style texture triangle\nnp.random.seed(42)\nn_points = 50\n\n# Dirichlet mixtures give a realistic spread across sand-, silt-, and clay-heavy soils\nraw1 = np.random.dirichlet(alpha=[5, 1, 1], size=n_points // 3) * 100  # Sand-heavy\nraw2 = np.random.dirichlet(alpha=[1, 5, 1], size=n_points // 3) * 100  # Silt-heavy\nraw3 = np.random.dirichlet(alpha=[1, 1, 5], size=n_points - 2 * (n_points // 3)) * 100  # Clay-heavy\nraw = np.vstack([raw1, raw2, raw3])\nnp.random.shuffle(raw)\nsand = raw[:, 0]\nsilt = raw[:, 1]\nclay = raw[:, 2]\n\n# Convert ternary coordinates to Cartesian (equilateral triangle, unit height)\ntotal = sand + silt + clay\nx_data = 0.5 * (2 * silt + clay) / total\ny_data = (np.sqrt(3) / 2) * clay / total\n\n# Ideal loam target (USDA loam zone center: ~42% sand, 42% silt, 16% clay) — the\n# focal point every sample is compared against, encoded as a continuous gradient\ntarget_sand, target_silt, target_clay = 42.0, 42.0, 16.0\nx_target = 0.5 * (2 * target_silt + target_clay) / 100.0\ny_target = (np.sqrt(3) / 2) * target_clay / 100.0\ndistance = np.sqrt((x_data - x_target) ** 2 + (y_data - y_target) ** 2)\n\ndf = pd.DataFrame({\"x\": x_data, \"y\": y_data, \"sand\": sand, \"silt\": silt, \"clay\": clay, \"distance\": distance})\ntarget_df = pd.DataFrame({\"x\": [x_target], \"y\": [y_target]})\ntarget_label_df = pd.DataFrame({\"x\": [x_target + 0.1], \"y\": [y_target - 0.02], \"label\": [\"Ideal loam\"]})\n\n# Triangle vertices (for the frame)\nvertices = pd.DataFrame({\"x\": [0, 1, 0.5, 0], \"y\": [0, 0, np.sqrt(3) / 2, 0]})\n\n# Grid lines at 20% intervals\ngrid_lines = []\nfor pct in [0.2, 0.4, 0.6, 0.8]:\n    # Lines parallel to bottom (constant clay)\n    x1 = 0.5 * (2 * 0 + pct)\n    y1 = (np.sqrt(3) / 2) * pct\n    x2 = 0.5 * (2 * (1 - pct) + pct)\n    y2 = (np.sqrt(3) / 2) * pct\n    grid_lines.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Lines parallel to left side (constant silt)\n    x1 = 0.5 * (2 * pct + (1 - pct))\n    y1 = (np.sqrt(3) / 2) * (1 - pct)\n    x2 = 0.5 * (2 * pct + 0)\n    y2 = 0\n    grid_lines.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Lines parallel to right side (constant sand)\n    x1 = 0.5 * (2 * 0 + (1 - pct))\n    y1 = (np.sqrt(3) / 2) * (1 - pct)\n    x2 = 0.5 * (2 * (1 - pct) + 0)\n    y2 = 0\n    grid_lines.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\ngrid_df = pd.DataFrame(grid_lines)\n\n# Tick labels along edges\ntick_labels = []\nlabel_offset = 0.045\nfor pct in [0, 20, 40, 60, 80, 100]:\n    frac = pct / 100\n    # Sand axis (left edge going up)\n    x = 0.5 * (2 * 0 + frac)\n    y = (np.sqrt(3) / 2) * frac\n    tick_labels.append({\"x\": x - label_offset, \"y\": y, \"label\": str(pct)})\n\n    # Silt axis (bottom edge)\n    x = 0.5 * (2 * frac + 0)\n    y = 0\n    tick_labels.append({\"x\": x, \"y\": y - label_offset * 0.8, \"label\": str(pct)})\n\n    # Clay axis (right edge going up)\n    x = 0.5 * (2 * (1 - frac) + frac)\n    y = (np.sqrt(3) / 2) * frac\n    tick_labels.append({\"x\": x + label_offset, \"y\": y, \"label\": str(pct)})\n\ntick_df = pd.DataFrame(tick_labels)\n\n# Vertex labels\nvertex_labels = pd.DataFrame(\n    {\n        \"x\": [0 - 0.02, 1 + 0.02, 0.5],\n        \"y\": [0 - 0.07, 0 - 0.07, np.sqrt(3) / 2 + 0.05],\n        \"label\": [\"Sand (%)\", \"Silt (%)\", \"Clay (%)\"],\n    }\n)\n\n# Build the plot\nplot = (\n    ggplot()\n    # Triangle frame\n    + geom_polygon(data=vertices, mapping=aes(x=\"x\", y=\"y\"), fill=PAGE_BG, color=BRAND, size=1.4)\n    # Grid lines\n    + geom_segment(data=grid_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK, size=0.4, alpha=0.15)\n    # Data points, colored by distance to the ideal-loam target — brand green (close)\n    # to blue (far), the imprint_seq sequential colormap. alpha=0.7 keeps overlapping\n    # samples in the vertex-heavy Dirichlet clusters distinguishable.\n    + geom_point(data=df, mapping=aes(x=\"x\", y=\"y\", color=\"distance\"), size=3, alpha=0.7)\n    + scale_color_gradient(low=BRAND, high=BLUE, name=\"Distance to\\nideal loam\")\n    # Target marker — theme-neutral reference point, not a data series\n    + geom_point(data=target_df, mapping=aes(x=\"x\", y=\"y\"), color=INK, size=4.5, shape=\"D\", stroke=1.2)\n    + geom_text(\n        data=target_label_df,\n        mapping=aes(x=\"x\", y=\"y\", label=\"label\"),\n        size=3.6,\n        color=INK,\n        fontweight=\"bold\",\n        ha=\"left\",\n    )\n    # Tick labels\n    + geom_text(data=tick_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=3, color=INK_SOFT)\n    # Vertex labels\n    + geom_text(data=vertex_labels, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=4.2, fontweight=\"bold\", color=INK)\n    # Title and theme\n    + labs(title=\"ternary-basic · plotnine · anyplot.ai\")\n    + coord_fixed(ratio=1)\n    + theme_void()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        plot_title=element_text(size=13, ha=\"center\", color=INK, weight=\"medium\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_text=element_text(size=7, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        # Inset legend (NPC coords within the panel) instead of a separate right-side\n        # column — coord_fixed already leaves whitespace beside the triangle since the\n        # panel is wider than the triangle's aspect ratio; anchoring the legend high\n        # and to the right (where the clay vertex tapers away) keeps it close to the\n        # plot, clear of the triangle frame, instead of stranded past an empty margin.\n        legend_position=(0.86, 0.86),\n        legend_direction=\"vertical\",\n        legend_key_size=14,\n        plot_margin=0.02,\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}