{"spec_id":"scatter-ashby-material","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG, 32-bit safe via Math.imul) -------------------\nlet seed = 42;\nfunction rand() {\n  seed = (Math.imul(1103515245, seed) + 12345) | 0;\n  return (seed >>> 0) / 4294967296;\n}\nfunction logUniform(min, max) {\n  const lo = Math.log10(min);\n  const hi = Math.log10(max);\n  return Math.pow(10, lo + rand() * (hi - lo));\n}\n\n// --- Data: cost vs. thermal conductivity, by material family ---------------\n// (heat-exchanger material selection: want high conductivity at low cost)\nconst families = [\n  { name: \"Metals\", xRange: [1, 30], yRange: [15, 400], count: 18,\n    materials: [\"Copper\", \"Aluminum Alloy\", \"Steel\", \"Titanium Alloy\", \"Brass\", \"Magnesium Alloy\", \"Nickel Alloy\"] },\n  { name: \"Ceramics\", xRange: [2, 50], yRange: [1, 150], count: 18,\n    materials: [\"Silicon Carbide\", \"Alumina\", \"Aluminum Nitride\", \"Zirconia\", \"Borosilicate Glass\", \"Silicon Nitride\"] },\n  { name: \"Composites\", xRange: [10, 150], yRange: [0.3, 8], count: 18,\n    materials: [\"CFRP\", \"GFRP\", \"Al-SiC MMC\", \"Kevlar Composite\", \"Plywood\"] },\n  { name: \"Polymers\", xRange: [1, 8], yRange: [0.15, 0.5], count: 18,\n    materials: [\"Polyethylene\", \"Polypropylene\", \"Nylon\", \"PVC\", \"Polycarbonate\", \"PTFE\"] },\n  { name: \"Elastomers\", xRange: [2, 15], yRange: [0.1, 0.3], count: 18,\n    materials: [\"Natural Rubber\", \"Silicone Rubber\", \"Neoprene\", \"EPDM\", \"Polyurethane Elastomer\"] },\n  { name: \"Foams\", xRange: [1, 20], yRange: [0.02, 0.09], count: 18,\n    materials: [\"Polyurethane Foam\", \"Polystyrene Foam\", \"Cork\", \"Metal Foam\", \"Mineral Wool\"] },\n];\n\nfamilies.forEach((f) => {\n  f.points = Array.from({ length: f.count }, () => ({\n    x: logUniform(f.xRange[0], f.xRange[1]),\n    y: logUniform(f.yRange[0], f.yRange[1]),\n    material: f.materials[Math.floor(rand() * f.materials.length)],\n  }));\n});\n\n// --- Title (mandated format, fontsize scaled to length) --------------------\nconst title = \"Thermal Conductivity vs. Cost · scatter-ashby-material · javascript · highcharts · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / title.length)));\n\n// --- Chart -------------------------------------------------------------------\nconst chart = Highcharts.chart(\"container\", {\n  chart: { type: \"scatter\", backgroundColor: \"transparent\", animation: false,\n           style: { fontFamily: \"inherit\" } },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: { text: title, style: { color: t.ink, fontSize: `${titleFontSize}px`, fontWeight: \"600\" } },\n  xAxis: { type: \"logarithmic\", gridLineWidth: 1, gridLineColor: t.grid,\n           lineColor: t.inkSoft, tickColor: t.inkSoft,\n           labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n           title: { text: \"Cost (USD / kg)\", style: { color: t.inkSoft, fontSize: \"16px\" } } },\n  yAxis: { type: \"logarithmic\", gridLineWidth: 1, gridLineColor: t.grid,\n           lineColor: t.inkSoft, tickColor: t.inkSoft,\n           labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n           title: { text: \"Thermal Conductivity (W / m·K)\", style: { color: t.inkSoft, fontSize: \"16px\" } } },\n  legend: { enabled: false },\n  plotOptions: { series: { animation: false } },\n  series: [\n    ...families.map((f, i) => ({\n      name: f.name,\n      type: \"scatter\",\n      color: t.palette[i],\n      data: f.points.map((p) => ({ x: p.x, y: p.y, material: p.material })),\n      marker: { radius: 5, lineColor: t.pageBg, lineWidth: 1 },\n      tooltip: { pointFormat: \"<b>{point.material}</b><br/>Cost: {point.x:.2f} $/kg<br/>k: {point.y:.3f} W/m·K\" },\n    })),\n    {\n      name: \"Thermal value index\",\n      type: \"line\",\n      color: t.ink,\n      dashStyle: \"Dash\",\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      data: [[0.5, 1], [200, 400]],\n    },\n  ],\n});\n\n// --- Family bubble regions: padded convex hulls + direct labels ------------\n// Computed in pixel space (already log-transformed by the axes), so a straight\n// monotone-chain hull reflects the visual point cloud, not the raw values.\nfunction convexHull(pts) {\n  const s = pts.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  upper.pop();\n  lower.pop();\n  return lower.concat(upper);\n}\nfunction padHull(hull, pad) {\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  return hull.map(([x, y]) => {\n    const dx = x - cx;\n    const dy = y - cy;\n    const len = Math.sqrt(dx * dx + dy * dy) || 1;\n    return [x + (dx / len) * pad, y + (dy / len) * pad];\n  });\n}\n\nconst xAxis = chart.xAxis[0];\nconst yAxis = chart.yAxis[0];\nconst hullGroup = chart.renderer.g(\"ashby-hulls\").attr({ zIndex: 2 }).add();\nconst labelGroup = chart.renderer.g(\"ashby-labels\").attr({ zIndex: 7 }).add();\n\nfamilies.forEach((f, i) => {\n  const pixelPts = f.points.map((p) => [xAxis.toPixels(p.x, false), yAxis.toPixels(p.y, false)]);\n  const hull = padHull(convexHull(pixelPts), 22);\n  const path = hull.map(([x, y], idx) => `${idx === 0 ? \"M\" : \"L\"}${x},${y}`).join(\" \") + \" Z\";\n  const color = t.palette[i];\n  chart.renderer\n    .path()\n    .attr({\n      d: path,\n      fill: Highcharts.color(color).setOpacity(0.14).get(),\n      stroke: Highcharts.color(color).setOpacity(0.55).get(),\n      \"stroke-width\": 1.5,\n      \"stroke-linejoin\": \"round\",\n    })\n    .add(hullGroup);\n\n  const topPoint = hull.reduce((top, p) => (p[1] < top[1] ? p : top), hull[0]);\n  const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;\n  chart.renderer\n    .text(f.name, cx, topPoint[1] - 8)\n    .attr({ align: \"center\", zIndex: 7 })\n    .css({ color, fontSize: \"15px\", fontWeight: \"600\", textOutline: `2px ${t.pageBg}` })\n    .add(labelGroup);\n});\n\n// --- Guide-line label, aligned to the line's on-screen slope ----------------\nconst gx1 = xAxis.toPixels(0.5, false);\nconst gy1 = yAxis.toPixels(1, false);\nconst gx2 = xAxis.toPixels(200, false);\nconst gy2 = yAxis.toPixels(400, false);\nconst guideAngle = (Math.atan2(gy2 - gy1, gx2 - gx1) * 180) / Math.PI;\nchart.renderer\n  .text(\"k / cost = 2 W·kg / (m·K·$)\", (gx1 + gx2) / 2 - 60, (gy1 + gy2) / 2 - 60)\n  .attr({ rotation: guideAngle, zIndex: 7 })\n  .css({ color: t.inkSoft, fontSize: \"13px\", fontStyle: \"italic\" })\n  .add(labelGroup);\n"}