{"spec_id":"ternary-density","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nternary-density: Ternary Density Plot\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 72/100 | Updated: 2026-05-19\n\"\"\"\n\nimport numpy as np\nfrom bokeh.io import export_png\nfrom bokeh.models import ColorBar, Label, LinearColorMapper\nfrom bokeh.plotting import figure\nfrom scipy.stats import gaussian_kde\n\n\n# Generate synthetic compositional data (sediment: sand/silt/clay)\nnp.random.seed(42)\nn_points = 500\n\n# Create three clusters of compositions\n# Cluster 1: Sandy sediments (high sand)\nn1 = 200\nsand1 = np.random.beta(5, 2, n1) * 80 + 10\nsilt1 = np.random.beta(2, 3, n1) * (100 - sand1) * 0.6\nclay1 = 100 - sand1 - silt1\n\n# Cluster 2: Silty sediments (high silt)\nn2 = 150\nsilt2 = np.random.beta(5, 2, n2) * 70 + 20\nsand2 = np.random.beta(2, 3, n2) * (100 - silt2) * 0.5\nclay2 = 100 - sand2 - silt2\n\n# Cluster 3: Clayey sediments (high clay)\nn3 = 150\nclay3 = np.random.beta(5, 2, n3) * 60 + 30\nsand3 = np.random.beta(2, 3, n3) * (100 - clay3) * 0.4\nsilt3 = 100 - sand3 - clay3\n\n# Combine all clusters\nsand = np.concatenate([sand1, sand2, sand3])\nsilt = np.concatenate([silt1, silt2, silt3])\nclay = np.concatenate([clay1, clay2, clay3])\n\n# Normalize to ensure they sum to 100\ntotal = sand + silt + clay\nsand = sand / total * 100\nsilt = silt / total * 100\nclay = clay / total * 100\n\n\n# Convert ternary to Cartesian coordinates\ndef ternary_to_cartesian(a, b, c):\n    \"\"\"Convert ternary coordinates (a, b, c) to Cartesian (x, y).\"\"\"\n    total = a + b + c\n    a, b, c = a / total, b / total, c / total\n    x = 0.5 * (2 * b + c)\n    y = (np.sqrt(3) / 2) * c\n    return x, y\n\n\n# Transform data points\nx_data, y_data = ternary_to_cartesian(sand, silt, clay)\n\n# Create density grid\ngrid_resolution = 200\nx_grid = np.linspace(0, 1, grid_resolution)\ny_grid = np.linspace(0, np.sqrt(3) / 2, grid_resolution)\nxx, yy = np.meshgrid(x_grid, y_grid)\n\n# Create mask for valid ternary region (inside the triangle)\n# Triangle vertices: (0, 0), (1, 0), (0.5, sqrt(3)/2)\nmask = np.zeros_like(xx, dtype=bool)\nfor i in range(grid_resolution):\n    for j in range(grid_resolution):\n        px, py = xx[i, j], yy[i, j]\n        # Check if point is inside triangle using barycentric coordinates\n        # Left edge: y <= sqrt(3) * x\n        # Right edge: y <= sqrt(3) * (1 - x)\n        # Bottom edge: y >= 0\n        if py >= 0 and py <= np.sqrt(3) * px + 1e-6 and py <= np.sqrt(3) * (1 - px) + 1e-6:\n            mask[i, j] = True\n\n# Compute kernel density estimation\npoints = np.vstack([x_data, y_data])\nkde = gaussian_kde(points, bw_method=\"scott\")\n\n# Evaluate on grid\npositions = np.vstack([xx.ravel(), yy.ravel()])\ndensity = kde(positions).reshape(xx.shape)\n\n# Apply mask (set values outside triangle to NaN)\ndensity_masked = np.where(mask, density, np.nan)\n\n# Create figure (4800 x 2700 for landscape format)\np = figure(\n    width=4800,\n    height=2700,\n    title=\"Sediment Composition · ternary-density · bokeh · pyplots.ai\",\n    x_range=(-0.15, 1.15),\n    y_range=(-0.12, 1.05),\n    tools=\"\",\n    toolbar_location=None,\n)\n\n# Remove axes and grid (we'll draw our own ternary grid)\np.xaxis.visible = False\np.yaxis.visible = False\np.xgrid.visible = False\np.ygrid.visible = False\np.outline_line_color = None\n\n# Color mapper for density\ncolor_mapper = LinearColorMapper(\n    palette=\"Viridis256\", low=np.nanmin(density_masked), high=np.nanmax(density_masked), nan_color=\"rgba(0, 0, 0, 0)\"\n)\n\n# Plot density as image\np.image(image=[density_masked], x=0, y=0, dw=1, dh=np.sqrt(3) / 2, color_mapper=color_mapper, alpha=0.85)\n\n# Triangle vertices\ntri_x = [0, 1, 0.5, 0]\ntri_y = [0, 0, np.sqrt(3) / 2, 0]\n\n# Draw triangle outline\np.line(tri_x, tri_y, line_width=4, line_color=\"#306998\", line_alpha=0.9)\n\n# Draw grid lines inside triangle\ngrid_alpha = 0.4\ngrid_width = 2\ngrid_color = \"#306998\"\n\n# Draw lines parallel to each edge (10%, 20%, ..., 90%)\nfor pct in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]:\n    # Lines parallel to bottom edge (constant clay %)\n    y_line = pct * np.sqrt(3) / 2\n    x_left = pct / 2\n    x_right = 1 - pct / 2\n    p.line([x_left, x_right], [y_line, y_line], line_width=grid_width, line_color=grid_color, line_alpha=grid_alpha)\n\n    # Lines parallel to left edge (constant silt %)\n    x_start = pct\n    y_start = 0\n    x_end = pct / 2 + 0.5\n    y_end = (1 - pct) * np.sqrt(3) / 2\n    p.line([x_start, x_end], [y_start, y_end], line_width=grid_width, line_color=grid_color, line_alpha=grid_alpha)\n\n    # Lines parallel to right edge (constant sand %)\n    x_start = 1 - pct\n    y_start = 0\n    x_end = (1 - pct) / 2\n    y_end = pct * np.sqrt(3) / 2\n    p.line([x_start, x_end], [y_start, y_end], line_width=grid_width, line_color=grid_color, line_alpha=grid_alpha)\n\n# Add vertex labels\nlabel_font_size = \"32pt\"\nlabel_offset = 0.08\n\n# Sand (bottom left)\nsand_label = Label(\n    x=0 - label_offset,\n    y=0 - label_offset / 2,\n    text=\"Sand\",\n    text_font_size=label_font_size,\n    text_color=\"#306998\",\n    text_font_style=\"bold\",\n    text_align=\"center\",\n)\np.add_layout(sand_label)\n\n# Silt (bottom right)\nsilt_label = Label(\n    x=1 + label_offset,\n    y=0 - label_offset / 2,\n    text=\"Silt\",\n    text_font_size=label_font_size,\n    text_color=\"#306998\",\n    text_font_style=\"bold\",\n    text_align=\"center\",\n)\np.add_layout(silt_label)\n\n# Clay (top)\nclay_label = Label(\n    x=0.5,\n    y=np.sqrt(3) / 2 + label_offset,\n    text=\"Clay\",\n    text_font_size=label_font_size,\n    text_color=\"#306998\",\n    text_font_style=\"bold\",\n    text_align=\"center\",\n)\np.add_layout(clay_label)\n\n# Add percentage labels along edges\ntick_font_size = \"18pt\"\n\n# Bottom edge (Sand percentage, from left to right is 100% to 0%)\nfor pct in [20, 40, 60, 80]:\n    x_pos = pct / 100\n    p.add_layout(\n        Label(\n            x=x_pos,\n            y=-0.05,\n            text=f\"{100 - pct}%\",\n            text_font_size=tick_font_size,\n            text_color=\"#666666\",\n            text_align=\"center\",\n        )\n    )\n\n# Left edge (Clay percentage, from bottom to top is 0% to 100%)\nfor pct in [20, 40, 60, 80]:\n    x_pos = pct / 100 / 2\n    y_pos = pct / 100 * np.sqrt(3) / 2\n    p.add_layout(\n        Label(\n            x=x_pos - 0.06,\n            y=y_pos,\n            text=f\"{pct}%\",\n            text_font_size=tick_font_size,\n            text_color=\"#666666\",\n            text_align=\"right\",\n        )\n    )\n\n# Right edge (Silt percentage)\nfor pct in [20, 40, 60, 80]:\n    x_pos = 1 - pct / 100 / 2\n    y_pos = pct / 100 * np.sqrt(3) / 2\n    p.add_layout(\n        Label(\n            x=x_pos + 0.06,\n            y=y_pos,\n            text=f\"{pct}%\",\n            text_font_size=tick_font_size,\n            text_color=\"#666666\",\n            text_align=\"left\",\n        )\n    )\n\n# Add color bar\ncolor_bar = ColorBar(\n    color_mapper=color_mapper,\n    width=40,\n    location=(0, 0),\n    title=\"Density\",\n    title_text_font_size=\"24pt\",\n    major_label_text_font_size=\"18pt\",\n    title_text_color=\"#306998\",\n    major_label_text_color=\"#666666\",\n    title_standoff=15,\n    margin=20,\n)\np.add_layout(color_bar, \"right\")\n\n# Title styling\np.title.text_font_size = \"36pt\"\np.title.text_color = \"#306998\"\np.title.align = \"center\"\n\n# Save\nexport_png(p, filename=\"plot.png\")\n"}