{"spec_id":"scatter-ashby-material","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nscatter-ashby-material: Ashby Material Selection Chart\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_abline,\n    geom_point,\n    geom_text,\n    ggplot,\n    guide_legend,\n    guides,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_log10,\n    scale_y_log10,\n    stat_ellipse,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens — Imprint palette (see prompts/default-style-guide.md)\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 categorical palette — positions 1–6 for 6 material families\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data — Density (kg/m³) vs. Young's Modulus (GPa) for common engineering materials\nnp.random.seed(42)\n\nfamilies = {\n    \"Metals\": {\"density\": (2700, 8900), \"modulus\": (45, 400), \"n\": 30},\n    \"Ceramics\": {\"density\": (2200, 4500), \"modulus\": (150, 450), \"n\": 20},\n    \"Polymers\": {\"density\": (900, 1500), \"modulus\": (0.2, 4.0), \"n\": 25},\n    \"Composites\": {\"density\": (1400, 2200), \"modulus\": (15, 200), \"n\": 20},\n    \"Elastomers\": {\"density\": (900, 1300), \"modulus\": (0.001, 0.1), \"n\": 18},\n    \"Foams\": {\"density\": (25, 300), \"modulus\": (0.001, 0.3), \"n\": 18},\n}\n\nmaterials = []\nfor family, props in families.items():\n    log_d_min = np.log10(props[\"density\"][0])\n    log_d_max = np.log10(props[\"density\"][1])\n    log_m_min = np.log10(props[\"modulus\"][0])\n    log_m_max = np.log10(props[\"modulus\"][1])\n    density = 10 ** np.random.uniform(log_d_min, log_d_max, props[\"n\"])\n    modulus = 10 ** np.random.uniform(log_m_min, log_m_max, props[\"n\"])\n    for d, m in zip(density, modulus, strict=True):\n        materials.append({\"family\": family, \"density\": d, \"modulus\": m})\n\ndf = pd.DataFrame(materials)\nfamily_order = list(families.keys())\n\n# Label positions — group centroids in log space with nudges to reduce upper-right crowding\nlabel_rows = []\nfor family in family_order:\n    subset = df[df[\"family\"] == family]\n    cx = np.log10(subset[\"density\"].values).mean()\n    cy = np.log10(subset[\"modulus\"].values).mean()\n\n    nudge_x, nudge_y = 0.0, 0.0\n    if family == \"Ceramics\":\n        nudge_y = 0.42  # push well above Metals cluster\n        nudge_x = -0.20\n    elif family == \"Metals\":\n        nudge_x = 0.28  # push right and slightly down from Ceramics\n        nudge_y = -0.22\n    elif family == \"Composites\":\n        nudge_y = -0.42  # push below the Ceramics/Metals zone\n        nudge_x = -0.15\n    elif family == \"Foams\":\n        nudge_x = -0.15\n\n    label_rows.append({\"family\": family, \"density\": 10 ** (cx + nudge_x), \"modulus\": 10 ** (cy + nudge_y)})\n\ndf_labels = pd.DataFrame(label_rows)\npalette = dict(zip(family_order, IMPRINT, strict=True))\n\ndf[\"family\"] = pd.Categorical(df[\"family\"], categories=family_order, ordered=True)\ndf_labels[\"family\"] = pd.Categorical(df_labels[\"family\"], categories=family_order, ordered=True)\n\n# E/ρ performance index guide lines: log10(E) = log10(ρ) + log10(C), slope=1 in log-log space\nguide_intercepts = [np.log10(c) for c in [0.001, 0.1, 10]]\nguide_labels_df = pd.DataFrame(\n    {\n        \"density\": [18.0, 18.0, 18.0],\n        \"modulus\": [18 * 0.001, 18 * 0.1, 18 * 10],\n        \"label\": [\"E/ρ = 0.001\", \"E/ρ = 0.1\", \"E/ρ = 10\"],\n    }\n)\n\ntitle = \"scatter-ashby-material · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot(df, aes(x=\"density\", y=\"modulus\"))\n    # E/ρ guide lines (behind data)\n    + geom_abline(intercept=guide_intercepts[0], slope=1, linetype=\"dashed\", color=INK_MUTED, size=0.35, alpha=0.5)\n    + geom_abline(intercept=guide_intercepts[1], slope=1, linetype=\"dashed\", color=INK_MUTED, size=0.35, alpha=0.5)\n    + geom_abline(intercept=guide_intercepts[2], slope=1, linetype=\"dashed\", color=INK_MUTED, size=0.35, alpha=0.5)\n    + geom_text(\n        guide_labels_df,\n        aes(x=\"density\", y=\"modulus\", label=\"label\"),\n        size=2.5,\n        color=INK_MUTED,\n        fontstyle=\"italic\",\n        ha=\"left\",\n        va=\"bottom\",\n        show_legend=False,\n    )\n    # Family envelope ellipses — idiomatic plotnine for material region boundaries\n    + stat_ellipse(\n        aes(fill=\"family\", group=\"family\"),\n        geom=\"polygon\",\n        level=0.90,\n        alpha=0.12,\n        color=INK_SOFT,\n        size=0.3,\n        linetype=\"solid\",\n    )\n    # Scatter points\n    + geom_point(aes(color=\"family\"), size=2.5, alpha=0.8, stroke=0.3)\n    + scale_x_log10()\n    + scale_y_log10()\n    + scale_color_manual(values=palette, name=\"Material Family\")\n    + scale_fill_manual(values=palette)\n    + labs(\n        x=\"Density (kg/m³)\",\n        y=\"Young's Modulus (GPa)\",\n        title=title,\n        subtitle=\"Density vs. stiffness · E/ρ performance index lines\",\n    )\n    # Family name labels at nudged centroids\n    + geom_text(\n        df_labels,\n        aes(x=\"density\", y=\"modulus\", label=\"family\"),\n        size=3.5,\n        fontweight=\"bold\",\n        color=INK,\n        show_legend=False,\n    )\n    + guides(color=guide_legend(override_aes={\"size\": 3, \"alpha\": 1}), fill=\"none\")\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", color=INK),\n        plot_subtitle=element_text(size=9, color=INK_SOFT),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, weight=\"bold\", color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_position=\"right\",\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        legend_key=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid_minor=element_blank(),\n        panel_grid_major=element_line(color=INK, size=0.2, alpha=0.12),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_border=element_blank(),\n        axis_line=element_line(color=INK_SOFT, size=0.3),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}