{"spec_id":"scatter-ashby-material","library":"muix","language":"javascript","code":"// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-24\n//# anyplot-orientation: landscape\n// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\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-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsTooltip } from \"@mui/x-charts/ChartsTooltip\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Deterministic LCG (seed 42) — no Math.random() in the browser harness\nlet seed = 42;\nfunction rng() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\n\n// --- Data: classic density vs. Young's modulus Ashby chart -----------------\n// Each family spans an archetypal (density, modulus) box; materials are\n// sampled log-uniformly inside that box, which is how real material families\n// cluster on a log-log Ashby chart — a compact, roughly rectangular smear\n// rather than a tight point cloud.\nconst FAMILIES = [\n  {\n    name: \"Metals\",\n    rho: [1800, 11400],\n    e: [40, 410],\n    materials: [\n      \"Mild Steel\", \"Stainless Steel\", \"Cast Iron\", \"Aluminum Alloys\",\n      \"Titanium Alloys\", \"Copper Alloys\", \"Magnesium Alloys\", \"Nickel Superalloys\",\n      \"Zinc Alloys\", \"Lead Alloys\", \"Tungsten\", \"Molybdenum\",\n      \"Brass\", \"Bronze\", \"Chromium\", \"Tool Steel\",\n    ],\n  },\n  {\n    name: \"Polymers\",\n    rho: [900, 1500],\n    e: [0.5, 4.5],\n    materials: [\n      \"HDPE\", \"Polypropylene\", \"PVC\", \"Nylon 6,6\",\n      \"Polycarbonate\", \"Acrylic (PMMA)\", \"ABS\", \"Polystyrene\",\n      \"PTFE\", \"Epoxy Resin\", \"Polyester Resin\", \"Phenolic Resin\",\n      \"Melamine\", \"Rigid Polyurethane\", \"PEEK\",\n    ],\n  },\n  {\n    name: \"Ceramics\",\n    rho: [2000, 4200],\n    e: [10, 450],\n    materials: [\n      \"Alumina\", \"Silicon Carbide\", \"Silicon Nitride\", \"Zirconia\",\n      \"Boron Carbide\", \"Tungsten Carbide\", \"Magnesia\", \"Soda-Lime Glass\",\n      \"Borosilicate Glass\", \"Concrete\", \"Fired Brick\", \"Natural Stone\",\n      \"Fireclay\", \"Cement Paste\",\n    ],\n  },\n  {\n    name: \"Composites\",\n    rho: [500, 1900],\n    e: [5, 200],\n    materials: [\n      \"CFRP\", \"GFRP\", \"Kevlar Composite\", \"Boron Composite\",\n      \"Carbon-Epoxy\", \"Glass-Epoxy\", \"Al-SiC MMC\", \"Cermet\",\n      \"Plywood\", \"Wood (Along Grain)\", \"Wood (Across Grain)\", \"Bamboo\",\n      \"Laminated Veneer\",\n    ],\n  },\n  {\n    name: \"Elastomers\",\n    rho: [900, 1300],\n    e: [0.001, 0.1],\n    materials: [\n      \"Natural Rubber\", \"Silicone Rubber\", \"Neoprene\", \"Butyl Rubber\",\n      \"Nitrile Rubber\", \"EPDM\", \"Polyurethane Elastomer\", \"Latex Rubber\",\n      \"Isoprene Rubber\", \"SBR\", \"Fluoroelastomer\", \"Polychloroprene\",\n    ],\n  },\n  {\n    name: \"Foams\",\n    rho: [20, 300],\n    e: [0.001, 0.5],\n    materials: [\n      \"Rigid Polyurethane Foam\", \"Polystyrene Foam\", \"Polyethylene Foam\", \"Cork\",\n      \"Balsa Wood\", \"Aluminum Foam\", \"Flexible PU Foam\", \"Syntactic Foam\",\n      \"Melamine Foam\", \"Foamed Glass\", \"Cellular Ceramic Foam\", \"Phenolic Foam\",\n    ],\n  },\n];\n\n// A few named materials are common knowledge and would look wrong if sampled\n// from their family's full range — dense refractory metals/ceramics and PTFE\n// sit well outside the rest of their family, so they get a tight sub-range\n// around their real published density/modulus instead.\nconst MATERIAL_OVERRIDES = {\n  Tungsten: { rho: [18900, 19600], e: [385, 411] },\n  \"Tungsten Carbide\": { rho: [15400, 15800], e: [530, 650] },\n  PTFE: { rho: [2150, 2200], e: [0.4, 0.75] },\n};\n\nconst familyPoints = FAMILIES.map((family) => ({\n  ...family,\n  points: family.materials.map((material) => {\n    const range = MATERIAL_OVERRIDES[material] ?? { rho: family.rho, e: family.e };\n    const logRho = Math.log10(range.rho[0]) + rng() * (Math.log10(range.rho[1]) - Math.log10(range.rho[0]));\n    const logE = Math.log10(range.e[0]) + rng() * (Math.log10(range.e[1]) - Math.log10(range.e[0]));\n    return { material, rho: 10 ** logRho, e: 10 ** logE };\n  }),\n}));\n\nconst allRho = familyPoints.flatMap((f) => f.points.map((p) => p.rho));\nconst allE = familyPoints.flatMap((f) => f.points.map((p) => p.e));\nconst X_MIN = Math.min(...allRho) / 1.7;\nconst X_MAX = Math.max(...allRho) * 1.7;\nconst Y_MIN = Math.min(...allE) / 2.4;\nconst Y_MAX = Math.max(...allE) * 3.2;\n\nfunction formatAxisValue(v) {\n  if (v >= 100) return Math.round(v).toLocaleString();\n  if (v >= 1) return Number(v.toPrecision(2)).toString();\n  return Number(v.toPrecision(1)).toString();\n}\n\n// d3's log-scale ticks include the 2..9 minor steps within every decade,\n// which collide at this plot's span. `tickInterval` as a filter function is\n// only honored for point scales, so force the tick set itself down to just\n// the decade values (…, 0.1, 1, 10, 100, …) via an explicit array.\nfunction decadeTicks(min, max) {\n  const start = Math.floor(Math.log10(min));\n  const end = Math.ceil(Math.log10(max));\n  const ticks = [];\n  for (let p = start; p <= end; p += 1) ticks.push(10 ** p);\n  return ticks;\n}\n\n// Convex hull via the monotone-chain algorithm — the \"convex-hull envelope\"\n// the spec calls out for showing each material family as a region.\nfunction convexHull(points) {\n  const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n  const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);\n  const lower = [];\n  for (const p of sorted) {\n    while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) lower.pop();\n    lower.push(p);\n  }\n  const upper = [];\n  for (let i = sorted.length - 1; i >= 0; i -= 1) {\n    const p = sorted[i];\n    while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) upper.pop();\n    upper.push(p);\n  }\n  upper.pop();\n  lower.pop();\n  return lower.concat(upper);\n}\n\nconst HULL_MARGIN = 15;\n\n// Renders each family as an inflated convex-hull region behind the points,\n// with a direct color-matched label — the standard Ashby-chart convention\n// (no legend needed once every region carries its own name).\nfunction FamilyRegions() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  if (!xScale || !yScale) return null;\n\n  return (\n    <g>\n      {familyPoints.map((family, i) => {\n        const pixels = family.points.map((p) => [xScale(p.rho), yScale(p.e)]);\n        const hull = convexHull(pixels);\n        const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;\n        const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;\n        const inflated = hull.map(([x, y]) => {\n          const dx = x - cx;\n          const dy = y - cy;\n          const len = Math.hypot(dx, dy) || 1;\n          return [x + (dx / len) * HULL_MARGIN, y + (dy / len) * HULL_MARGIN];\n        });\n        const d = `M ${inflated.map((p) => p.join(\",\")).join(\" L \")} Z`;\n        const labelY = Math.min(...inflated.map((p) => p[1])) - 8;\n        return (\n          <g key={family.name}>\n            <path\n              d={d}\n              fill={t.palette[i]}\n              fillOpacity={0.07}\n              stroke={t.palette[i]}\n              strokeOpacity={0.6}\n              strokeWidth={1.5}\n              strokeLinejoin=\"round\"\n            />\n            <text\n              x={cx}\n              y={labelY}\n              textAnchor=\"middle\"\n              fontSize={15}\n              fontWeight={600}\n              fill={t.palette[i]}\n              stroke={t.pageBg}\n              strokeWidth={4}\n              paintOrder=\"stroke\"\n            >\n              {family.name}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Constant specific-stiffness (E / rho) guide lines — a classic Ashby-chart\n// selection aid for lightweight-and-stiff design. Straight in log-log space,\n// so a clip against the padded axis box needs only the two endpoints.\nconst GUIDE_INDICES = [1, 100];\n\nfunction clipIsoline(index, xDomain, yDomain) {\n  const k = index / 1000; // E (GPa) = k * rho (kg/m^3)\n  let x0 = xDomain[0];\n  let x1 = xDomain[1];\n  let y0 = k * x0;\n  let y1 = k * x1;\n  if (y0 < yDomain[0]) { x0 = yDomain[0] / k; y0 = yDomain[0]; }\n  if (y0 > yDomain[1]) { x0 = yDomain[1] / k; y0 = yDomain[1]; }\n  if (y1 < yDomain[0]) { x1 = yDomain[0] / k; y1 = yDomain[0]; }\n  if (y1 > yDomain[1]) { x1 = yDomain[1] / k; y1 = yDomain[1]; }\n  if (x0 >= x1) return null;\n  return { x0, y0, x1, y1 };\n}\n\nfunction PerformanceGuideLines() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  if (!xScale || !yScale) return null;\n  const xDomain = xScale.domain();\n  const yDomain = yScale.domain();\n\n  return (\n    <g>\n      {GUIDE_INDICES.map((index) => {\n        const seg = clipIsoline(index, xDomain, yDomain);\n        if (!seg) return null;\n        const x0 = xScale(seg.x0);\n        const y0 = yScale(seg.y0);\n        const x1 = xScale(seg.x1);\n        const y1 = yScale(seg.y1);\n        const angle = (Math.atan2(y1 - y0, x1 - x0) * 180) / Math.PI;\n        const midX = (x0 + x1) / 2;\n        const midY = (y0 + y1) / 2;\n        return (\n          <g key={index}>\n            <line x1={x0} y1={y0} x2={x1} y2={y1} stroke={t.inkSoft} strokeWidth={1.5} strokeDasharray=\"7 6\" opacity={0.55} />\n            <text\n              x={midX}\n              y={midY - 8}\n              textAnchor=\"middle\"\n              transform={`rotate(${angle}, ${midX}, ${midY - 8})`}\n              fontSize={12}\n              fill={t.inkSoft}\n              stroke={t.pageBg}\n              strokeWidth={4}\n              paintOrder=\"stroke\"\n            >\n              {`E/ρ = ${index}`}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"scatter-ashby-material · javascript · muix · anyplot.ai\";\nconst SUBTITLE = \"Density vs. Young's modulus · dashed lines mark constant specific stiffness E/ρ\";\n\nconst MARGIN = { top: 130, right: 105, bottom: 90, left: 110 };\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={MARGIN}\n      skipAnimation\n      sx={{ \"& .MuiChartsGrid-line\": { stroke: t.grid, strokeWidth: 1 } }}\n      series={familyPoints.map((family, i) => ({\n        type: \"scatter\",\n        id: family.name,\n        label: family.name,\n        color: t.palette[i],\n        markerSize: 6,\n        data: family.points.map((p, j) => ({ x: p.rho, y: p.e, id: `${family.name}-${j}` })),\n      }))}\n      xAxis={[\n        {\n          id: \"density\",\n          scaleType: \"log\",\n          min: X_MIN,\n          max: X_MAX,\n          disableTicks: true,\n          tickInterval: decadeTicks(X_MIN, X_MAX),\n          label: \"Density (kg/m³)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n          valueFormatter: formatAxisValue,\n        },\n      ]}\n      yAxis={[\n        {\n          id: \"modulus\",\n          scaleType: \"log\",\n          min: Y_MIN,\n          max: Y_MAX,\n          disableTicks: true,\n          tickInterval: decadeTicks(Y_MIN, Y_MAX),\n          label: \"Young's Modulus (GPa)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 13, fill: t.inkSoft },\n          valueFormatter: formatAxisValue,\n        },\n      ]}\n    >\n      <ChartsGrid vertical horizontal />\n      <FamilyRegions />\n      <PerformanceGuideLines />\n      <ScatterPlot />\n      <ChartsXAxis axisId=\"density\" />\n      <ChartsYAxis axisId=\"modulus\" />\n      <ChartsTooltip trigger=\"item\" />\n      <text x={width / 2} y={46} textAnchor=\"middle\" fontSize={26} fontWeight={600} fill={t.ink}>\n        {TITLE}\n      </text>\n      <text x={width / 2} y={78} textAnchor=\"middle\" fontSize={15} fill={t.inkSoft}>\n        {SUBTITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}