{"spec_id":"scatter-ashby-material","library":"d3","language":"javascript","code":"// anyplot.ai\n// scatter-ashby-material: Ashby Material Selection Chart\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-03\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 72, right: 60, bottom: 92, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Material data: density (kg/m³) vs Young's modulus (GPa)\nconst data = [\n  // Metals\n  { family: \"Metals\", rho: 7800,  E: 200  },\n  { family: \"Metals\", rho: 2700,  E: 70   },\n  { family: \"Metals\", rho: 4500,  E: 110  },\n  { family: \"Metals\", rho: 8900,  E: 120  },\n  { family: \"Metals\", rho: 1740,  E: 45   },\n  { family: \"Metals\", rho: 8900,  E: 200  },\n  { family: \"Metals\", rho: 7200,  E: 170  },\n  { family: \"Metals\", rho: 19300, E: 400  },\n  { family: \"Metals\", rho: 11300, E: 18   },\n  { family: \"Metals\", rho: 7100,  E: 100  },\n  { family: \"Metals\", rho: 4430,  E: 115  },\n  { family: \"Metals\", rho: 2700,  E: 26   },\n  // Ceramics\n  { family: \"Ceramics\", rho: 3900, E: 390 },\n  { family: \"Ceramics\", rho: 3210, E: 420 },\n  { family: \"Ceramics\", rho: 2500, E: 70  },\n  { family: \"Ceramics\", rho: 3200, E: 300 },\n  { family: \"Ceramics\", rho: 2400, E: 30  },\n  { family: \"Ceramics\", rho: 5600, E: 200 },\n  { family: \"Ceramics\", rho: 2500, E: 450 },\n  { family: \"Ceramics\", rho: 3000, E: 340 },\n  // Polymers\n  { family: \"Polymers\", rho: 1320, E: 3.7 },\n  { family: \"Polymers\", rho: 1200, E: 2.4 },\n  { family: \"Polymers\", rho: 1140, E: 3.0 },\n  { family: \"Polymers\", rho: 2200, E: 0.5 },\n  { family: \"Polymers\", rho: 950,  E: 0.7 },\n  { family: \"Polymers\", rho: 1200, E: 3.5 },\n  { family: \"Polymers\", rho: 900,  E: 1.5 },\n  { family: \"Polymers\", rho: 1400, E: 2.5 },\n  { family: \"Polymers\", rho: 1050, E: 2.8 },\n  { family: \"Polymers\", rho: 1380, E: 3.1 },\n  // Composites\n  { family: \"Composites\", rho: 1600, E: 150 },\n  { family: \"Composites\", rho: 1900, E: 25  },\n  { family: \"Composites\", rho: 1400, E: 75  },\n  { family: \"Composites\", rho: 2000, E: 200 },\n  { family: \"Composites\", rho: 1750, E: 70  },\n  { family: \"Composites\", rho: 1550, E: 100 },\n  // Elastomers\n  { family: \"Elastomers\", rho: 920,  E: 0.05  },\n  { family: \"Elastomers\", rho: 1200, E: 0.005 },\n  { family: \"Elastomers\", rho: 1100, E: 0.04  },\n  { family: \"Elastomers\", rho: 1240, E: 0.02  },\n  { family: \"Elastomers\", rho: 960,  E: 0.001 },\n  { family: \"Elastomers\", rho: 1050, E: 0.01  },\n  // Foams\n  { family: \"Foams\", rho: 25,  E: 0.00025 },\n  { family: \"Foams\", rho: 50,  E: 0.0005  },\n  { family: \"Foams\", rho: 100, E: 0.001   },\n  { family: \"Foams\", rho: 200, E: 0.005   },\n  { family: \"Foams\", rho: 300, E: 0.05    },\n  { family: \"Foams\", rho: 400, E: 3.0     },\n  // Natural Materials\n  { family: \"Natural\", rho: 200,  E: 0.03 },\n  { family: \"Natural\", rho: 600,  E: 8    },\n  { family: \"Natural\", rho: 700,  E: 11   },\n  { family: \"Natural\", rho: 900,  E: 20   },\n  { family: \"Natural\", rho: 960,  E: 0.1  },\n  { family: \"Natural\", rho: 1250, E: 2    },\n  { family: \"Natural\", rho: 1900, E: 20   },\n];\n\nconst families = [\"Metals\", \"Ceramics\", \"Polymers\", \"Composites\", \"Elastomers\", \"Foams\", \"Natural\"];\nconst colorScale = d3.scaleOrdinal().domain(families).range(t.palette);\n\n// Log-log scales\nconst x = d3.scaleLog().domain([18, 25000]).range([0, iw]);\nconst y = d3.scaleLog().domain([0.0002, 800]).range([ih, 0]);\n\n// SVG\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Clip path for data area\nsvg.append(\"defs\").append(\"clipPath\").attr(\"id\", \"plot-area\")\n  .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\n\n// Grid lines\nconst xTickVals = [30, 100, 300, 1000, 3000, 10000];\nconst yTickVals = [0.001, 0.01, 0.1, 1, 10, 100];\n\ng.selectAll(\".xg\").data(xTickVals).join(\"line\")\n  .attr(\"x1\", d => x(d)).attr(\"x2\", d => x(d))\n  .attr(\"y1\", 0).attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.8);\n\ng.selectAll(\".yg\").data(yTickVals).join(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", d => y(d)).attr(\"y2\", d => y(d))\n  .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.8);\n\n// Guide lines: E/ρ = const (slope = 1 in log-log space, indicating equal specific stiffness)\nconst guideKs = [5e-6, 5e-5, 5e-4];\nconst guideGroup = g.append(\"g\").attr(\"clip-path\", \"url(#plot-area)\");\nfor (const k of guideKs) {\n  const rhoA = 18, rhoB = 25000;\n  const eA = k * rhoA, eB = k * rhoB;\n  const clampA = Math.max(eA, 0.0002), clampB = Math.min(eB, 800);\n  const rClampA = clampA / k, rClampB = clampB / k;\n  guideGroup.append(\"line\")\n    .attr(\"x1\", x(rClampA)).attr(\"y1\", y(clampA))\n    .attr(\"x2\", x(rClampB)).attr(\"y2\", y(clampB))\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 1)\n    .attr(\"stroke-dasharray\", \"5,4\").attr(\"opacity\", 0.3);\n}\n\n// Guide annotation — placed just inside right margin along the steepest guide\ng.append(\"text\")\n  .attr(\"x\", x(18000)).attr(\"y\", y(5e-4 * 18000) - 10)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\").style(\"font-style\", \"italic\")\n  .text(\"E/ρ = c\");\n\n// Hull region drawing\nfunction expandHull(hull, factor) {\n  const cx = d3.mean(hull, p => p[0]);\n  const cy = d3.mean(hull, p => p[1]);\n  return hull.map(([px, py]) => [cx + (px - cx) * factor, cy + (py - cy) * factor]);\n}\n\nconst blobLine = d3.line().curve(d3.curveCatmullRomClosed.alpha(0.5));\nconst byFamily = d3.group(data, d => d.family);\nconst hullGroup = g.append(\"g\").attr(\"clip-path\", \"url(#plot-area)\");\n\nfor (const [fam, pts] of byFamily) {\n  const sc = pts.map(d => [x(d.rho), y(d.E)]);\n  let hull = d3.polygonHull(sc);\n  if (!hull) hull = sc;\n  const expanded = expandHull(hull, 1.14);\n  const col = colorScale(fam);\n  hullGroup.append(\"path\")\n    .datum(expanded).attr(\"d\", blobLine)\n    .attr(\"fill\", col).attr(\"fill-opacity\", 0.13)\n    .attr(\"stroke\", col).attr(\"stroke-width\", 2).attr(\"stroke-opacity\", 0.7)\n    .attr(\"stroke-linejoin\", \"round\");\n}\n\n// Scatter points\ng.append(\"g\").attr(\"clip-path\", \"url(#plot-area)\")\n  .selectAll(\"circle\").data(data).join(\"circle\")\n  .attr(\"cx\", d => x(d.rho)).attr(\"cy\", d => y(d.E))\n  .attr(\"r\", 5)\n  .attr(\"fill\", d => colorScale(d.family))\n  .attr(\"fill-opacity\", 0.9)\n  .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.2);\n\n// Family labels at visual centroid of each hull group\nconst labelLines = {\n  \"Natural\": [\"Natural\", \"Materials\"],\n};\n\nfor (const [fam, pts] of byFamily) {\n  const sc = pts.map(d => [x(d.rho), y(d.E)]);\n  const cx = d3.mean(sc, p => p[0]);\n  const cy = d3.mean(sc, p => p[1]);\n  const col = colorScale(fam);\n  const lines = labelLines[fam] || [fam];\n  const label = g.append(\"text\")\n    .attr(\"x\", cx).attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", col).style(\"font-size\", \"14px\").style(\"font-weight\", \"700\");\n  lines.forEach((line, i) =>\n    label.append(\"tspan\")\n      .attr(\"x\", cx).attr(\"y\", cy + (i - (lines.length - 1) / 2) * 17)\n      .text(line)\n  );\n}\n\n// Axes\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).tickValues(xTickVals)\n    .tickFormat(d => d >= 1000 ? `${d / 1000}k` : String(d)).tickSize(5));\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).tickValues(yTickVals)\n    .tickFormat(d3.format(\"~g\")).tickSize(5));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// Axis labels\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 66)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"500\")\n  .text(\"Density  (kg/m³)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2).attr(\"y\", -86)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"500\")\n  .text(\"Young's Modulus  (GPa)\");\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"scatter-ashby-material · javascript · d3 · anyplot.ai\");\n"}