{"spec_id":"ternary-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nternary-basic: Basic Ternary Plot\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-08-04\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_polygon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    theme,\n)\n\n\nLetsPlot.setup_html()\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\"\n\n# Imprint palette (categorical, positions 1-3)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\"]\n\n# Data: Soil composition samples (Sand, Silt, Clay)\nnp.random.seed(42)\n\n# Generate realistic soil composition data in different soil type regions\nsamples = []\nsoil_types = []\n\n# Sandy soils (high sand)\nfor _ in range(20):\n    sand = np.random.uniform(60, 90)\n    remaining = 100 - sand\n    silt = np.random.uniform(0, remaining)\n    clay = remaining - silt\n    samples.append((sand, silt, clay))\n    soil_types.append(\"Sandy\")\n\n# Silty soils (high silt)\nfor _ in range(20):\n    silt = np.random.uniform(50, 80)\n    remaining = 100 - silt\n    sand = np.random.uniform(0, remaining)\n    clay = remaining - sand\n    samples.append((sand, silt, clay))\n    soil_types.append(\"Silty\")\n\n# Clay soils (high clay)\nfor _ in range(20):\n    clay = np.random.uniform(40, 70)\n    remaining = 100 - clay\n    sand = np.random.uniform(0, remaining)\n    silt = remaining - sand\n    samples.append((sand, silt, clay))\n    soil_types.append(\"Clayey\")\n\n# Convert ternary to Cartesian coordinates\n# Formula: x = 0.5 * (2*b + c) / (a+b+c), y = sqrt(3)/2 * c / (a+b+c)\n# where a=Sand (bottom-left), b=Silt (bottom-right), c=Clay (top)\nsqrt3_2 = np.sqrt(3) / 2\nx_coords = []\ny_coords = []\nfor sand, silt, clay in samples:\n    total = sand + silt + clay\n    silt_norm = silt / total\n    clay_norm = clay / total\n    x_coords.append(0.5 * (2 * silt_norm + clay_norm))\n    y_coords.append(sqrt3_2 * clay_norm)\n\ndf = pd.DataFrame(\n    {\n        \"x\": x_coords,\n        \"y\": y_coords,\n        \"soil_type\": soil_types,\n        \"sand\": [round(s, 1) for s, _, _ in samples],\n        \"silt\": [round(s, 1) for _, s, _ in samples],\n        \"clay\": [round(c, 1) for _, _, c in samples],\n    }\n)\n\n# Triangle vertices (Sand at bottom-left, Silt at bottom-right, Clay at top)\nvertices = pd.DataFrame({\"x\": [0, 1, 0.5, 0], \"y\": [0, 0, sqrt3_2, 0]})\n\n# Grid lines at 20% intervals\ngrid_segments = []\nfor pct in [0.2, 0.4, 0.6, 0.8]:\n    # Lines parallel to Sand-Silt edge (constant Clay)\n    x1 = 0.5 * (0 + pct)\n    y1 = sqrt3_2 * pct\n    x2 = 0.5 * (2 * (1 - pct) + pct)\n    y2 = sqrt3_2 * pct\n    grid_segments.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Lines parallel to Sand-Clay edge (constant Silt)\n    x1 = 0.5 * (2 * pct + 0)\n    y1 = 0\n    x2 = 0.5 * (2 * pct + (1 - pct))\n    y2 = sqrt3_2 * (1 - pct)\n    grid_segments.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\n    # Lines parallel to Silt-Clay edge (constant Sand)\n    x1 = 0.5 * (2 * (1 - pct) + 0)\n    y1 = 0\n    x2 = 0.5 * (0 + (1 - pct))\n    y2 = sqrt3_2 * (1 - pct)\n    grid_segments.append({\"x\": x1, \"y\": y1, \"xend\": x2, \"yend\": y2})\n\ngrid_df = pd.DataFrame(grid_segments)\n\n# Vertex labels with offset for readability\nlabel_offset = 0.06\nlabels_df = pd.DataFrame(\n    {\n        \"x\": [0 + label_offset, 1 - label_offset, 0.5],\n        \"y\": [-label_offset - 0.02, -label_offset - 0.02, sqrt3_2 + label_offset],\n        \"label\": [\"Sand (%)\", \"Silt (%)\", \"Clay (%)\"],\n    }\n)\n\n# Tick labels along edges (at 20%, 40%, 60%, 80%)\ntick_labels = []\nfor pct in [20, 40, 60, 80]:\n    frac = pct / 100\n    # Along left edge\n    x = 0.5 * frac\n    y = sqrt3_2 * frac\n    tick_labels.append({\"x\": x - 0.04, \"y\": y + 0.02, \"label\": str(pct)})\n\n    # Along right edge\n    x = 0.5 * (2 * (1 - frac) + frac)\n    y = sqrt3_2 * frac\n    tick_labels.append({\"x\": x + 0.04, \"y\": y + 0.02, \"label\": str(pct)})\n\n    # Along bottom edge\n    x = 0.5 * (2 * frac)\n    y = 0\n    tick_labels.append({\"x\": x, \"y\": y - 0.04, \"label\": str(pct)})\n\ntick_df = pd.DataFrame(tick_labels)\n\n# Cluster centroids nudged toward the triangle center, for a storytelling\n# callout per region beyond plain color-coded scatter.\ntri_center_x, tri_center_y = 0.5, sqrt3_2 / 3\ncentroid_df = df.groupby(\"soil_type\", as_index=False)[[\"x\", \"y\"]].mean()\ncentroid_df[\"label_x\"] = centroid_df[\"x\"] + 0.14 * (tri_center_x - centroid_df[\"x\"])\ncentroid_df[\"label_y\"] = centroid_df[\"y\"] + 0.14 * (tri_center_y - centroid_df[\"y\"]) + 0.075\n\n# Distinctive letsplot feature: interactive per-point tooltips carrying the\n# exact composition (not just position) — surfaced in the exported HTML.\npoint_tooltips = (\n    layer_tooltips().line(\"Soil type|@soil_type\").line(\"Sand (%)|@sand\").line(\"Silt (%)|@silt\").line(\"Clay (%)|@clay\")\n)\n\n# Plot\nplot = (\n    ggplot()\n    # Triangle outline\n    + geom_polygon(data=vertices, mapping=aes(x=\"x\", y=\"y\"), fill=PAGE_BG, color=INK_SOFT, size=1.2, alpha=1)\n    # Grid lines\n    + geom_segment(\n        data=grid_df, mapping=aes(x=\"x\", y=\"y\", xend=\"xend\", yend=\"yend\"), color=INK_SOFT, size=0.4, alpha=0.3\n    )\n    # Data points\n    + geom_point(data=df, mapping=aes(x=\"x\", y=\"y\", color=\"soil_type\"), size=4.2, alpha=0.85, tooltips=point_tooltips)\n    # Cluster centroid markers (ring) + labels, calling out each soil region\n    + geom_point(\n        data=centroid_df,\n        mapping=aes(x=\"x\", y=\"y\", color=\"soil_type\"),\n        size=9,\n        shape=21,\n        fill=\"white\",\n        stroke=2.2,\n        show_legend=False,\n    )\n    + geom_text(\n        data=centroid_df, mapping=aes(x=\"label_x\", y=\"label_y\", label=\"soil_type\"), size=3.8, fontface=\"bold\", color=INK\n    )\n    # Vertex labels\n    + geom_text(data=labels_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=5.5, fontface=\"bold\", color=INK)\n    # Tick labels\n    + geom_text(data=tick_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), size=3.2, color=INK_SOFT)\n    # Color scale using Imprint palette\n    + scale_color_manual(values=IMPRINT)\n    # Labels and title\n    + labs(title=\"ternary-basic · python · letsplot · anyplot.ai\", color=\"Soil Type\")\n    # Theme\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        plot_title=element_text(size=16, face=\"bold\", color=INK),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK, face=\"bold\"),\n        legend_text=element_text(size=10, color=INK_SOFT),\n        axis_title=element_blank(),\n        axis_text=element_blank(),\n        axis_ticks=element_blank(),\n        axis_line=element_blank(),\n        panel_grid=element_blank(),\n        legend_position=\"right\",\n    )\n    + ggsize(800, 450)\n)\n\n# Save as PNG (scale 4x to get 3200 x 1800 px) and HTML\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}