{"spec_id":"scatter-ashby-material","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-03\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Material data: x = density (kg/m³), y = Young's modulus (GPa)\nconst families = [\n  {\n    name: \"Metals\",\n    color: t.palette[0],\n    data: [\n      { x: 7900, y: 200 }, { x: 7850, y: 210 }, { x: 7800, y: 190 },\n      { x: 2700, y: 70 },  { x: 2710, y: 69 },  { x: 4500, y: 115 },\n      { x: 8900, y: 130 }, { x: 8940, y: 120 }, { x: 1740, y: 45 },\n      { x: 8440, y: 105 }, { x: 7150, y: 200 }, { x: 19300, y: 400 },\n    ],\n  },\n  {\n    name: \"Ceramics\",\n    color: t.palette[1],\n    data: [\n      { x: 3200, y: 380 }, { x: 3500, y: 300 }, { x: 2600, y: 76 },\n      { x: 3900, y: 400 }, { x: 2200, y: 400 }, { x: 3000, y: 200 },\n      { x: 2500, y: 310 }, { x: 3800, y: 340 }, { x: 4200, y: 320 },\n      { x: 3100, y: 170 }, { x: 2900, y: 420 }, { x: 3300, y: 250 },\n    ],\n  },\n  {\n    name: \"Polymers\",\n    color: t.palette[2],\n    data: [\n      { x: 950,  y: 2.0 }, { x: 1050, y: 3.0 }, { x: 1300, y: 1.3 },\n      { x: 1400, y: 3.5 }, { x: 1100, y: 0.8 }, { x: 960,  y: 1.7 },\n      { x: 1200, y: 4.5 }, { x: 1350, y: 2.5 }, { x: 1150, y: 1.2 },\n      { x: 1250, y: 3.2 }, { x: 1080, y: 2.1 }, { x: 990,  y: 1.5 },\n    ],\n  },\n  {\n    name: \"Composites\",\n    color: t.palette[3],\n    data: [\n      { x: 1600, y: 70 },  { x: 1800, y: 130 }, { x: 2000, y: 200 },\n      { x: 1750, y: 100 }, { x: 1900, y: 150 }, { x: 2100, y: 180 },\n      { x: 1650, y: 80 },  { x: 1550, y: 50 },  { x: 2200, y: 220 },\n      { x: 1700, y: 90 },  { x: 1850, y: 120 }, { x: 2050, y: 160 },\n    ],\n  },\n  {\n    name: \"Elastomers\",\n    color: t.palette[4],\n    data: [\n      { x: 1000, y: 0.002 }, { x: 1100, y: 0.005 }, { x: 1050, y: 0.003 },\n      { x: 950,  y: 0.008 }, { x: 1150, y: 0.001 }, { x: 1080, y: 0.004 },\n      { x: 1020, y: 0.006 }, { x: 980,  y: 0.01  }, { x: 1070, y: 0.003 },\n    ],\n  },\n  {\n    name: \"Foams\",\n    color: t.palette[5],\n    data: [\n      { x: 30,  y: 0.001 }, { x: 80,  y: 0.01  }, { x: 150, y: 0.05 },\n      { x: 200, y: 0.1   }, { x: 60,  y: 0.005 }, { x: 120, y: 0.03 },\n      { x: 50,  y: 0.003 }, { x: 250, y: 0.15  }, { x: 100, y: 0.02 },\n    ],\n  },\n  {\n    name: \"Natural\",\n    color: t.palette[6],\n    data: [\n      { x: 600, y: 10 }, { x: 800, y: 15 }, { x: 500, y: 8 },\n      { x: 700, y: 12 }, { x: 450, y: 6  }, { x: 1100, y: 20 },\n      { x: 650, y: 11 }, { x: 900, y: 17 }, { x: 550, y: 9  },\n    ],\n  },\n];\n\n// Log-centroid for label placement\nfunction logCentroid(pts) {\n  const lx = pts.map(p => Math.log10(p.x));\n  const ly = pts.map(p => Math.log10(p.y));\n  return {\n    x: Math.pow(10, lx.reduce((a, b) => a + b, 0) / lx.length),\n    y: Math.pow(10, ly.reduce((a, b) => a + b, 0) / ly.length),\n  };\n}\n\n// Guide line: E/ρ = k → E = k·ρ (lightweight stiffness index, slope=1 on log-log)\nfunction guideLineData(k, xMin, xMax, steps) {\n  const result = [];\n  const logMin = Math.log10(xMin);\n  const logMax = Math.log10(xMax);\n  for (let i = 0; i <= steps; i++) {\n    const logX = logMin + (i / steps) * (logMax - logMin);\n    const x = Math.pow(10, logX);\n    result.push({ x, y: k * x / 1000 }); // k in GPa·m³/kg, x in kg/m³ → y in GPa\n  }\n  return result;\n}\n\n// Andrew's monotone chain — convex hull of pixel-space points\nfunction convexHull(pts) {\n  if (pts.length < 3) return pts.slice();\n  function cross(o, a, b) {\n    return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x);\n  }\n  const sorted = pts.slice().sort((a, b) => a.x - b.x || a.y - b.y);\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--) {\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  lower.pop();\n  upper.pop();\n  return lower.concat(upper);\n}\n\n// Expand hull outward from centroid by `margin` pixels for visual padding\nfunction expandHull(hull, cx, cy, margin) {\n  return hull.map(p => {\n    const dx = p.x - cx;\n    const dy = p.y - cy;\n    const dist = Math.sqrt(dx * dx + dy * dy) || 1;\n    return { x: p.x + (dx / dist) * margin, y: p.y + (dy / dist) * margin };\n  });\n}\n\n// Plugin: convex-hull envelopes + family labels + guide-line annotations\nconst familyLabelPlugin = {\n  id: \"familyLabels\",\n  afterDatasetsDraw(chart) {\n    const ctx = chart.ctx;\n    const xScale = chart.scales.x;\n    const yScale = chart.scales.y;\n\n    // 1. Draw convex-hull filled regions behind labels\n    families.forEach(family => {\n      const pixelPts = family.data.map(p => ({\n        x: xScale.getPixelForValue(p.x),\n        y: yScale.getPixelForValue(p.y),\n      }));\n      const hull = convexHull(pixelPts);\n      if (hull.length < 2) return;\n\n      // Centroid of hull for expansion\n      const cx = hull.reduce((s, p) => s + p.x, 0) / hull.length;\n      const cy = hull.reduce((s, p) => s + p.y, 0) / hull.length;\n      const expanded = expandHull(hull, cx, cy, 18);\n\n      ctx.save();\n      ctx.beginPath();\n      ctx.moveTo(expanded[0].x, expanded[0].y);\n      for (let i = 1; i < expanded.length; i++) ctx.lineTo(expanded[i].x, expanded[i].y);\n      ctx.closePath();\n      ctx.fillStyle = family.color + \"28\"; // ~16% opacity fill\n      ctx.strokeStyle = family.color + \"70\"; // ~44% opacity border\n      ctx.lineWidth = 1.5;\n      ctx.fill();\n      ctx.stroke();\n      ctx.restore();\n    });\n\n    // 2. Draw family name pills at log-centroids\n    families.forEach(family => {\n      const c = logCentroid(family.data);\n      const px = xScale.getPixelForValue(c.x);\n      const py = yScale.getPixelForValue(c.y);\n\n      ctx.save();\n      ctx.font = \"bold 18px sans-serif\";\n      const metrics = ctx.measureText(family.name);\n      const pad = 6;\n      const bw = metrics.width + pad * 2;\n      const bh = 26 + pad * 2;\n\n      ctx.fillStyle = t.pageBg + \"D0\";\n      ctx.beginPath();\n      ctx.roundRect(px - bw / 2, py - bh / 2, bw, bh, 5);\n      ctx.fill();\n\n      ctx.fillStyle = family.color;\n      ctx.textAlign = \"center\";\n      ctx.textBaseline = \"middle\";\n      ctx.fillText(family.name, px, py);\n      ctx.restore();\n    });\n\n    // 3. Label the E/ρ guide lines at a mid-right anchor\n    const labelDataX = 12000;\n    const glPx = xScale.getPixelForValue(labelDataX);\n    const gl1Py = yScale.getPixelForValue(0.02 * labelDataX / 1000);\n    const gl2Py = yScale.getPixelForValue(0.004 * labelDataX / 1000);\n\n    ctx.save();\n    ctx.font = \"italic 13px sans-serif\";\n    ctx.fillStyle = t.inkSoft + \"CC\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"bottom\";\n    ctx.fillText(\"E/ρ = 0.02\", glPx + 6, gl1Py - 4);\n    ctx.fillText(\"E/ρ = 0.004\", glPx + 6, gl2Py - 4);\n    ctx.restore();\n  },\n};\n\n// --- Mount ---\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// Performance guide lines (E/ρ = const, two levels)\nconst guideLine1 = guideLineData(0.02, 20, 25000, 60);\nconst guideLine2 = guideLineData(0.004, 20, 25000, 60);\nconst guideColor = t.inkSoft + \"55\";\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: \"scatter\",\n  plugins: [familyLabelPlugin],\n  data: {\n    datasets: [\n      // Guide lines (drawn first, behind family data)\n      {\n        type: \"line\",\n        label: \"E/ρ guide\",\n        data: guideLine1,\n        borderColor: guideColor,\n        borderWidth: 1.5,\n        borderDash: [6, 4],\n        pointRadius: 0,\n        fill: false,\n        showLine: true,\n        tension: 0,\n        parsing: false,\n      },\n      {\n        type: \"line\",\n        label: \"_guide2\",\n        data: guideLine2,\n        borderColor: guideColor,\n        borderWidth: 1.5,\n        borderDash: [6, 4],\n        pointRadius: 0,\n        fill: false,\n        showLine: true,\n        tension: 0,\n        parsing: false,\n      },\n      // Family scatter datasets\n      ...families.map(fam => ({\n        label: fam.name,\n        data: fam.data,\n        backgroundColor: fam.color + \"88\",\n        borderColor: fam.color,\n        borderWidth: 1.2,\n        pointRadius: 9,\n        pointHoverRadius: 11,\n        parsing: false,\n      })),\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"scatter-ashby-material · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"bold\" },\n        padding: { top: 10, bottom: 18 },\n      },\n      legend: {\n        display: true,\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          boxWidth: 12,\n          boxHeight: 12,\n          filter: item => !item.text.startsWith(\"_\") && item.text !== \"E/ρ guide\",\n        },\n      },\n      tooltip: {\n        callbacks: {\n          label: ctx => `${ctx.dataset.label}: ρ=${ctx.parsed.x.toLocaleString()} kg/m³, E=${ctx.parsed.y} GPa`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"logarithmic\",\n        min: 10,\n        max: 30000,\n        title: {\n          display: true,\n          text: \"Density  (kg/m³)\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n          padding: { top: 8 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          maxTicksLimit: 8,\n          callback: v => {\n            const nice = [10, 30, 100, 300, 1000, 3000, 10000, 30000];\n            return nice.includes(v) ? v.toLocaleString() : null;\n          },\n        },\n        grid: { color: t.grid },\n      },\n      y: {\n        type: \"logarithmic\",\n        min: 0.0005,\n        max: 1000,\n        title: {\n          display: true,\n          text: \"Young's Modulus  (GPa)\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n          padding: { bottom: 8 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 13 },\n          maxTicksLimit: 8,\n          callback: v => {\n            const nice = [0.001, 0.01, 0.1, 1, 10, 100, 1000];\n            return nice.includes(v) ? v : null;\n          },\n        },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}