{"spec_id":"scatter-ashby-material","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nscatter-ashby-material: Ashby Material Selection Chart\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport matplotlib.patches as mpatches\nimport matplotlib.path as mpath\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom scipy.spatial import ConvexHull\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — 8 hues, canonical order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — Density (kg/m³) vs Young's Modulus (GPa) for engineering materials\nnp.random.seed(42)\n\nfamilies = {\n    \"Metals\": {\n        \"density\": [2700, 4500, 7850, 8900, 7200, 8000, 11340, 1740, 7870, 8940, 2810, 7140, 8500, 4620, 7300],\n        \"modulus\": [69, 116, 200, 117, 170, 193, 16, 45, 210, 130, 71, 105, 100, 110, 190],\n    },\n    \"Polymers\": {\n        \"density\": [950, 1050, 1200, 1140, 1380, 910, 1420, 1040, 1300, 1170, 980, 1090, 1250, 1060, 1350],\n        \"modulus\": [0.9, 2.5, 3.5, 2.8, 3.0, 1.3, 2.9, 2.0, 4.0, 3.2, 0.7, 1.8, 3.8, 2.2, 3.3],\n    },\n    \"Ceramics\": {\n        \"density\": [3980, 3200, 2200, 3900, 5680, 2650, 3150, 5900, 2400, 3500, 3100, 4000, 2500, 3700, 2800],\n        \"modulus\": [380, 440, 70, 350, 200, 73, 310, 210, 65, 400, 300, 360, 60, 320, 90],\n    },\n    \"Composites\": {\n        \"density\": [1600, 1550, 2000, 1800, 1450, 1700, 1900, 1500, 1650, 1850, 1520, 1750, 1400, 1620, 1950],\n        \"modulus\": [140, 70, 45, 80, 180, 60, 50, 200, 120, 55, 90, 65, 160, 100, 40],\n    },\n    \"Elastomers\": {\n        \"density\": [920, 1100, 1250, 1500, 1050, 1150, 1300, 1000, 1200, 1400, 960, 1080, 1350, 1450, 1180],\n        \"modulus\": [0.005, 0.01, 0.05, 0.03, 0.008, 0.02, 0.04, 0.003, 0.015, 0.035, 0.004, 0.012, 0.045, 0.025, 0.007],\n    },\n    \"Foams\": {\n        \"density\": [30, 60, 120, 200, 50, 80, 150, 40, 100, 180, 35, 70, 130, 90, 160],\n        \"modulus\": [0.001, 0.01, 0.05, 0.2, 0.005, 0.02, 0.1, 0.003, 0.03, 0.15, 0.002, 0.015, 0.06, 0.025, 0.12],\n    },\n    \"Natural\\nMaterials\": {\n        \"density\": [600, 700, 500, 1200, 800, 450, 650, 1500, 550, 750, 900, 480, 680, 1100, 850],\n        \"modulus\": [12, 14, 8, 20, 10, 6, 11, 25, 9, 13, 16, 7, 12.5, 18, 15],\n    },\n}\n\nfamily_names = list(families.keys())\nfamily_colors = {name: IMPRINT_PALETTE[i] for i, name in enumerate(family_names)}\n\n# Label offsets (log10 coords) to reduce upper-right crowding\nlabel_offsets = {\"Metals\": (0.30, -0.35), \"Ceramics\": (-0.25, 0.22), \"Composites\": (-0.30, -0.15)}\n\n# Plot\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\nfor family_name, data in families.items():\n    density = np.array(data[\"density\"], dtype=float)\n    modulus = np.array(data[\"modulus\"], dtype=float)\n    color = family_colors[family_name]\n\n    ax.scatter(density, modulus, s=80, alpha=0.7, color=color, edgecolors=PAGE_BG, linewidth=0.5, zorder=4)\n\n    # Smooth Bezier convex hull envelope using matplotlib Path/PathPatch\n    log_pts = np.column_stack([np.log10(density), np.log10(modulus)])\n    rng = np.random.RandomState(hash(family_name) % 2**31)\n    log_pts_jittered = log_pts + rng.randn(*log_pts.shape) * 0.01\n\n    try:\n        hull = ConvexHull(log_pts_jittered)\n        hull_verts = np.vstack([log_pts_jittered[hull.vertices], log_pts_jittered[hull.vertices[0]]])\n        centroid = np.mean(hull_verts[:-1], axis=0)\n        directions = hull_verts - centroid\n        norms = np.linalg.norm(directions, axis=1, keepdims=True)\n        norms = np.where(norms == 0, 1, norms)\n        hull_verts = hull_verts + directions / norms * 0.08\n        n_verts = len(hull_verts) - 1\n        codes = [mpath.Path.MOVETO]\n        pts = [hull_verts[0]]\n        for i in range(n_verts):\n            p0, p1 = hull_verts[i], hull_verts[(i + 1) % n_verts]\n            pts.extend([p0 + (p1 - p0) * 0.33, p0 + (p1 - p0) * 0.67, p1])\n            codes.extend([mpath.Path.CURVE4, mpath.Path.CURVE4, mpath.Path.CURVE4])\n        codes.append(mpath.Path.CLOSEPOLY)\n        pts.append(pts[0])\n        pts_arr = np.array(pts)\n        pts_data = np.column_stack([10 ** pts_arr[:, 0], 10 ** pts_arr[:, 1]])\n        path = mpath.Path(pts_data, codes)\n        ax.add_patch(mpatches.PathPatch(path, facecolor=color, edgecolor=\"none\", alpha=0.15, zorder=2))\n        ax.add_patch(mpatches.PathPatch(path, facecolor=\"none\", edgecolor=color, alpha=0.5, linewidth=1.2, zorder=3))\n    except Exception:\n        pass\n\n    # Family label at geometric center with optional offset to reduce crowding\n    cx_log = np.mean(np.log10(density))\n    cy_log = np.mean(np.log10(modulus))\n    dx, dy = label_offsets.get(family_name, (0, 0))\n    ax.text(\n        10 ** (cx_log + dx),\n        10 ** (cy_log + dy),\n        family_name,\n        fontsize=8,\n        fontweight=\"bold\",\n        color=color,\n        ha=\"center\",\n        va=\"center\",\n        zorder=5,\n        bbox={\"boxstyle\": \"round,pad=0.25\", \"facecolor\": ELEVATED_BG, \"edgecolor\": \"none\", \"alpha\": 0.9},\n    )\n\n# Guide lines for constant E/rho (lightweight stiffness index)\nguide_density = np.logspace(1, 5, 100)\nfor c_val, label_text in [(0.01, r\"$E/\\rho = 10$ kPa$\\cdot$m$^3$/kg\"), (1.0, r\"$E/\\rho = 1$ GPa$\\cdot$m$^3$/Mg\")]:\n    guide_modulus = c_val * guide_density / 1000\n    mask = (guide_modulus >= 5e-4) & (guide_modulus <= 600)\n    ax.plot(\n        guide_density[mask], guide_modulus[mask], color=INK_MUTED, linewidth=0.8, linestyle=\"--\", alpha=0.6, zorder=1\n    )\n    valid_idx = np.where(mask)[0]\n    if len(valid_idx) > 0:\n        mid = valid_idx[len(valid_idx) // 3]\n        ax.text(\n            guide_density[mid],\n            guide_modulus[mid] * 1.5,\n            label_text,\n            fontsize=7,\n            color=INK_MUTED,\n            rotation=30,\n            ha=\"center\",\n            va=\"bottom\",\n        )\n\n# Style\ntitle = \"scatter-ashby-material · python · matplotlib · anyplot.ai\"\ntitle_len = len(title)\ntitle_fs = max(8, round(12 * 67 / title_len)) if title_len > 67 else 12\n\nax.set_xscale(\"log\")\nax.set_yscale(\"log\")\nax.set_xlim(10, 30000)\nax.set_ylim(5e-4, 600)\nax.set_xlabel(\"Density (kg/m$^3$)\", fontsize=10, color=INK, labelpad=6)\nax.set_ylabel(\"Young's Modulus (GPa)\", fontsize=10, color=INK, labelpad=6)\nax.set_title(title, fontsize=title_fs, fontweight=\"medium\", color=INK, pad=10)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\n\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\nax.grid(True, which=\"major\", alpha=0.15, linewidth=0.6, color=INK, zorder=0)\nax.grid(True, which=\"minor\", alpha=0.08, linewidth=0.4, color=INK, zorder=0)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}