{"spec_id":"ternary-density","library":"muix","language":"javascript","code":"// anyplot.ai\n// ternary-density: Ternary Density Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\n//# anyplot-orientation: square\n// anyplot.ai\n// ternary-density: Ternary Density Plot\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\nconst TITLE =\n  \"Soil Texture Density · ternary-density · javascript · muix · anyplot.ai\";\nconst FONT =\n  '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n// --- Ternary <-> Cartesian projection ---------------------------------------\n// Equilateral triangle: Sand (a) at the top apex, Silt (b) at bottom-left,\n// Clay (c) at bottom-right — the classic USDA soil-texture layout. Only two\n// degrees of freedom exist (a+b+c=1), so every composition maps to a unique\n// point inside (or on) the triangle.\nconst SQRT3_2 = Math.sqrt(3) / 2;\nfunction toXY(a, b, c) {\n  const total = a + b + c;\n  const aN = a / total;\n  const cN = c / total;\n  return { x: cN + aN / 2, y: SQRT3_2 * aN };\n}\nconst VERTEX_TOP = toXY(1, 0, 0);\nconst VERTEX_LEFT = toXY(0, 1, 0);\nconst VERTEX_RIGHT = toXY(0, 0, 1);\n\n// --- Data: sand/silt/clay soil samples (deterministic, Dirichlet-like) -----\n// Three characteristic soil-texture clouds — sandy, loamy, and clay-rich —\n// each a Dirichlet-distributed cluster around a representative composition.\n// The clusters overlap in the middle of the simplex, giving the KDE surface\n// below a realistic multi-modal shape instead of one tidy blob.\nlet seed = 20260902 >>> 0;\nfunction rand() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction erlang(k) {\n  let s = 0;\n  for (let i = 0; i < k; i++) s += -Math.log(rand() || 1e-9);\n  return s;\n}\n\nconst CLUSTERS = [\n  { n: 190, shape: [19, 4, 3] }, // sandy soils — sand-dominant, tight cluster\n  { n: 220, shape: [8, 8, 4] }, // loamy soils — the most common texture class\n  { n: 170, shape: [4, 5, 11] }, // clay-rich soils — clay-dominant\n];\n\nconst samplePoints = [];\nfor (const cluster of CLUSTERS) {\n  for (let i = 0; i < cluster.n; i++) {\n    const gSand = erlang(cluster.shape[0]);\n    const gSilt = erlang(cluster.shape[1]);\n    const gClay = erlang(cluster.shape[2]);\n    const total = gSand + gSilt + gClay;\n    samplePoints.push(toXY(gSand / total, gSilt / total, gClay / total));\n  }\n}\n\n// --- Kernel density estimate over a triangular lattice ---------------------\n// A Gaussian KDE evaluated on every lattice point of a barycentric grid\n// (i + j + k = GRID_N) — the natural equal-spacing grid for a simplex, and\n// what the scatter markers below render as a continuous-looking heatmap.\nconst GRID_N = 46;\nconst BANDWIDTH = 0.055;\nconst TWO_H2 = 2 * BANDWIDTH * BANDWIDTH;\n\nconst densityPoints = [];\nconst sandPct = [];\nconst siltPct = [];\nconst clayPct = [];\nlet maxDensity = 0;\nfor (let i = 0; i <= GRID_N; i++) {\n  for (let j = 0; j <= GRID_N - i; j++) {\n    const k = GRID_N - i - j;\n    const { x, y } = toXY(i, j, k);\n    let density = 0;\n    for (let s = 0; s < samplePoints.length; s++) {\n      const dx = x - samplePoints[s].x;\n      const dy = y - samplePoints[s].y;\n      density += Math.exp(-(dx * dx + dy * dy) / TWO_H2);\n    }\n    density /= samplePoints.length;\n    if (density > maxDensity) maxDensity = density;\n    densityPoints.push({ x, y, z: density, rawZ: density, id: `cell-${i}-${j}` });\n    sandPct.push(Math.round((i / GRID_N) * 100));\n    siltPct.push(Math.round((j / GRID_N) * 100));\n    clayPct.push(Math.round((k / GRID_N) * 100));\n  }\n}\n\n// Steepen the density -> color mapping with a gamma > 1: this pushes the\n// broad low-density field further toward the green end of imprint_seq while\n// leaving true peaks near the blue end, so the three clusters read as\n// distinct hotspots instead of one uniform green wash. `rawZ` (linear,\n// untouched) still drives the opacity mask below and the tooltip percentage.\nconst COLOR_GAMMA = 1.7;\nfor (const p of densityPoints) {\n  p.z = Math.pow(p.rawZ / maxDensity, COLOR_GAMMA) * maxDensity;\n}\n\n// --- Reference grid: lines of constant composition, parallel to each edge --\nconst GRID_LEVELS = [0.2, 0.4, 0.6, 0.8];\nconst GRID_SEGMENTS = [];\nfor (const f of GRID_LEVELS) {\n  GRID_SEGMENTS.push([toXY(f, 1 - f, 0), toXY(f, 0, 1 - f)]); // constant Sand\n  GRID_SEGMENTS.push([toXY(1 - f, f, 0), toXY(0, f, 1 - f)]); // constant Silt\n  GRID_SEGMENTS.push([toXY(1 - f, 0, f), toXY(0, 1 - f, f)]); // constant Clay\n}\nconst TICK_LEVELS = [20, 40, 60, 80];\nconst LEFT_TICKS = TICK_LEVELS.map((pct) => ({\n  pct,\n  pos: toXY(pct / 100, 1 - pct / 100, 0),\n}));\nconst BOTTOM_TICKS = TICK_LEVELS.map((pct) => ({\n  pct,\n  pos: toXY(0, pct / 100, 1 - pct / 100),\n}));\nconst RIGHT_TICKS = TICK_LEVELS.map((pct) => ({\n  pct,\n  pos: toXY(1 - pct / 100, 0, pct / 100),\n}));\n\n// Domain padding around the [0,1] x [0, sqrt3/2] triangle. Equal x/y spans\n// (1.216) keep the triangle equilateral on the square canvas — extra room\n// above for the title, and generously below for vertex labels + the density\n// legend, which both live outside the triangle itself.\nconst X_MIN = -0.108;\nconst X_MAX = 1.108;\nconst Y_MIN = -0.26;\nconst Y_MAX = 0.956;\n\nfunction TriangleGrid() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g>\n      {GRID_SEGMENTS.map((seg, i) => (\n        <line\n          key={i}\n          x1={xs(seg[0].x)}\n          y1={ys(seg[0].y)}\n          x2={xs(seg[1].x)}\n          y2={ys(seg[1].y)}\n          stroke={t.grid}\n          strokeWidth={1.5}\n          strokeDasharray=\"6 5\"\n        />\n      ))}\n      <path\n        d={`M${xs(VERTEX_TOP.x)} ${ys(VERTEX_TOP.y)} L${xs(VERTEX_LEFT.x)} ${ys(VERTEX_LEFT.y)} L${xs(VERTEX_RIGHT.x)} ${ys(VERTEX_RIGHT.y)} Z`}\n        fill=\"none\"\n        stroke={t.inkSoft}\n        strokeWidth={2.5}\n        strokeLinejoin=\"round\"\n      />\n    </g>\n  );\n}\n\nfunction DensityLayer() {\n  const xs = useXScale();\n  const ys = useYScale();\n  const clipD = `M${xs(VERTEX_TOP.x)} ${ys(VERTEX_TOP.y)} L${xs(VERTEX_LEFT.x)} ${ys(VERTEX_LEFT.y)} L${xs(VERTEX_RIGHT.x)} ${ys(VERTEX_RIGHT.y)} Z`;\n  return (\n    <>\n      <defs>\n        <clipPath id=\"ternaryTriangleClip\">\n          <path d={clipD} />\n        </clipPath>\n      </defs>\n      <g clipPath=\"url(#ternaryTriangleClip)\" opacity={0.85}>\n        <ScatterPlot />\n      </g>\n    </>\n  );\n}\n\n// A second, higher-contrast pass of the reference grid drawn ON TOP of the\n// density layer: a page-background-colored halo \"cuts through\" the density\n// fill, then the actual dashed grid stroke reads over it — guaranteed\n// visible everywhere (including the high-density peaks that fully occlude\n// the beneath-the-density copy in TriangleGrid) in both themes.\nfunction GridOverlay() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g>\n      {GRID_SEGMENTS.map((seg, i) => (\n        <line\n          key={`halo-${i}`}\n          x1={xs(seg[0].x)}\n          y1={ys(seg[0].y)}\n          x2={xs(seg[1].x)}\n          y2={ys(seg[1].y)}\n          stroke={t.pageBg}\n          strokeWidth={4}\n          strokeOpacity={0.6}\n          strokeDasharray=\"6 5\"\n        />\n      ))}\n      {GRID_SEGMENTS.map((seg, i) => (\n        <line\n          key={`line-${i}`}\n          x1={xs(seg[0].x)}\n          y1={ys(seg[0].y)}\n          x2={xs(seg[1].x)}\n          y2={ys(seg[1].y)}\n          stroke={t.grid}\n          strokeWidth={1.5}\n          strokeDasharray=\"6 5\"\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction EdgeTicks() {\n  const xs = useXScale();\n  const ys = useYScale();\n  const label = (tick, dx, dy, anchor) => (\n    <text\n      key={`${dx}-${dy}-${tick.pct}`}\n      x={xs(tick.pos.x) + dx}\n      y={ys(tick.pos.y) + dy}\n      textAnchor={anchor}\n      dominantBaseline=\"middle\"\n      fontFamily={FONT}\n      fontSize={13}\n      fill={t.inkSoft}\n    >\n      {tick.pct}%\n    </text>\n  );\n  return (\n    <g>\n      {LEFT_TICKS.map((tk) => label(tk, -16, 0, \"end\"))}\n      {BOTTOM_TICKS.map((tk) => label(tk, 0, 22, \"middle\"))}\n      {RIGHT_TICKS.map((tk) => label(tk, 16, 0, \"start\"))}\n    </g>\n  );\n}\n\nfunction VertexLabels() {\n  const xs = useXScale();\n  const ys = useYScale();\n  return (\n    <g fontFamily={FONT} fontSize={22} fontWeight={700} fill={t.ink}>\n      <text\n        x={xs(VERTEX_TOP.x)}\n        y={ys(VERTEX_TOP.y) - 22}\n        textAnchor=\"middle\"\n        dominantBaseline=\"baseline\"\n      >\n        Sand\n      </text>\n      <text\n        x={xs(VERTEX_LEFT.x)}\n        y={ys(VERTEX_LEFT.y) + 30}\n        textAnchor=\"middle\"\n        dominantBaseline=\"hanging\"\n      >\n        Silt\n      </text>\n      <text\n        x={xs(VERTEX_RIGHT.x)}\n        y={ys(VERTEX_RIGHT.y) + 30}\n        textAnchor=\"middle\"\n        dominantBaseline=\"hanging\"\n      >\n        Clay\n      </text>\n    </g>\n  );\n}\n\nfunction DensityLegend() {\n  const { left, top, width, height } = useDrawingArea();\n  const barWidth = Math.min(380, width * 0.36);\n  const barHeight = 20;\n  const cx = left + width / 2;\n  const barY = top + height - 92;\n  const barX = cx - barWidth / 2;\n  return (\n    <g fontFamily={FONT}>\n      <defs>\n        <linearGradient id=\"ternaryDensityGradient\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n          <stop offset=\"0%\" stopColor={t.seq[0]} />\n          <stop offset=\"100%\" stopColor={t.seq[1]} />\n        </linearGradient>\n      </defs>\n      <text\n        x={cx}\n        y={barY - 14}\n        textAnchor=\"middle\"\n        fontSize={15}\n        fill={t.inkSoft}\n      >\n        Relative sample density\n      </text>\n      <rect\n        x={barX}\n        y={barY}\n        width={barWidth}\n        height={barHeight}\n        rx={4}\n        fill=\"url(#ternaryDensityGradient)\"\n        stroke={t.ink}\n        strokeOpacity={0.15}\n      />\n      <text\n        x={barX}\n        y={barY + barHeight + 20}\n        textAnchor=\"start\"\n        fontSize={13}\n        fill={t.inkSoft}\n      >\n        Low\n      </text>\n      <text\n        x={barX + barWidth}\n        y={barY + barHeight + 20}\n        textAnchor=\"end\"\n        fontSize={13}\n        fill={t.inkSoft}\n      >\n        High\n      </text>\n    </g>\n  );\n}\n\nfunction Title() {\n  const xs = useXScale();\n  const ys = useYScale();\n  const n = TITLE.length;\n  const ratio = n > 67 ? 67 / n : 1.0;\n  const fontSize = Math.max(15, Math.round(22 * ratio));\n  return (\n    <text\n      x={xs((X_MIN + X_MAX) / 2)}\n      y={ys(Y_MAX) + fontSize}\n      textAnchor=\"middle\"\n      dominantBaseline=\"hanging\"\n      fontFamily={FONT}\n      fontSize={fontSize}\n      fontWeight={600}\n      fill={t.ink}\n    >\n      {TITLE}\n    </text>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={SIZE.width}\n      height={SIZE.height}\n      margin={{ top: 24, right: 24, bottom: 24, left: 24 }}\n      series={[\n        {\n          type: \"scatter\",\n          label: \"Sample density\",\n          color: t.seq[0],\n          markerSize: 13,\n          data: densityPoints,\n          valueFormatter: (_value, ctx) =>\n            `Sand ${sandPct[ctx.dataIndex]}% · Silt ${siltPct[ctx.dataIndex]}% · Clay ${clayPct[ctx.dataIndex]}% — ${Math.round((densityPoints[ctx.dataIndex].rawZ / maxDensity) * 100)}% of peak density`,\n        },\n      ]}\n      xAxis={[{ scaleType: \"linear\", min: X_MIN, max: X_MAX }]}\n      yAxis={[{ scaleType: \"linear\", min: Y_MIN, max: Y_MAX }]}\n      zAxis={[\n        {\n          id: \"density\",\n          min: 0,\n          max: maxDensity,\n          colorMap: { type: \"continuous\", color: [t.seq[0], t.seq[1]] },\n        },\n      ]}\n      skipAnimation\n    >\n      <TriangleGrid />\n      <DensityLayer />\n      <GridOverlay />\n      <EdgeTicks />\n      <VertexLabels />\n      <DensityLegend />\n      <Title />\n      <ChartsTooltip trigger=\"item\" />\n    </ChartContainer>\n  );\n}\n"}