{"spec_id":"ternary-density","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nternary-density: Ternary Density Plot\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-05-19\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\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\"\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# Data — clustered sediment compositions (sand/silt/clay)\nnp.random.seed(42)\nn_per_cluster = 400\n\n# Cluster 1: Sandy sediment (high sand, low clay)\nc1_sand = np.random.beta(12, 2, n_per_cluster) * 80 + 15\nc1_clay = np.random.beta(2, 8, n_per_cluster) * (100 - c1_sand) * 0.3\nc1_silt = 100 - c1_sand - c1_clay\n\n# Cluster 2: Silty sediment (high silt)\nc2_silt = np.random.beta(10, 2, n_per_cluster) * 70 + 25\nc2_sand = np.random.beta(3, 6, n_per_cluster) * (100 - c2_silt) * 0.7\nc2_clay = 100 - c2_silt - c2_sand\n\n# Cluster 3: Clay-rich sediment (high clay)\nc3_clay = np.random.beta(8, 3, n_per_cluster) * 60 + 30\nc3_sand = np.random.beta(2, 5, n_per_cluster) * (100 - c3_clay) * 0.5\nc3_silt = 100 - c3_clay - c3_sand\n\n# Combine and normalize to exact 100%\nsand = np.concatenate([c1_sand, c2_sand, c3_sand])\nsilt = np.concatenate([c1_silt, c2_silt, c3_silt])\nclay = np.concatenate([c1_clay, c2_clay, c3_clay])\nsand = np.clip(sand, 0.1, 99.8)\nsilt = np.clip(silt, 0.1, 99.8)\nclay = np.clip(clay, 0.1, 99.8)\ntotal = sand + silt + clay\nsand, silt, clay = sand / total * 100, silt / total * 100, clay / total * 100\n\n# Ternary → Cartesian: Sand=(0,0), Silt=(1,0), Clay=(0.5, √3/2)\nsqrt3_2 = np.sqrt(3) / 2\nx_pts = 0.5 * (2 * silt + clay) / 100\ny_pts = sqrt3_2 * clay / 100\n\n# KDE on the transformed coordinates\ngrid_res = 100\nx_grid = np.linspace(0, 1, grid_res)\ny_grid = np.linspace(0, sqrt3_2, grid_res)\nxx, yy = np.meshgrid(x_grid, y_grid)\nkde = gaussian_kde(np.vstack([x_pts, y_pts]), bw_method=\"scott\")\ndensity = kde(np.vstack([xx.ravel(), yy.ravel()])).reshape(xx.shape)\n\n# Triangle mask (half-plane method)\nmargin = 0.005\ninside = (yy >= -margin) & (np.sqrt(3) * xx + yy <= np.sqrt(3) + margin) & (yy - np.sqrt(3) * xx <= margin)\n\n# Cell half-widths for mark_rect (pixel-perfect coverage)\ndx = (x_grid[1] - x_grid[0]) / 2\ndy = (y_grid[1] - y_grid[0]) / 2\n\n# Density DataFrame — explicit cell bounds for clean edges\ndensity_rows = [\n    {\"x1\": xx[i, j] - dx, \"x2\": xx[i, j] + dx, \"y1\": yy[i, j] - dy, \"y2\": yy[i, j] + dy, \"density\": density[i, j]}\n    for i in range(grid_res)\n    for j in range(grid_res)\n    if inside[i, j]\n]\ndensity_df = pd.DataFrame(density_rows)\n\n# Triangle outline vertices\ntriangle_df = pd.DataFrame({\"x\": [0, 1, 0.5, 0], \"y\": [0, 0, sqrt3_2, 0], \"order\": [0, 1, 2, 3]})\n\n# Ternary grid lines (10% intervals for all three axes)\ngrid_lines = []\nfor i in range(1, 10):\n    frac = i / 10\n    # Constant clay (horizontal)\n    y_val = frac * sqrt3_2\n    x_left = y_val / np.sqrt(3)\n    x_right = 1 - y_val / np.sqrt(3)\n    grid_lines += [\n        {\"x\": x_left, \"y\": y_val, \"line\": f\"h{i}\", \"o\": 0},\n        {\"x\": x_right, \"y\": y_val, \"line\": f\"h{i}\", \"o\": 1},\n    ]\n    # Constant sand\n    grid_lines += [\n        {\"x\": frac, \"y\": 0, \"line\": f\"s{i}\", \"o\": 0},\n        {\"x\": frac / 2, \"y\": frac * sqrt3_2, \"line\": f\"s{i}\", \"o\": 1},\n    ]\n    # Constant silt\n    grid_lines += [\n        {\"x\": 1 - frac, \"y\": 0, \"line\": f\"t{i}\", \"o\": 0},\n        {\"x\": 1 - frac / 2, \"y\": frac * sqrt3_2, \"line\": f\"t{i}\", \"o\": 1},\n    ]\ngrid_df = pd.DataFrame(grid_lines)\n\n# Vertex labels\nlabels_df = pd.DataFrame(\n    {\"x\": [-0.02, 1.02, 0.5], \"y\": [-0.05, -0.05, sqrt3_2 + 0.05], \"label\": [\"Sand (%)\", \"Silt (%)\", \"Clay (%)\"]}\n)\n\n# Shared scale domains for all layers\nX_DOMAIN = [-0.12, 1.12]\nY_DOMAIN = [-0.12, sqrt3_2 + 0.12]\n\n# Density heatmap — mark_rect with explicit cell bounds\nheatmap = (\n    alt.Chart(density_df)\n    .mark_rect(opacity=0.92)\n    .encode(\n        x=alt.X(\"x1:Q\", scale=alt.Scale(domain=X_DOMAIN), axis=None),\n        x2=alt.X2(\"x2:Q\"),\n        y=alt.Y(\"y1:Q\", scale=alt.Scale(domain=Y_DOMAIN), axis=None),\n        y2=alt.Y2(\"y2:Q\"),\n        color=alt.Color(\n            \"density:Q\",\n            scale=alt.Scale(scheme=\"viridis\"),\n            legend=alt.Legend(title=\"Density\", titleFontSize=20, labelFontSize=16, orient=\"right\"),\n        ),\n    )\n)\n\n# Triangle outline\ntriangle = (\n    alt.Chart(triangle_df)\n    .mark_line(color=INK, strokeWidth=3)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=X_DOMAIN), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=Y_DOMAIN), axis=None),\n        order=\"order:O\",\n    )\n)\n\n# Grid lines\ngrid = (\n    alt.Chart(grid_df)\n    .mark_line(color=INK_SOFT, strokeWidth=1, opacity=0.35)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=X_DOMAIN), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=Y_DOMAIN), axis=None),\n        detail=\"line:N\",\n        order=\"o:O\",\n    )\n)\n\n# Vertex labels\nlabels = (\n    alt.Chart(labels_df)\n    .mark_text(fontSize=24, fontWeight=\"bold\", color=INK)\n    .encode(\n        x=alt.X(\"x:Q\", scale=alt.Scale(domain=X_DOMAIN), axis=None),\n        y=alt.Y(\"y:Q\", scale=alt.Scale(domain=Y_DOMAIN), axis=None),\n        text=\"label:N\",\n    )\n)\n\n# Compose layers\nchart = (\n    alt.layer(grid, heatmap, triangle, labels)\n    .properties(\n        background=PAGE_BG,\n        width=1600,\n        height=900,\n        title=alt.Title(\n            \"Sediment Composition · ternary-density · python · altair · anyplot.ai\", fontSize=28, color=INK\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save\nchart.save(f\"plot-{THEME}.png\", scale_factor=3.0)\nchart.save(f\"plot-{THEME}.html\")\n"}