{"spec_id":"ternary-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// ternary-basic: Basic Ternary Plot\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 88/100 | Created: 2026-08-04\n//# anyplot-orientation: square\n// anyplot.ai\n// ternary-basic: Basic Ternary 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-08-04\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst SIZE = window.ANYPLOT_SIZE;\nconst TITLE = \"Bronze Alloy Composition · ternary-basic · javascript · muix · anyplot.ai\";\nconst FONT = '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif';\n\n// --- Ternary <-> Cartesian projection ---------------------------------------\n// Equilateral triangle: Copper (a) at the top apex, Tin (b) at bottom-left,\n// Zinc (c) at bottom-right. Only two degrees of freedom exist (a+b+c=1), so\n// every composition maps to a unique 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: bronze/gunmetal alloy samples (Cu-Sn-Zn, deterministic) ---------\n// Foundry bronze is copper-dominant with smaller tin and zinc fractions, so\n// the sample cloud naturally clusters toward the Copper apex while still\n// spreading across the simplex — the \"story\" comes from that real material\n// tendency, not from an added annotation.\nlet seed = 20260804 >>> 0;\nfunction rand() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\nfunction gammaShape(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 N = 65;\nconst cu = new Array(N);\nconst sn = new Array(N);\nconst zn = new Array(N);\nconst points = new Array(N);\nfor (let i = 0; i < N; i++) {\n  const gCu = gammaShape(5);\n  const gSn = gammaShape(2);\n  const gZn = gammaShape(2);\n  const total = gCu + gSn + gZn;\n  const a = gCu / total;\n  const b = gSn / total;\n  const c = gZn / total;\n  cu[i] = a * 100;\n  sn[i] = b * 100;\n  zn[i] = c * 100;\n  const { x, y } = toXY(a, b, c);\n  points[i] = { x, y, id: `alloy-${i}` };\n}\n\n// --- 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 Copper\n  GRID_SEGMENTS.push([toXY(1 - f, f, 0), toXY(0, f, 1 - f)]); // constant Tin\n  GRID_SEGMENTS.push([toXY(1 - f, 0, f), toXY(0, 1 - f, f)]); // constant Zinc\n}\n\n// --- Edge tick marks (0-100 in steps of 20) ---------------------------------\nconst TICK_LEVELS = [0, 20, 40, 60, 80, 100];\nconst LEFT_TICKS = TICK_LEVELS.map((pct) => ({ pct, pos: toXY(pct / 100, 1 - pct / 100, 0) }));\nconst BOTTOM_TICKS = TICK_LEVELS.map((pct) => ({ pct, pos: toXY(0, pct / 100, 1 - pct / 100) }));\nconst RIGHT_TICKS = TICK_LEVELS.map((pct) => ({ pct, pos: toXY(1 - pct / 100, 0, pct / 100) }));\n\n// Half-extent margins around the [0,1] x [0, sqrt3/2] triangle. Equal x/y\n// spans (1.12) keep the triangle equilateral while filling most of the\n// square canvas — just enough room above for the title + apex label and\n// below/beside for the tick and vertex labels.\nconst X_MIN = -0.06;\nconst X_MAX = 1.06;\nconst Y_MIN = -0.09;\nconst Y_MAX = 1.03;\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 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={14}\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 x={xs(VERTEX_TOP.x)} y={ys(VERTEX_TOP.y) - 24} textAnchor=\"middle\" dominantBaseline=\"baseline\">\n        Copper (%)\n      </text>\n      <text x={xs(VERTEX_LEFT.x)} y={ys(VERTEX_LEFT.y) + 32} textAnchor=\"middle\" dominantBaseline=\"hanging\">\n        Tin (%)\n      </text>\n      <text x={xs(VERTEX_RIGHT.x)} y={ys(VERTEX_RIGHT.y) + 32} textAnchor=\"middle\" dominantBaseline=\"hanging\">\n        Zinc (%)\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: 20, right: 20, bottom: 20, left: 20 }}\n      series={[\n        {\n          type: \"scatter\",\n          label: \"Alloy sample\",\n          color: \"rgba(0, 158, 115, 0.85)\",\n          markerSize: 8,\n          data: points,\n          valueFormatter: (_value, ctx) =>\n            `Cu ${cu[ctx.dataIndex].toFixed(0)}% · Sn ${sn[ctx.dataIndex].toFixed(0)}% · Zn ${zn[ctx.dataIndex].toFixed(0)}%`,\n        },\n      ]}\n      xAxis={[{ scaleType: \"linear\", min: X_MIN, max: X_MAX }]}\n      yAxis={[{ scaleType: \"linear\", min: Y_MIN, max: Y_MAX }]}\n      skipAnimation\n    >\n      <TriangleGrid />\n      <ScatterPlot />\n      <EdgeTicks />\n      <VertexLabels />\n      <Title />\n      <ChartsTooltip trigger=\"item\" />\n    </ChartContainer>\n  );\n}\n"}