{"spec_id":"ternary-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// ternary-basic: Basic Ternary Plot\n// Library: echarts 6.1.0 | JavaScript 22.23.1\n// Quality: 88/100 | Created: 2026-08-04\n\n//# anyplot-orientation: square\n\n// ECharts has no built-in ternary coordinate system, so the triangle, grid\n// lines, and tick/vertex labels are laid out here as plain pixel geometry\n// (barycentric -> Cartesian) and drawn with the `graphic` component. Data\n// points still go through a real (coordinate-less) `custom` series so\n// tooltip/hover stay genuinely interactive in the emitted HTML.\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data: bronze alloy samples (Copper / Zinc / Tin, % by mass) -----------\nfunction makeRng(seed) {\n  let s = seed >>> 0;\n  return function () {\n    s = (s * 1664525 + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\nconst rng = makeRng(42);\nfunction gaussian() {\n  const u1 = Math.max(rng(), 1e-6);\n  const u2 = rng();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Realistic 88/2/10 Cu-Zn-Sn bronze (copper-dominant, trace zinc, tin secondary)\nconst centerCopper = 88;\nconst centerZinc = 2;\nconst centerTin = 10;\nconst samples = [];\nfor (let i = 0; i < 70; i++) {\n  let copper = Math.max(centerCopper + gaussian() * 3, 70);\n  let zinc = Math.max(centerZinc + gaussian() * 1, 0.2);\n  let tin = Math.max(centerTin + gaussian() * 2.5, 0.5);\n  const total = copper + zinc + tin;\n  samples.push([(copper / total) * 100, (zinc / total) * 100, (tin / total) * 100]);\n}\n\n// Density cue: points near the batch mean get lower opacity (reduces overlap\n// in the dense core), points further out read more opaque (highlights outliers).\nconst meanA = samples.reduce((s, p) => s + p[0], 0) / samples.length;\nconst meanB = samples.reduce((s, p) => s + p[1], 0) / samples.length;\nconst meanC = samples.reduce((s, p) => s + p[2], 0) / samples.length;\nconst sampleDists = samples.map((p) => Math.hypot(p[0] - meanA, p[1] - meanB, p[2] - meanC));\nconst maxDist = Math.max(...sampleDists) || 1;\n\n// --- Triangle geometry (pixel space, apex-up) -------------------------------\nconst marginTop = 170;\nconst marginBottom = 110;\nconst marginSide = 130;\nconst availW = size.width - 2 * marginSide;\nconst availH = size.height - marginTop - marginBottom;\nconst sideLen = Math.min(availW, availH / (Math.sqrt(3) / 2));\nconst triHeight = sideLen * (Math.sqrt(3) / 2);\nconst top = marginTop + (availH - triHeight) / 2;\nconst bottom = top + triHeight;\nconst cx = size.width / 2;\nconst left = cx - sideLen / 2;\nconst right = cx + sideLen / 2;\n\nconst APEX = { x: cx, y: top }; // component_c = 100\nconst BL = { x: left, y: bottom }; // component_a = 100\nconst BR = { x: right, y: bottom }; // component_b = 100\nconst centroid = { x: (APEX.x + BL.x + BR.x) / 3, y: (APEX.y + BL.y + BR.y) / 3 };\n\nfunction ternaryToPixel(a, b, c) {\n  return {\n    x: (a * BL.x + b * BR.x + c * APEX.x) / 100,\n    y: (a * BL.y + b * BR.y + c * APEX.y) / 100,\n  };\n}\n\nfunction outward(point, edgeP1, edgeP2, dist) {\n  let nx = -(edgeP2.y - edgeP1.y);\n  let ny = edgeP2.x - edgeP1.x;\n  const len = Math.hypot(nx, ny) || 1;\n  nx /= len;\n  ny /= len;\n  if (nx * (centroid.x - point.x) + ny * (centroid.y - point.y) > 0) {\n    nx = -nx;\n    ny = -ny;\n  }\n  return { x: point.x + nx * dist, y: point.y + ny * dist };\n}\n\n// --- Grid lines at 20% intervals + edge tick marks/labels -------------------\nconst levels = [20, 40, 60, 80];\nconst gridLineShapes = [];\nconst tickElements = [];\n\nlevels.forEach((L) => {\n  const constA = [ternaryToPixel(L, 100 - L, 0), ternaryToPixel(L, 0, 100 - L)];\n  const constB = [ternaryToPixel(100 - L, L, 0), ternaryToPixel(0, L, 100 - L)];\n  const constC = [ternaryToPixel(100 - L, 0, L), ternaryToPixel(0, 100 - L, L)];\n  gridLineShapes.push(constA, constB, constC);\n\n  // Left edge: component_a ticks\n  const aPoint = ternaryToPixel(L, 0, 100 - L);\n  const aLabelPos = outward(aPoint, APEX, BL, 26);\n  tickElements.push(\n    { type: \"line\", shape: { x1: aPoint.x, y1: aPoint.y, x2: outward(aPoint, APEX, BL, 10).x, y2: outward(aPoint, APEX, BL, 10).y }, style: { stroke: t.inkSoft, lineWidth: 1.5 } },\n    { type: \"text\", style: { text: String(L), x: aLabelPos.x, y: aLabelPos.y, fill: t.inkSoft, fontSize: 13, align: \"right\", verticalAlign: \"middle\" } },\n  );\n\n  // Bottom edge: component_b ticks\n  const bPoint = ternaryToPixel(100 - L, L, 0);\n  const bLabelPos = outward(bPoint, BL, BR, 26);\n  tickElements.push(\n    { type: \"line\", shape: { x1: bPoint.x, y1: bPoint.y, x2: outward(bPoint, BL, BR, 10).x, y2: outward(bPoint, BL, BR, 10).y }, style: { stroke: t.inkSoft, lineWidth: 1.5 } },\n    { type: \"text\", style: { text: String(L), x: bLabelPos.x, y: bLabelPos.y, fill: t.inkSoft, fontSize: 13, align: \"center\", verticalAlign: \"top\" } },\n  );\n\n  // Right edge: component_c ticks\n  const cPoint = ternaryToPixel(0, 100 - L, L);\n  const cLabelPos = outward(cPoint, BR, APEX, 26);\n  tickElements.push(\n    { type: \"line\", shape: { x1: cPoint.x, y1: cPoint.y, x2: outward(cPoint, BR, APEX, 10).x, y2: outward(cPoint, BR, APEX, 10).y }, style: { stroke: t.inkSoft, lineWidth: 1.5 } },\n    { type: \"text\", style: { text: String(L), x: cLabelPos.x, y: cLabelPos.y, fill: t.inkSoft, fontSize: 13, align: \"left\", verticalAlign: \"middle\" } },\n  );\n});\n\n// --- Vertex labels ------------------------------------------------------\nconst vertexLabels = [\n  { point: APEX, edgeA: BR, edgeB: BL, text: \"Tin (C)\" },\n  { point: BL, edgeA: APEX, edgeB: BR, text: \"Copper (A)\" },\n  { point: BR, edgeA: BL, edgeB: APEX, text: \"Zinc (B)\" },\n].map(({ point, text }) => {\n  const dir = { x: point.x - centroid.x, y: point.y - centroid.y };\n  const len = Math.hypot(dir.x, dir.y) || 1;\n  const pos = { x: point.x + (dir.x / len) * 50, y: point.y + (dir.y / len) * 50 };\n  return {\n    type: \"text\",\n    style: { text, x: pos.x, y: pos.y, fill: t.ink, fontSize: 22, fontWeight: 600, align: \"center\", verticalAlign: \"middle\" },\n  };\n});\n\n// --- Reference marker: bell-bronze target composition (higher-tin family, -----\n// well clear of the statuary-bronze sample cluster) for a real point of comparison\nconst target = { a: 78, b: 0, c: 22 }; // Cu 78% / Sn 22% — classic bell bronze\nconst targetPx = ternaryToPixel(target.a, target.b, target.c);\nconst towardCentroid = { x: centroid.x - targetPx.x, y: centroid.y - targetPx.y };\nconst towardLen = Math.hypot(towardCentroid.x, towardCentroid.y) || 1;\nconst targetLeaderEnd = {\n  x: targetPx.x + (towardCentroid.x / towardLen) * 90,\n  y: targetPx.y + (towardCentroid.y / towardLen) * 90,\n};\nconst targetLabelPos = {\n  x: targetPx.x + (towardCentroid.x / towardLen) * 130,\n  y: targetPx.y + (towardCentroid.y / towardLen) * 130,\n};\nconst referenceMarker = [\n  {\n    type: \"polygon\",\n    shape: {\n      points: [\n        [targetPx.x, targetPx.y - 9],\n        [targetPx.x + 9, targetPx.y],\n        [targetPx.x, targetPx.y + 9],\n        [targetPx.x - 9, targetPx.y],\n      ],\n    },\n    style: { stroke: t.ink, lineWidth: 2, fill: \"transparent\" },\n  },\n  {\n    type: \"line\",\n    shape: { x1: targetPx.x, y1: targetPx.y, x2: targetLeaderEnd.x, y2: targetLeaderEnd.y },\n    style: { stroke: t.ink, lineWidth: 1, lineDash: [3, 3] },\n  },\n  {\n    type: \"text\",\n    style: {\n      text: \"Reference: Bell Bronze (78/22)\",\n      x: targetLabelPos.x,\n      y: targetLabelPos.y,\n      fill: t.ink,\n      fontSize: 14,\n      fontWeight: 600,\n      align: \"center\",\n      verticalAlign: \"middle\",\n    },\n  },\n];\n\n// --- Title (fontsize scales down for titles longer than the 67-char baseline)\nconst title = \"Bronze Alloy Composition · ternary-basic · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / title.length));\n\n// --- Chart -------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  title: {\n    text: title,\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n  },\n  tooltip: {\n    formatter: (params) => {\n      const [copper, zinc, tin] = params.data;\n      return `Copper ${copper.toFixed(1)}%<br/>Zinc ${zinc.toFixed(1)}%<br/>Tin ${tin.toFixed(1)}%`;\n    },\n  },\n  graphic: {\n    elements: [\n      {\n        type: \"polyline\",\n        z: 1,\n        shape: { points: [[APEX.x, APEX.y], [BL.x, BL.y], [BR.x, BR.y], [APEX.x, APEX.y]] },\n        style: { stroke: t.inkSoft, lineWidth: 2.5, fill: \"transparent\" },\n      },\n      ...gridLineShapes.map(([p1, p2]) => ({\n        type: \"line\",\n        z: 1,\n        shape: { x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y },\n        style: { stroke: t.grid, lineWidth: 1 },\n      })),\n      ...tickElements.map((el) => ({ ...el, z: 1 })),\n      ...vertexLabels.map((el) => ({ ...el, z: 1 })),\n      ...referenceMarker.map((el) => ({ ...el, z: 3 })),\n    ],\n  },\n  series: [\n    {\n      type: \"custom\",\n      coordinateSystem: \"none\",\n      z: 2,\n      data: samples,\n      renderItem: (params, api) => {\n        const p = ternaryToPixel(api.value(0), api.value(1), api.value(2));\n        const opacity = 0.5 + 0.35 * (sampleDists[params.dataIndex] / maxDist);\n        return {\n          type: \"circle\",\n          shape: { cx: p.x, cy: p.y, r: 6.5 },\n          style: { fill: t.palette[0], stroke: t.pageBg, lineWidth: 1, opacity },\n        };\n      },\n    },\n  ],\n});\n"}