{"spec_id":"scatter-ashby-material","library":"echarts","language":"javascript","code":"// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-03\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Build a padded convex hull in log–log space; return linear-space coordinates\nfunction buildPaddedHull(linPts, padLog) {\n  const log = linPts.map(p => [Math.log10(p[0]), Math.log10(p[1])]);\n  const s = log.slice().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 s) {\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 = s.length - 1; i >= 0; i--) {\n    const p = s[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(); upper.pop();\n  let hull = lower.concat(upper);\n  if (hull.length < 3) {\n    const cx = log.reduce((s, p) => s + p[0], 0) / log.length;\n    const cy = log.reduce((s, p) => s + p[1], 0) / log.length;\n    hull = [\n      [cx - padLog, cy - padLog], [cx + padLog, cy - padLog],\n      [cx + padLog, cy + padLog], [cx - padLog, cy + padLog],\n    ];\n  }\n  const hcx = hull.reduce((s, p) => s + p[0], 0) / hull.length;\n  const hcy = hull.reduce((s, p) => s + p[1], 0) / hull.length;\n  return hull.map(p => {\n    const dx = p[0] - hcx, dy = p[1] - hcy;\n    const d = Math.hypot(dx, dy) || 0.001;\n    return [Math.pow(10, p[0] + (dx / d) * padLog), Math.pow(10, p[1] + (dy / d) * padLog)];\n  });\n}\n\n// --- Material data: [family_idx, density_kg_m3, youngs_modulus_GPa] ---------\nconst FAMILIES = [\n  { name: \"Metals\",     color: t.palette[0] },\n  { name: \"Ceramics\",   color: t.palette[1] },\n  { name: \"Polymers\",   color: t.palette[2] },\n  { name: \"Composites\", color: t.palette[3] },\n  { name: \"Elastomers\", color: t.palette[4] },\n  { name: \"Natural\",    color: t.palette[5] },\n  { name: \"Foams\",      color: t.palette[6] },\n];\n\nconst POINTS = [\n  // Metals\n  [0,  2700,  70],  // Aluminium alloys\n  [0,  7800, 200],  // Steels\n  [0,  4500, 110],  // Titanium alloys\n  [0,  8900, 120],  // Copper alloys\n  [0,  8900, 200],  // Nickel alloys\n  [0,  1740,  45],  // Magnesium alloys\n  [0, 11340,  16],  // Lead alloys\n  [0,  1850, 290],  // Beryllium\n  [0,  7900, 215],  // Cast iron\n  [0, 19300, 400],  // Tungsten alloys\n  [0,  8400, 130],  // Brass\n  [0,  4500, 105],  // Titanium (cp)\n  // Ceramics\n  [1, 3900, 380],  // Alumina (Al₂O₃)\n  [1, 3200, 410],  // Silicon carbide\n  [1, 3200, 310],  // Silicon nitride\n  [1, 2500,  70],  // Borosilicate glass\n  [1, 2400,  30],  // Concrete\n  [1, 5600, 200],  // Zirconia\n  [1, 3600, 445],  // Boron carbide\n  [1, 2600,  72],  // Soda-lime glass\n  [1, 3500, 350],  // Aluminium nitride\n  // Polymers\n  [2,  950,   1.0],\n  [2, 1400,   2.5],\n  [2, 1140,   3.0],\n  [2, 2200,   0.5],  // PTFE\n  [2, 1200,   2.3],  // Polycarbonate\n  [2, 1200,   3.5],  // Epoxy\n  [2,  910,   1.3],  // Polypropylene\n  [2, 1050,   2.0],  // ABS\n  [2, 1300,   1.8],  // PMMA\n  [2, 1350,   3.0],  // PET\n  [2, 1100,   0.8],  // LDPE\n  // Composites\n  [3, 1550, 140],\n  [3, 1750,  40],  // GFRP\n  [3, 1400,  75],  // Kevlar / epoxy\n  [3, 2700, 100],  // Al / SiC MMC\n  [3, 1600,  80],  // CFRP quasi-iso\n  [3, 1800,  55],  // GFRP woven\n  [3, 1500, 120],  // CFRP random\n  // Elastomers\n  [4,  930, 0.050],  // Natural rubber\n  [4, 1250, 0.010],  // Neoprene\n  [4, 1200, 0.007],  // Silicone rubber\n  [4, 1050, 0.040],  // Polyurethane elastomer\n  [4, 1000, 0.002],  // Soft rubber\n  [4, 1100, 0.020],  // EPDM\n  // Natural materials\n  [5,  700,  12],   // Oak\n  [5,  550,   9],   // Pine\n  [5,  650,  15],   // Bamboo\n  [5,  120,  0.02], // Cork\n  [5, 2000,  20],   // Cortical bone\n  [5, 2600,  60],   // Nacre / shell\n  [5,  400,   2],   // Balsa\n  // Foams\n  [6,   25,  0.003],  // Polystyrene foam\n  [6,   30,  0.001],  // Polyurethane foam\n  [6,  300,   2.0],   // Aluminium foam\n  [6,  400,   1.0],   // Ceramic foam\n  [6,   50,  0.005],  // Cork foam\n];\n\n// Group data points by family index\nconst familyPts = FAMILIES.map((_, fi) =>\n  POINTS.filter(p => p[0] === fi).map(p => [p[1], p[2]])\n);\n\n// Precompute padded hulls (0.20 log₁₀ units padding)\nconst hullData = familyPts.map(pts => buildPaddedHull(pts, 0.20));\n\n// Log-centroid per family for label placement\nconst logCentroids = familyPts.map(pts => {\n  const lx = pts.reduce((s, p) => s + Math.log10(p[0]), 0) / pts.length;\n  const ly = pts.reduce((s, p) => s + Math.log10(p[1]), 0) / pts.length;\n  return [Math.pow(10, lx), Math.pow(10, ly)];\n});\n\n// --- Guide lines: constant E^(1/2)/ρ (lightweight stiffness index) ----------\nconst DENSITY_RANGE = [10, 25000];\nconst M_VALUES = [3e-5, 3e-4];\nconst guideLineSeries = M_VALUES.map((M, i) => ({\n  type: \"line\",\n  name: i === 0 ? \"E^½/ρ = const\" : \"__guide2\",\n  data: DENSITY_RANGE.map(rho => [rho, Math.pow(M * rho, 2)]).filter(p => p[1] >= 0.001 && p[1] <= 1000),\n  symbol: \"none\",\n  silent: true,\n  legendHoverLink: false,\n  lineStyle: { color: t.inkSoft, type: \"dashed\", width: 1.5, opacity: 0.45 },\n  z: 0,\n}));\n\n// --- Chart ------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: \"scatter-ashby-material · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" },\n  },\n\n  legend: {\n    data: FAMILIES.map(f => f.name),\n    bottom: 18,\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    itemWidth: 14,\n    itemHeight: 14,\n    itemGap: 20,\n  },\n\n  grid: {\n    left: 110,\n    right: 60,\n    top: 80,\n    bottom: 100,\n  },\n\n  xAxis: {\n    type: \"log\",\n    name: \"Density  (kg / m³)\",\n    nameLocation: \"middle\",\n    nameGap: 48,\n    min: 10,\n    max: 25000,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n\n  yAxis: {\n    type: \"log\",\n    name: \"Young's Modulus  (GPa)\",\n    nameLocation: \"middle\",\n    nameGap: 72,\n    min: 0.001,\n    max: 1000,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    axisTick: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n\n  series: [\n    // Hull regions + family labels (custom series, one render per family)\n    {\n      type: \"custom\",\n      silent: true,\n      legendHoverLink: false,\n      renderItem(params, api) {\n        const fi = params.dataIndex;\n        const hull = hullData[fi];\n        const col = FAMILIES[fi].color;\n        const name = FAMILIES[fi].name;\n        const pixPoly = hull.map(p => api.coord([p[0], p[1]]));\n        const [cx, cy] = api.coord(logCentroids[fi]);\n        return {\n          type: \"group\",\n          children: [\n            {\n              type: \"polygon\",\n              transition: [],\n              shape: { points: pixPoly },\n              style: { fill: col, opacity: 0.15, stroke: col, lineWidth: 1.8, strokeOpacity: 0.55 },\n            },\n            {\n              type: \"text\",\n              style: {\n                text: name,\n                x: cx,\n                y: cy,\n                textAlign: \"center\",\n                textVerticalAlign: \"middle\",\n                font: \"bold 17px sans-serif\",\n                fill: col,\n                opacity: 0.95,\n              },\n            },\n          ],\n        };\n      },\n      data: logCentroids,\n      z: 1,\n    },\n\n    // Scatter series per family\n    ...FAMILIES.map((f, fi) => ({\n      type: \"scatter\",\n      name: f.name,\n      data: familyPts[fi],\n      symbolSize: 12,\n      itemStyle: { color: f.color, opacity: 0.9, borderColor: t.pageBg, borderWidth: 1 },\n      z: 3,\n    })),\n\n    // Guide lines for constant E^(1/2)/ρ performance index\n    ...guideLineSeries,\n  ],\n});\n"}