{"spec_id":"scatter-ashby-material","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nscatter-ashby-material: Ashby Material Selection Chart\nLibrary: bokeh 3.9.0 | Python 3.13.13\nQuality: 90/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Prevent this file (bokeh.py) from shadowing the bokeh package on sys.path\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if p and os.path.abspath(p) != _here]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem, Range1d, Title\nfrom bokeh.plotting import figure\nfrom PIL import Image\nfrom scipy.spatial import ConvexHull\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens — Imprint palette style guide\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 — canonical order, first series always #009E73\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Data — density (kg/m³) vs Young's modulus (GPa) for 7 material families\nnp.random.seed(42)\n\nfamilies = {\n    \"Metals\": {\n        \"density\": [\n            2700,\n            4500,\n            7800,\n            7900,\n            8900,\n            8500,\n            7300,\n            19300,\n            1740,\n            7200,\n            2800,\n            7600,\n            8000,\n            8700,\n            7100,\n            4600,\n            8200,\n            7400,\n            7850,\n            8100,\n        ],\n        \"modulus\": [69, 116, 210, 193, 117, 100, 45, 400, 44, 170, 73, 200, 195, 130, 90, 110, 205, 160, 215, 180],\n    },\n    \"Polymers\": {\n        \"density\": [950, 1050, 1200, 1400, 1140, 900, 1300, 1070, 1240, 1350, 1420, 960, 1100, 1180, 1500],\n        \"modulus\": [0.9, 2.5, 3.0, 2.8, 3.5, 1.3, 4.0, 2.0, 2.9, 3.8, 7.0, 0.4, 1.8, 2.2, 4.5],\n    },\n    \"Ceramics\": {\n        \"density\": [3900, 3200, 2200, 3100, 5600, 3500, 2650, 3980, 6000, 2500, 3850, 3300, 5700, 2350, 3150],\n        \"modulus\": [380, 310, 70, 200, 210, 270, 73, 400, 230, 95, 370, 290, 200, 62, 220],\n    },\n    \"Composites\": {\n        \"density\": [1600, 1550, 2000, 1800, 1500, 1700, 1900, 1450, 1650, 2100, 1750, 1580, 1850, 1950, 2050],\n        \"modulus\": [140, 70, 45, 90, 180, 100, 55, 200, 120, 40, 80, 150, 65, 50, 35],\n    },\n    \"Elastomers\": {\n        \"density\": [1100, 920, 1250, 1500, 1150, 1050, 980, 1300, 1380, 1200],\n        \"modulus\": [0.005, 0.002, 0.01, 0.05, 0.008, 0.003, 0.001, 0.02, 0.04, 0.015],\n    },\n    \"Foams\": {\n        \"density\": [30, 60, 120, 200, 50, 80, 150, 35, 100, 250, 45, 70, 180, 25, 110],\n        \"modulus\": [0.001, 0.01, 0.1, 0.3, 0.005, 0.02, 0.2, 0.002, 0.05, 0.5, 0.003, 0.015, 0.25, 0.0008, 0.08],\n    },\n    \"Natural Materials\": {\n        \"density\": [600, 700, 500, 1500, 900, 450, 800, 650, 1100, 400, 750, 550, 1300, 850, 1000],\n        \"modulus\": [12, 14, 8, 30, 10, 6, 11, 9, 25, 5, 16, 7, 20, 13, 18],\n    },\n}\n\n# Imprint palette in canonical order; 7 families use first 7 positions\nfamily_names = list(families.keys())\nfamily_colors = {name: IMPRINT_PALETTE[i] for i, name in enumerate(family_names)}\n\n# Visual hierarchy — primary structural families carry more emphasis\nemphasis = {\n    \"Metals\": {\"fill_alpha\": 0.20, \"line_width\": 3.0, \"marker_size\": 22},\n    \"Ceramics\": {\"fill_alpha\": 0.18, \"line_width\": 2.5, \"marker_size\": 20},\n    \"Composites\": {\"fill_alpha\": 0.16, \"line_width\": 2.5, \"marker_size\": 18},\n    \"Polymers\": {\"fill_alpha\": 0.14, \"line_width\": 2.0, \"marker_size\": 18},\n    \"Natural Materials\": {\"fill_alpha\": 0.12, \"line_width\": 2.0, \"marker_size\": 16},\n    \"Elastomers\": {\"fill_alpha\": 0.12, \"line_width\": 1.5, \"marker_size\": 14},\n    \"Foams\": {\"fill_alpha\": 0.12, \"line_width\": 1.5, \"marker_size\": 12},\n}\n\nrows = []\nfor family_name, props in families.items():\n    for d, m in zip(props[\"density\"], props[\"modulus\"], strict=True):\n        jitter_d = d * (1 + np.random.uniform(-0.08, 0.08))\n        jitter_m = m * (1 + np.random.uniform(-0.12, 0.12))\n        rows.append({\"family\": family_name, \"density\": jitter_d, \"modulus\": jitter_m})\ndf = pd.DataFrame(rows)\n\n# Title — 52 chars < 67 baseline, no font scaling needed\ntitle_str = \"scatter-ashby-material · python · bokeh · anyplot.ai\"\n\n# Plot — 3200×1800 landscape, log-log axes\np = figure(\n    width=3200,\n    height=1800,\n    x_axis_type=\"log\",\n    y_axis_type=\"log\",\n    x_axis_label=\"Density (kg/m³)\",\n    y_axis_label=\"Young's Modulus (GPa)\",\n    title=title_str,\n    x_range=Range1d(10, 50000),\n    y_range=Range1d(0.0005, 1000),\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Subtitle\np.add_layout(\n    Title(\n        text=\"Young's Modulus vs Density — Material Selection Map\",\n        text_font_size=\"24pt\",\n        text_color=INK_MUTED,\n        text_font_style=\"italic\",\n    ),\n    \"above\",\n)\n\n# Hover tooltip\np.add_tools(\n    HoverTool(tooltips=[(\"Family\", \"@family\"), (\"Density\", \"@density{0,0} kg/m³\"), (\"Modulus\", \"@modulus{0.000} GPa\")])\n)\n\n# Performance index guide lines — E/ρ = constant (slope 1 on log-log)\nfor c_val, label_text, lx, ly in [(0.01, \"E/ρ = 0.01\", 8000, 0.01 * 8000), (1.0, \"E/ρ = 1\", 500, 1.0 * 500)]:\n    p.line(\n        [10, 50000],\n        [c_val * 10, c_val * 50000],\n        line_color=INK_MUTED,\n        line_width=2.0,\n        line_dash=\"dashed\",\n        line_alpha=0.55,\n    )\n    p.add_layout(\n        Label(\n            x=lx,\n            y=ly,\n            text=label_text,\n            text_font_size=\"16pt\",\n            text_font_style=\"italic\",\n            text_color=INK_MUTED,\n            x_offset=8,\n            y_offset=-10,\n            background_fill_color=ELEVATED_BG,\n            background_fill_alpha=0.80,\n        )\n    )\n\n# Convex hull envelopes + centroid labels per family\nlegend_items = []\nfor family_name in family_names:\n    fam_df = df[df[\"family\"] == family_name]\n    log_x = np.log10(fam_df[\"density\"].values)\n    log_y = np.log10(fam_df[\"modulus\"].values)\n    color = family_colors[family_name]\n    emph = emphasis[family_name]\n\n    if len(fam_df) >= 3:\n        pts = np.column_stack([log_x, log_y])\n        hull = ConvexHull(pts)\n        hull_indices = list(hull.vertices) + [hull.vertices[0]]\n        hull_pts = pts[hull_indices]\n\n        center_log_x = pts[hull.vertices, 0].mean()\n        center_log_y = pts[hull.vertices, 1].mean()\n\n        # Tighter expand for crowded upper region, more room for sparse lower families\n        expand_factor = 0.10 if family_name in (\"Metals\", \"Ceramics\", \"Composites\") else 0.16\n        expanded = hull_pts.copy()\n        for i in range(len(expanded)):\n            dx = expanded[i, 0] - center_log_x\n            dy = expanded[i, 1] - center_log_y\n            expanded[i, 0] += dx * expand_factor\n            expanded[i, 1] += dy * expand_factor\n\n        p.patch(\n            list(10 ** expanded[:, 0]),\n            list(10 ** expanded[:, 1]),\n            fill_alpha=emph[\"fill_alpha\"],\n            fill_color=color,\n            line_color=color,\n            line_alpha=0.65,\n            line_width=emph[\"line_width\"],\n        )\n\n        # Label at log-space centroid — reduces crowding vs top-of-hull approach\n        p.add_layout(\n            Label(\n                x=10**center_log_x,\n                y=10**center_log_y,\n                text=family_name,\n                text_font_size=\"20pt\",\n                text_font_style=\"bold\",\n                text_color=color,\n                x_offset=0,\n                y_offset=10,\n            )\n        )\n\n# Scatter points per family\nfor family_name in family_names:\n    fam_df = df[df[\"family\"] == family_name]\n    source = ColumnDataSource(\n        data={\n            \"x\": fam_df[\"density\"].values,\n            \"y\": fam_df[\"modulus\"].values,\n            \"density\": fam_df[\"density\"].values,\n            \"modulus\": fam_df[\"modulus\"].values,\n            \"family\": fam_df[\"family\"].values,\n        }\n    )\n    emph = emphasis[family_name]\n    renderer = p.scatter(\n        x=\"x\",\n        y=\"y\",\n        source=source,\n        size=emph[\"marker_size\"],\n        color=family_colors[family_name],\n        alpha=0.82,\n        line_color=PAGE_BG,\n        line_width=1.5,\n    )\n    legend_items.append(LegendItem(label=family_name, renderers=[renderer]))\n\n# Inside legend — bottom_right is empty in this Ashby chart layout\nlegend = Legend(\n    items=legend_items,\n    location=\"bottom_right\",\n    label_text_font_size=\"24pt\",\n    label_text_color=INK_SOFT,\n    glyph_height=40,\n    glyph_width=40,\n    spacing=8,\n    padding=16,\n    margin=20,\n    background_fill_alpha=0.92,\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    border_line_alpha=0.4,\n)\np.add_layout(legend)\n\n# Typography\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.title.text_font_style = \"normal\"\n\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\n\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\n\n# Grid — both axes, subtle opacity for log-log scatter\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.08\np.ygrid.grid_line_alpha = 0.12\np.xgrid.grid_line_width = 1\np.ygrid.grid_line_width = 1\n\n# Chrome — remove outline and axis lines for a clean Ashby aesthetic\np.outline_line_color = None\np.xaxis.axis_line_color = None\np.yaxis.axis_line_color = None\np.xaxis.minor_tick_line_color = None\np.yaxis.minor_tick_line_color = None\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\n\n# Save HTML (catalog interactive artifact)\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\n# Screenshot PNG via headless Chrome — Selenium 4 / Selenium Manager.\n# Chrome's viewport is smaller than the window size by a fixed overhead (~139 px\n# on this host). Use a 400 px taller window so the full 1800 px figure is visible,\n# then crop the screenshot back to exactly 3200×1800.\nW, H = 3200, 1800\nWIN_H = H + 400  # extra headroom so the full figure fits in the viewport\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{WIN_H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\ndriver.set_window_size(W, WIN_H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\nraw_path = f\"plot-{THEME}-raw.png\"\ndriver.save_screenshot(raw_path)\ndriver.quit()\n\n# Crop screenshot to exact canvas size\nimg = Image.open(raw_path)\nimg_cropped = img.crop((0, 0, W, H))\nimg_cropped.save(f\"plot-{THEME}.png\")\nPath(raw_path).unlink(missing_ok=True)\n"}