| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>Crash Intelligence Platform</title> |
| <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script> |
| <link rel="preconnect" href="https://fonts.googleapis.com" /> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@500;600;700&family=Source+Sans+3:wght@400;600;700&display=swap" rel="stylesheet" /> |
| <style> |
| :root { |
| --ink: #0f172a; |
| --muted: #475569; |
| --green: #0b6e4f; |
| --green-dark: #0b3d2e; |
| --blue: #1b4965; |
| --sky: #5fa8d3; |
| --bg: #f1f5f9; |
| --card: #ffffff; |
| --line: #e2e8f0; |
| --danger: #c44536; |
| } |
| * { box-sizing: border-box; } |
| html, body { |
| margin: 0; |
| padding: 0; |
| background: var(--bg); |
| color: var(--ink); |
| font-family: "Source Sans 3", "Segoe UI", sans-serif; |
| } |
| .layout { |
| display: grid; |
| grid-template-columns: 280px 1fr; |
| min-height: 100vh; |
| } |
| .sidebar { |
| background: linear-gradient(180deg, #0b3d2e 0%, #1b4965 100%); |
| color: #f8fafc; |
| padding: 1.25rem 1rem 2rem; |
| position: sticky; |
| top: 0; |
| height: 100vh; |
| overflow-y: auto; |
| } |
| .sidebar h2 { |
| font-family: "IBM Plex Sans", sans-serif; |
| font-size: 1.05rem; |
| margin: 0 0 0.75rem; |
| color: #fff; |
| } |
| .sidebar label { |
| display: block; |
| font-size: 0.82rem; |
| margin: 0.75rem 0 0.3rem; |
| color: #e2e8f0; |
| } |
| .sidebar select, |
| .sidebar input[type="range"] { |
| width: 100%; |
| } |
| .sidebar select { |
| background: rgba(255,255,255,0.12); |
| color: #fff; |
| border: 1px solid rgba(255,255,255,0.25); |
| border-radius: 8px; |
| padding: 0.45rem 0.5rem; |
| } |
| .sidebar select option { color: #0f172a; } |
| .family-box { |
| max-height: 180px; |
| overflow-y: auto; |
| background: rgba(255,255,255,0.08); |
| border-radius: 8px; |
| padding: 0.4rem 0.55rem; |
| border: 1px solid rgba(255,255,255,0.15); |
| } |
| .family-box label { |
| display: flex; |
| gap: 0.4rem; |
| align-items: center; |
| margin: 0.25rem 0; |
| font-size: 0.78rem; |
| color: #f1f5f9; |
| } |
| .weight-val { float: right; color: #bae6fd; } |
| .caption { |
| margin-top: 1rem; |
| font-size: 0.75rem; |
| color: #cbd5e1; |
| line-height: 1.4; |
| } |
| .main { padding: 1.1rem 1.4rem 2rem; } |
| .hero { |
| background: linear-gradient(120deg, #0b3d2e 0%, #1b4965 55%, #5fa8d3 100%); |
| color: #fff; |
| border-radius: 14px; |
| padding: 1.35rem 1.5rem; |
| margin-bottom: 1rem; |
| } |
| .hero h1 { |
| font-family: "IBM Plex Sans", sans-serif; |
| margin: 0 0 0.35rem; |
| font-size: 1.85rem; |
| color: #fff; |
| } |
| .hero p { margin: 0; color: #e2e8f0; max-width: 900px; line-height: 1.45; } |
| .tabs { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.4rem; |
| margin: 0.8rem 0 1rem; |
| } |
| .tab-btn { |
| background: #fff; |
| border: 1px solid var(--line); |
| color: var(--ink); |
| border-radius: 999px; |
| padding: 0.45rem 0.9rem; |
| font-weight: 600; |
| cursor: pointer; |
| font-size: 0.9rem; |
| } |
| .tab-btn.active { |
| background: var(--green); |
| border-color: var(--green); |
| color: #fff; |
| } |
| .panel { display: none; } |
| .panel.active { display: block; } |
| .metrics { |
| display: grid; |
| grid-template-columns: repeat(5, minmax(0, 1fr)); |
| gap: 0.75rem; |
| margin-bottom: 1rem; |
| } |
| .metric { |
| background: var(--card); |
| border: 1px solid var(--line); |
| border-left: 4px solid var(--green); |
| border-radius: 10px; |
| padding: 0.85rem 1rem; |
| } |
| .metric .label { color: var(--muted); font-size: 0.82rem; } |
| .metric .value { |
| color: var(--green); |
| font-size: 1.45rem; |
| font-weight: 700; |
| margin-top: 0.15rem; |
| font-family: "IBM Plex Sans", sans-serif; |
| } |
| .grid-2 { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 0.9rem; |
| margin-bottom: 0.9rem; |
| } |
| .grid-3 { |
| display: grid; |
| grid-template-columns: 1fr 1fr 1fr; |
| gap: 0.9rem; |
| margin-bottom: 0.9rem; |
| } |
| .card { |
| background: var(--card); |
| border: 1px solid var(--line); |
| border-radius: 12px; |
| padding: 0.75rem; |
| min-height: 120px; |
| } |
| .card h3 { |
| margin: 0 0 0.5rem; |
| font-size: 1rem; |
| color: var(--green-dark); |
| font-family: "IBM Plex Sans", sans-serif; |
| } |
| .note { |
| background: #fff; |
| border: 1px solid var(--line); |
| border-left: 4px solid var(--green); |
| border-radius: 10px; |
| padding: 0.9rem 1rem; |
| color: var(--ink); |
| margin: 0.8rem 0; |
| line-height: 1.45; |
| } |
| table.data { |
| width: 100%; |
| border-collapse: collapse; |
| font-size: 0.85rem; |
| background: #fff; |
| } |
| table.data th, table.data td { |
| border-bottom: 1px solid var(--line); |
| padding: 0.45rem 0.5rem; |
| text-align: left; |
| color: var(--ink); |
| } |
| table.data th { |
| background: #ecfdf5; |
| color: var(--green-dark); |
| position: sticky; |
| top: 0; |
| } |
| .table-wrap { |
| max-height: 360px; |
| overflow: auto; |
| border: 1px solid var(--line); |
| border-radius: 10px; |
| background: #fff; |
| } |
| .controls-row { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 0.75rem; |
| align-items: end; |
| margin-bottom: 0.8rem; |
| } |
| .controls-row label { |
| font-size: 0.82rem; |
| color: var(--muted); |
| display: block; |
| margin-bottom: 0.25rem; |
| } |
| .controls-row select, .controls-row button, .controls-row input { |
| padding: 0.45rem 0.6rem; |
| border-radius: 8px; |
| border: 1px solid var(--line); |
| background: #fff; |
| color: var(--ink); |
| } |
| .controls-row button { |
| background: var(--green); |
| color: #fff; |
| border: none; |
| font-weight: 600; |
| cursor: pointer; |
| } |
| pre.card-text { |
| background: #0f172a; |
| color: #e2e8f0; |
| padding: 1rem; |
| border-radius: 10px; |
| overflow: auto; |
| font-size: 0.82rem; |
| line-height: 1.4; |
| min-height: 320px; |
| } |
| .loading { |
| min-height: 60vh; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: var(--green-dark); |
| font-size: 1.1rem; |
| } |
| .footer { |
| margin-top: 1.2rem; |
| color: var(--muted); |
| font-size: 0.8rem; |
| border-top: 1px solid var(--line); |
| padding-top: 0.8rem; |
| } |
| @media (max-width: 1100px) { |
| .layout { grid-template-columns: 1fr; } |
| .sidebar { position: relative; height: auto; } |
| .metrics { grid-template-columns: repeat(2, 1fr); } |
| .grid-2, .grid-3 { grid-template-columns: 1fr; } |
| } |
| </style> |
| </head> |
| <body> |
| <div id="app" class="loading">Loading Crash Intelligence comprehensive dashboard…</div> |
|
|
| <script> |
| const COLORS = ["#0B6E4F","#08A045","#1B4965","#5FA8D3","#C44536","#E8871E","#6B4C9A","#2A9D8F","#E76F51","#264653","#F4A261","#457B9D","#9B2226","#005F73","#CA6702"]; |
| const LAYOUT = { |
| paper_bgcolor: "#ffffff", |
| plot_bgcolor: "#f8fafc", |
| font: { family: "Source Sans 3, Segoe UI, sans-serif", color: "#0f172a", size: 12 }, |
| margin: { l: 55, r: 25, t: 45, b: 50 }, |
| legend: { bgcolor: "rgba(255,255,255,0.92)", bordercolor: "#ddd", borderwidth: 1 } |
| }; |
| const PLOT_CONFIG = { |
| responsive: true, |
| displayModeBar: true, |
| displaylogo: false, |
| scrollZoom: true, |
| staticPlot: false, |
| modeBarButtonsToRemove: ["lasso2d", "select2d"] |
| }; |
| |
| let DATA = null; |
| let state = { |
| families: [], |
| scenario: "Side Impact", |
| component: "Battery Enclosure", |
| maxCost: 40, |
| minUts: 200, |
| maxDensity: 8, |
| weights: { crash: 0.3, weight: 0.2, cost: 0.2, sustain: 0.15, failure: 0.15 }, |
| tab: "overview" |
| }; |
| |
| function num(v, d=1) { return Number(v).toFixed(d); } |
| function pct(v) { return `${(100 * v).toFixed(0)}%`; } |
| |
| function filteredTable() { |
| return DATA.table.filter(r => |
| state.families.includes(r.family) && |
| r.cost_usd_kg <= state.maxCost && |
| r.uts_mpa >= state.minUts && |
| r.density_g_cm3 <= state.maxDensity |
| ); |
| } |
| |
| function filteredScatter() { |
| return DATA.scatter.filter(r => |
| state.families.includes(r.family) && |
| r.cost_usd_kg <= state.maxCost && |
| r.uts_mpa >= state.minUts && |
| r.density_g_cm3 <= state.maxDensity |
| ); |
| } |
| |
| function familySummary() { |
| return DATA.family_summary.filter(r => state.families.includes(r.family)); |
| } |
| |
| function moScore(r) { |
| const w = state.weights; |
| const tot = w.crash + w.weight + w.cost + w.sustain + w.failure || 1; |
| return ( |
| (w.crash / tot) * r.crashworthiness_index + |
| (w.weight / tot) * r.lightweighting_score + |
| (w.cost / tot) * Math.min(r.cost_performance_score * 1.2, 100) + |
| (w.sustain / tot) * r.sustainability_score + |
| (w.failure / tot) * (100 * (1 - r.failure_risk)) |
| ); |
| } |
| |
| function rankedMaterials(n = 8) { |
| return [...filteredTable()] |
| .map(r => ({ ...r, mo_score: moScore(r) })) |
| .sort((a, b) => b.mo_score - a.mo_score) |
| .slice(0, n); |
| } |
| |
| function scenarioRecs() { |
| const key = `${state.scenario}||${state.component}`; |
| let rows = (DATA.recommendations[key] || []).filter(r => state.families.includes(r.family)); |
| if (!rows.length) { |
| rows = rankedMaterials(5).map(r => ({ |
| material_name: r.material_name, |
| family: r.family, |
| joining_method: "Hybrid Weld-Bond", |
| crash_score: r.crashworthiness_index, |
| energy_absorption_kj: r.energy_absorption_potential * 2, |
| intrusion_mm: 40, |
| peak_force_kn: r.uts_mpa * 0.03, |
| crush_force_efficiency: 0.65, |
| weight_reduction_pct: r.lightweighting_score * 0.4, |
| cost_score: r.cost_performance_score, |
| sustainability_score: r.sustainability_score, |
| simulation_risk: r.failure_risk * 100, |
| thickness_mm: 2.0 |
| })); |
| } |
| return rows.slice(0, 5); |
| } |
| |
| function plotBarH(div, rows, xKey, yKey, title, colorScale = false) { |
| const sorted = [...rows].sort((a, b) => a[xKey] - b[xKey]); |
| const trace = { |
| type: "bar", |
| orientation: "h", |
| x: sorted.map(r => r[xKey]), |
| y: sorted.map(r => r[yKey]), |
| marker: colorScale |
| ? { color: sorted.map(r => r[xKey]), colorscale: [[0, "#D8F3DC"], [1, "#0B6E4F"]], showscale: false } |
| : { color: "#0B6E4F" }, |
| hovertemplate: `%{y}<br>${xKey}: %{x:.2f}<extra></extra>` |
| }; |
| Plotly.newPlot(div, [trace], { ...LAYOUT, title, height: 430 }, PLOT_CONFIG); |
| } |
| |
| function plotScatter(div, rows, x, y, color, size, title) { |
| const families = [...new Set(rows.map(r => r[color]))]; |
| const traces = families.map((f, i) => { |
| const sub = rows.filter(r => r[color] === f); |
| return { |
| type: "scatter", |
| mode: "markers", |
| name: f, |
| x: sub.map(r => r[x]), |
| y: sub.map(r => r[y]), |
| text: sub.map(r => r.material_name || ""), |
| marker: { |
| size: sub.map(r => size ? Math.max(7, Math.min(22, (r[size] || 10) / 80)) : 9), |
| color: COLORS[i % COLORS.length], |
| opacity: 0.75, |
| line: { width: 0.5, color: "#fff" } |
| }, |
| hovertemplate: "%{text}<br>%{xaxis.title.text}: %{x:.1f}<br>%{yaxis.title.text}: %{y:.1f}<extra>" + f + "</extra>" |
| }; |
| }); |
| Plotly.newPlot(div, traces, { |
| ...LAYOUT, |
| title, |
| height: 430, |
| xaxis: { title: x.replaceAll("_", " ") }, |
| yaxis: { title: y.replaceAll("_", " ") } |
| }, PLOT_CONFIG); |
| } |
| |
| function plotRadar(div, rows) { |
| const cats = ["Crash", "Weight", "Cost-Perf", "Sustainability", "Energy Abs."]; |
| const traces = rows.slice(0, 5).map((r, i) => { |
| const vals = [ |
| r.crashworthiness_index, |
| r.lightweighting_score, |
| Math.min(r.cost_performance_score * 1.5, 100), |
| r.sustainability_score, |
| Math.min(r.energy_absorption_potential * 2, 100) |
| ]; |
| vals.push(vals[0]); |
| return { |
| type: "scatterpolar", |
| r: vals, |
| theta: [...cats, cats[0]], |
| fill: "toself", |
| name: r.material_name, |
| opacity: 0.55, |
| line: { color: COLORS[i % COLORS.length], width: 2 } |
| }; |
| }); |
| Plotly.newPlot(div, traces, { |
| ...LAYOUT, |
| title: "Multi-Objective Material Comparison", |
| height: 460, |
| polar: { |
| bgcolor: "#f8fafc", |
| radialaxis: { visible: true, range: [0, 100], gridcolor: "#e5e7eb" }, |
| angularaxis: { gridcolor: "#e5e7eb" } |
| } |
| }, PLOT_CONFIG); |
| } |
| |
| function plotHeatmap(div) { |
| const rows = DATA.heatmap.filter(r => state.families.includes(r.family)); |
| const scenarios = [...new Set(rows.map(r => r.crash_scenario))]; |
| const families = [...new Set(rows.map(r => r.family))]; |
| const z = scenarios.map(s => families.map(f => { |
| const hit = rows.find(r => r.crash_scenario === s && r.family === f); |
| return hit ? hit.crash_score : null; |
| })); |
| Plotly.newPlot(div, [{ |
| type: "heatmap", |
| z, x: families, y: scenarios, |
| colorscale: [[0, "#F1FAEE"], [0.5, "#1B4965"], [1, "#0B6E4F"]], |
| hoverongaps: false, |
| colorbar: { title: "Crash Score" } |
| }], { ...LAYOUT, title: "Avg Crash Score by Scenario × Family", height: 520 }, PLOT_CONFIG); |
| } |
| |
| function plotGauge(div, value, title, color) { |
| Plotly.newPlot(div, [{ |
| type: "indicator", |
| mode: "gauge+number", |
| value, |
| title: { text: title, font: { size: 14, color: "#1a1a1a" } }, |
| number: { font: { color: "#1a1a1a" } }, |
| gauge: { |
| axis: { range: [0, 100] }, |
| bar: { color }, |
| bgcolor: "#f1f5f9", |
| bordercolor: "#cbd5e1", |
| steps: [ |
| { range: [0, 40], color: "#fee2e2" }, |
| { range: [40, 70], color: "#fef3c7" }, |
| { range: [70, 100], color: "#dcfce7" } |
| ] |
| } |
| }], { height: 220, margin: { t: 40, b: 10, l: 20, r: 20 }, paper_bgcolor: "#fff", font: { color: "#1a1a1a" } }, PLOT_CONFIG); |
| } |
| |
| function plotCurves(div, ids) { |
| const traces = []; |
| ids.forEach((id, i) => { |
| const c = DATA.curves[id]; |
| if (!c) return; |
| traces.push({ |
| type: "scatter", mode: "lines", name: `${c.material_name} (QS)`, |
| x: c.strain, y: c.stress_mpa, |
| line: { color: COLORS[i % COLORS.length], width: 2.5 } |
| }); |
| traces.push({ |
| type: "scatter", mode: "lines", name: `${c.material_name} (HR)`, |
| x: c.strain, y: c.stress_high_rate_mpa, |
| line: { color: COLORS[i % COLORS.length], width: 2, dash: "dash" } |
| }); |
| }); |
| Plotly.newPlot(div, traces, { |
| ...LAYOUT, |
| title: "Stress–Strain Curves (Strain-Rate Sensitive)", |
| height: 440, |
| xaxis: { title: "True Strain" }, |
| yaxis: { title: "True Stress (MPa)" } |
| }, PLOT_CONFIG); |
| } |
| |
| function plotValidation(divParity, divHist) { |
| const rows = DATA.validation.filter(r => state.families.includes(r.family)); |
| Plotly.newPlot(divParity, [ |
| { |
| type: "scatter", mode: "markers", name: "AI vs CAE", |
| x: rows.map(r => r.cae_crash_score), |
| y: rows.map(r => r.ai_crash_score), |
| marker: { color: "#1B4965", size: 7, opacity: 0.55 } |
| }, |
| { |
| type: "scatter", mode: "lines", name: "Ideal", |
| x: [0, 100], y: [0, 100], |
| line: { color: "#C44536", dash: "dash", width: 2 } |
| } |
| ], { ...LAYOUT, title: "AI Prediction vs CAE Validation", height: 420, xaxis: { title: "CAE Crash Score" }, yaxis: { title: "AI Crash Score" } }, PLOT_CONFIG); |
| |
| const pass = rows.filter(r => r.pass_fail === "Pass").map(r => r.ai_cae_error_pct); |
| const review = rows.filter(r => r.pass_fail !== "Pass").map(r => r.ai_cae_error_pct); |
| Plotly.newPlot(divHist, [ |
| { type: "histogram", x: pass, name: "Pass", marker: { color: "#0B6E4F" }, opacity: 0.85 }, |
| { type: "histogram", x: review, name: "Review", marker: { color: "#C44536" }, opacity: 0.85 } |
| ], { ...LAYOUT, barmode: "overlay", title: "AI–CAE Error Distribution", height: 420, xaxis: { title: "AI–CAE Error (%)" } }, PLOT_CONFIG); |
| } |
| |
| function materialCard(mat, solver) { |
| const n = 10; |
| const strains = Array.from({ length: n }, (_, i) => i * mat.failure_strain / (n - 1)); |
| const stresses = strains.map((eps, i) => { |
| const t = i / (n - 1); |
| return mat.yield_strength_mpa + (mat.uts_mpa - mat.yield_strength_mpa) * t; |
| }); |
| const cardType = mat.material_card_type || "MAT_024"; |
| const pr = (mat.family || "").includes("Aluminum") || mat.family === "Magnesium" ? 0.30 : 0.29; |
| let text = `*KEYWORD (${solver} draft)\n`; |
| text += `$ Material: ${mat.material_name} (${mat.family})\n`; |
| text += `$ Card type: ${cardType}\n`; |
| text += `$ Confidence: ${num(mat.confidence_score, 2)}\n`; |
| text += `$ Validation: Draft — public-data prototype\n`; |
| text += `*MAT_PIECEWISE_LINEAR_PLASTICITY\n`; |
| text += `$ RO (g/cm3) = ${mat.density_g_cm3}\n`; |
| text += `$ E (GPa) = ${mat.youngs_modulus_gpa}\n`; |
| text += `$ PR = ${pr}\n`; |
| text += `$ SIGY (MPa) = ${mat.yield_strength_mpa}\n`; |
| text += `$ FAIL = ${mat.failure_strain}\n`; |
| text += `$ C (strain-rate) = ${mat.strain_rate_sensitivity}\n`; |
| text += `$ Plastic curve (strain, stress MPa):\n`; |
| strains.forEach((e, i) => { text += `$ ${e.toFixed(5)}, ${stresses[i].toFixed(2)}\n`; }); |
| text += `$ Damage: Linear softening to zero stress at failure strain\n`; |
| text += `$ Temperature: Room-temperature card; scale factors TBD\n*END`; |
| return { text, strains, stresses, cardType }; |
| } |
| |
| function tableHTML(rows, cols) { |
| if (!rows.length) return "<p>No rows match the current filters.</p>"; |
| const head = cols.map(c => `<th>${c.label}</th>`).join(""); |
| const body = rows.map(r => `<tr>${cols.map(c => `<td>${typeof r[c.key] === "number" ? num(r[c.key], c.digits ?? 2) : (r[c.key] ?? "")}</td>`).join("")}</tr>`).join(""); |
| return `<div class="table-wrap"><table class="data"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`; |
| } |
| |
| function renderShell() { |
| const families = DATA.meta.families; |
| if (!state.families.length) state.families = families.slice(); |
| |
| document.getElementById("app").className = ""; |
| document.getElementById("app").innerHTML = ` |
| <div class="layout"> |
| <aside class="sidebar"> |
| <h2>Filters & Targets</h2> |
| <label>Material families</label> |
| <div class="family-box" id="familyBox"></div> |
| <label>Crash scenario</label> |
| <select id="scenario"></select> |
| <label>Vehicle component</label> |
| <select id="component"></select> |
| <label>Max cost (USD/kg): <span id="maxCostVal">${state.maxCost}</span></label> |
| <input type="range" id="maxCost" min="1" max="80" step="1" value="${state.maxCost}" /> |
| <label>Min UTS (MPa): <span id="minUtsVal">${state.minUts}</span></label> |
| <input type="range" id="minUts" min="20" max="1800" step="20" value="${state.minUts}" /> |
| <label>Max density (g/cm³): <span id="maxDensityVal">${state.maxDensity}</span></label> |
| <input type="range" id="maxDensity" min="0.1" max="8" step="0.1" value="${state.maxDensity}" /> |
| <h2 style="margin-top:1.2rem">Multi-objective weights</h2> |
| <label>Crash <span class="weight-val" id="wCrashVal">${state.weights.crash.toFixed(2)}</span></label> |
| <input type="range" id="wCrash" min="0" max="1" step="0.05" value="${state.weights.crash}" /> |
| <label>Lightweighting <span class="weight-val" id="wWeightVal">${state.weights.weight.toFixed(2)}</span></label> |
| <input type="range" id="wWeight" min="0" max="1" step="0.05" value="${state.weights.weight}" /> |
| <label>Cost <span class="weight-val" id="wCostVal">${state.weights.cost.toFixed(2)}</span></label> |
| <input type="range" id="wCost" min="0" max="1" step="0.05" value="${state.weights.cost}" /> |
| <label>Sustainability <span class="weight-val" id="wSustVal">${state.weights.sustain.toFixed(2)}</span></label> |
| <input type="range" id="wSust" min="0" max="1" step="0.05" value="${state.weights.sustain}" /> |
| <label>Low failure risk <span class="weight-val" id="wFailVal">${state.weights.failure.toFixed(2)}</span></label> |
| <input type="range" id="wFail" min="0" max="1" step="0.05" value="${state.weights.failure}" /> |
| <p class="caption" id="dbCaption"></p> |
| </aside> |
| <main class="main"> |
| <div class="hero"> |
| <h1>Crash Intelligence Platform</h1> |
| <p>AI-powered material recommendation, prediction, and CAE card generation for automotive crash-performance across steel, aluminum, magnesium, composites, polymers, foams, elastomers, and adhesives.</p> |
| </div> |
| <div class="tabs" id="tabs"> |
| <button class="tab-btn" data-tab="overview">Overview</button> |
| <button class="tab-btn" data-tab="explorer">Material Explorer</button> |
| <button class="tab-btn" data-tab="scenario">Crash Scenario AI</button> |
| <button class="tab-btn" data-tab="compare">Compare & Rank</button> |
| <button class="tab-btn" data-tab="cards">Material Cards</button> |
| <button class="tab-btn" data-tab="validation">Validation</button> |
| <button class="tab-btn" data-tab="library">Data Library</button> |
| </div> |
| <div id="content"></div> |
| <div class="footer">Crash Intelligence Platform · Public-style material & crash databases · Calibrate with supplier cards and full-vehicle CAE for OEM production use.</div> |
| </main> |
| </div>`; |
| |
| const famBox = document.getElementById("familyBox"); |
| famBox.innerHTML = families.map(f => ` |
| <label><input type="checkbox" value="${f}" ${state.families.includes(f) ? "checked" : ""}/> ${f}</label> |
| `).join(""); |
| |
| const scen = document.getElementById("scenario"); |
| scen.innerHTML = DATA.meta.scenarios.map(s => `<option ${s === state.scenario ? "selected" : ""}>${s}</option>`).join(""); |
| const comp = document.getElementById("component"); |
| comp.innerHTML = DATA.meta.components.map(s => `<option ${s === state.component ? "selected" : ""}>${s}</option>`).join(""); |
| |
| document.getElementById("dbCaption").textContent = |
| `Database: ${DATA.meta.n_materials.toLocaleString()} materials · ${DATA.meta.n_recommendations.toLocaleString()} scenario predictions · ${DATA.meta.n_validation.toLocaleString()} validation pairs`; |
| |
| bindControls(); |
| setTab(state.tab); |
| } |
| |
| function bindControls() { |
| document.querySelectorAll("#familyBox input").forEach(cb => { |
| cb.addEventListener("change", () => { |
| state.families = [...document.querySelectorAll("#familyBox input:checked")].map(x => x.value); |
| if (!state.families.length) state.families = DATA.meta.families.slice(); |
| renderActive(); |
| }); |
| }); |
| const map = [ |
| ["scenario", v => state.scenario = v], |
| ["component", v => state.component = v], |
| ]; |
| map.forEach(([id, fn]) => document.getElementById(id).addEventListener("change", e => { fn(e.target.value); renderActive(); })); |
| |
| const ranges = [ |
| ["maxCost", v => { state.maxCost = +v; document.getElementById("maxCostVal").textContent = v; }], |
| ["minUts", v => { state.minUts = +v; document.getElementById("minUtsVal").textContent = v; }], |
| ["maxDensity", v => { state.maxDensity = +v; document.getElementById("maxDensityVal").textContent = v; }], |
| ["wCrash", v => { state.weights.crash = +v; document.getElementById("wCrashVal").textContent = (+v).toFixed(2); }], |
| ["wWeight", v => { state.weights.weight = +v; document.getElementById("wWeightVal").textContent = (+v).toFixed(2); }], |
| ["wCost", v => { state.weights.cost = +v; document.getElementById("wCostVal").textContent = (+v).toFixed(2); }], |
| ["wSust", v => { state.weights.sustain = +v; document.getElementById("wSustVal").textContent = (+v).toFixed(2); }], |
| ["wFail", v => { state.weights.failure = +v; document.getElementById("wFailVal").textContent = (+v).toFixed(2); }], |
| ]; |
| ranges.forEach(([id, fn]) => { |
| const el = document.getElementById(id); |
| el.addEventListener("input", e => fn(e.target.value)); |
| el.addEventListener("change", () => renderActive()); |
| }); |
| |
| document.querySelectorAll(".tab-btn").forEach(btn => { |
| btn.addEventListener("click", () => setTab(btn.dataset.tab)); |
| }); |
| } |
| |
| function setTab(tab) { |
| state.tab = tab; |
| document.querySelectorAll(".tab-btn").forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); |
| renderActive(); |
| } |
| |
| function renderActive() { |
| const root = document.getElementById("content"); |
| const filt = filteredTable(); |
| const avg = (key) => filt.length ? filt.reduce((s, r) => s + r[key], 0) / filt.length : 0; |
| |
| if (state.tab === "overview") { |
| root.innerHTML = ` |
| <div class="metrics"> |
| <div class="metric"><div class="label">Materials</div><div class="value">${filt.length.toLocaleString()}</div></div> |
| <div class="metric"><div class="label">Avg Crash Index</div><div class="value">${num(avg("crashworthiness_index"))}</div></div> |
| <div class="metric"><div class="label">Avg Energy Potential</div><div class="value">${num(avg("energy_absorption_potential"), 2)}</div></div> |
| <div class="metric"><div class="label">Avg Sustainability</div><div class="value">${num(avg("sustainability_score"))}</div></div> |
| <div class="metric"><div class="label">AI–CAE Pass Rate</div><div class="value">${pct(DATA.meta.pass_rate)}</div></div> |
| </div> |
| <div class="grid-3"> |
| <div class="card"><div id="g1"></div></div> |
| <div class="card"><div id="g2"></div></div> |
| <div class="card"><div id="g3"></div></div> |
| </div> |
| <div class="grid-2"> |
| <div class="card"><div id="p1"></div></div> |
| <div class="card"><div id="p2"></div></div> |
| </div> |
| <div class="card"><div id="p3"></div></div> |
| <div class="note"><strong>Value proposition:</strong> Shortlist safer, lighter, cheaper, and more sustainable crash-critical materials before expensive CAE and physical testing — then export draft solver-ready material cards for LS-DYNA, Abaqus, PAM-CRASH, and Radioss.</div> |
| `; |
| plotGauge("g1", avg("crashworthiness_index"), "Crashworthiness", "#0B6E4F"); |
| plotGauge("g2", avg("lightweighting_score"), "Lightweighting", "#1B4965"); |
| plotGauge("g3", avg("sustainability_score"), "Sustainability", "#2A9D8F"); |
| plotBarH("p1", familySummary(), "crash", "family", "Crashworthiness by Family", true); |
| plotScatter("p2", filteredScatter(), "lightweighting_score", "crashworthiness_index", "family", "uts_mpa", "Crashworthiness vs Lightweighting"); |
| plotHeatmap("p3"); |
| return; |
| } |
| |
| if (state.tab === "explorer") { |
| const curveIds = Object.keys(DATA.curves).filter(id => state.families.includes(DATA.curves[id].family)).slice(0, 4); |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Material Data Explorer</h3> |
| <p style="color:var(--muted)">Browse standardized mechanical, cost, and sustainability properties across automotive crash materials.</p> |
| <div class="grid-2"> |
| <div class="card"><div id="e1"></div></div> |
| <div class="card"><div id="e2"></div></div> |
| </div> |
| <div class="card"><div id="e3"></div></div> |
| ${tableHTML(filt.slice(0, 120), [ |
| {key:"material_name", label:"Material"}, |
| {key:"family", label:"Family"}, |
| {key:"density_g_cm3", label:"Density"}, |
| {key:"youngs_modulus_gpa", label:"E (GPa)"}, |
| {key:"yield_strength_mpa", label:"Yield"}, |
| {key:"uts_mpa", label:"UTS"}, |
| {key:"elongation_pct", label:"Elong %"}, |
| {key:"cost_usd_kg", label:"Cost"}, |
| {key:"crashworthiness_index", label:"Crash"}, |
| {key:"sustainability_score", label:"Sustain"}, |
| {key:"source", label:"Source"}, |
| {key:"confidence_score", label:"Conf", digits:2} |
| ])} |
| `; |
| plotBarH("e1", familySummary(), "energy", "family", "Energy Absorption Potential", true); |
| plotScatter("e2", filteredScatter(), "cost_usd_kg", "sustainability_score", "family", "crashworthiness_index", "Cost vs Sustainability"); |
| plotCurves("e3", curveIds); |
| return; |
| } |
| |
| if (state.tab === "scenario") { |
| const top = scenarioRecs(); |
| const best = top[0] || {}; |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Crash Scenario Intelligence</h3> |
| <p style="color:var(--muted)">Recommendations for <strong>${state.scenario}</strong> on <strong>${state.component}</strong>.</p> |
| <div class="metrics"> |
| <div class="metric"><div class="label">Top Crash Score</div><div class="value">${num(best.crash_score || 0)}</div></div> |
| <div class="metric"><div class="label">Best Weight Reduction</div><div class="value">${num(best.weight_reduction_pct || 0)}%</div></div> |
| <div class="metric"><div class="label">Top Material</div><div class="value" style="font-size:1.05rem">${best.material_name || "—"}</div></div> |
| <div class="metric"><div class="label">Family</div><div class="value" style="font-size:1.05rem">${best.family || "—"}</div></div> |
| <div class="metric"><div class="label">Joining</div><div class="value" style="font-size:0.95rem">${best.joining_method || "—"}</div></div> |
| </div> |
| <div class="grid-2"> |
| <div class="card"><div id="s1"></div></div> |
| <div class="card"><div id="s2"></div></div> |
| </div> |
| <div class="card"><div id="s3"></div></div> |
| ${tableHTML(top, [ |
| {key:"material_name", label:"Material"}, |
| {key:"family", label:"Family"}, |
| {key:"thickness_mm", label:"Thickness"}, |
| {key:"joining_method", label:"Joining"}, |
| {key:"crash_score", label:"Crash"}, |
| {key:"energy_absorption_kj", label:"Energy"}, |
| {key:"intrusion_mm", label:"Intrusion"}, |
| {key:"weight_reduction_pct", label:"ΔWeight %"}, |
| {key:"simulation_risk", label:"Sim Risk"} |
| ])} |
| <div class="note"><strong>Recommended action:</strong> Evaluate <em>${best.material_name || "candidate"}</em> |
| (${best.family || ""}) at ~${num(best.thickness_mm || 2, 2)} mm with <em>${best.joining_method || "hybrid joining"}</em>. |
| Expected crash score ${num(best.crash_score || 0)}. Run component-level ${state.scenario.toLowerCase()} CAE before physical validation.</div> |
| `; |
| plotBarH("s1", top.map(r => ({ family: r.material_name, crash: r.crash_score })), "crash", "family", "Top Recommended Materials", true); |
| plotScatter("s2", DATA.energy_intrusion.filter(r => state.families.includes(r.family)), "intrusion_mm", "energy_absorption_kj", "crash_scenario", null, "Energy Absorption vs Intrusion"); |
| plotBarH("s3", DATA.scenario_kpi.map(r => ({ family: r.crash_scenario, crash: r.avg_crash_score })), "crash", "family", "Average Crash Score by Scenario", true); |
| return; |
| } |
| |
| if (state.tab === "compare") { |
| const ranked = rankedMaterials(8); |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Material Comparison & Ranking</h3> |
| <div class="grid-2"> |
| <div class="card"><div id="c1"></div></div> |
| <div class="card"><div id="c2"></div></div> |
| </div> |
| ${tableHTML(ranked, [ |
| {key:"material_name", label:"Material"}, |
| {key:"family", label:"Family"}, |
| {key:"mo_score", label:"MO Score"}, |
| {key:"crashworthiness_index", label:"Crash"}, |
| {key:"lightweighting_score", label:"Weight"}, |
| {key:"cost_performance_score", label:"Cost-Perf"}, |
| {key:"sustainability_score", label:"Sustain"}, |
| {key:"failure_risk", label:"Fail Risk", digits:3}, |
| {key:"uts_mpa", label:"UTS"}, |
| {key:"density_g_cm3", label:"Density"}, |
| {key:"cost_usd_kg", label:"Cost"} |
| ])} |
| `; |
| plotRadar("c1", ranked); |
| plotBarH("c2", ranked.map(r => ({ family: r.material_name, crash: r.mo_score })), "crash", "family", "Multi-Objective Score (Top Materials)", true); |
| return; |
| } |
| |
| if (state.tab === "cards") { |
| const ranked = rankedMaterials(20); |
| const options = ranked.length ? ranked : filt.slice(0, 20); |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">CAE Material Card Generator</h3> |
| <p style="color:var(--muted)">Generate draft solver-ready material cards with elastic modulus, yield, plastic curve, strain-rate sensitivity, failure strain, and confidence score.</p> |
| <div class="controls-row"> |
| <div><label>Select material</label><select id="cardMat">${options.map(r => `<option value="${r.material_name}">${r.material_name} (${r.family})</option>`).join("")}</select></div> |
| <div><label>Target solver</label><select id="cardSolver">${DATA.solvers.map(s => `<option>${s}</option>`).join("")}</select></div> |
| <div><label> </label><button id="dlCard">Download card</button></div> |
| </div> |
| <div class="metrics" id="cardMetrics"></div> |
| <div class="grid-2"> |
| <pre class="card-text" id="cardText"></pre> |
| <div class="card"><div id="cardPlot"></div><div class="note" id="cardNote"></div></div> |
| </div> |
| `; |
| const updateCard = () => { |
| const name = document.getElementById("cardMat").value; |
| const solver = document.getElementById("cardSolver").value; |
| const mat = options.find(r => r.material_name === name) || options[0]; |
| if (!mat) return; |
| const card = materialCard(mat, solver); |
| document.getElementById("cardText").textContent = card.text; |
| document.getElementById("cardMetrics").innerHTML = ` |
| <div class="metric"><div class="label">Card Type</div><div class="value" style="font-size:1rem">${card.cardType.split(" ")[0]}</div></div> |
| <div class="metric"><div class="label">Yield (MPa)</div><div class="value">${num(mat.yield_strength_mpa, 0)}</div></div> |
| <div class="metric"><div class="label">Failure Strain</div><div class="value">${num(mat.failure_strain, 3)}</div></div> |
| <div class="metric"><div class="label">Confidence</div><div class="value">${num(mat.confidence_score, 2)}</div></div> |
| <div class="metric"><div class="label">UTS (MPa)</div><div class="value">${num(mat.uts_mpa, 0)}</div></div>`; |
| document.getElementById("cardNote").innerHTML = `<strong>Validation status:</strong> Draft — public-data prototype<br/><strong>Damage model:</strong> Linear softening to zero stress at failure strain<br/><strong>Temperature:</strong> Room-temperature card; scale factors TBD`; |
| if (DATA.curves[mat.material_id]) { |
| plotCurves("cardPlot", [mat.material_id]); |
| } else { |
| Plotly.newPlot("cardPlot", [{ |
| type: "scatter", mode: "lines+markers", |
| x: card.strains, y: card.stresses, |
| line: { color: "#0B6E4F", width: 3 }, name: "Plastic curve" |
| }], { ...LAYOUT, title: "Draft Plastic Curve", height: 380, xaxis: { title: "Plastic Strain" }, yaxis: { title: "Stress (MPa)" } }, PLOT_CONFIG); |
| } |
| document.getElementById("dlCard").onclick = () => { |
| const blob = new Blob([card.text], { type: "text/plain" }); |
| const a = document.createElement("a"); |
| a.href = URL.createObjectURL(blob); |
| a.download = `${mat.material_name}_${solver.replaceAll(" ", "_")}.k`; |
| a.click(); |
| }; |
| }; |
| document.getElementById("cardMat").addEventListener("change", updateCard); |
| document.getElementById("cardSolver").addEventListener("change", updateCard); |
| updateCard(); |
| return; |
| } |
| |
| if (state.tab === "validation") { |
| const rows = DATA.validation.filter(r => state.families.includes(r.family)); |
| const meanErr = rows.length ? rows.reduce((s, r) => s + r.ai_cae_error_pct, 0) / rows.length : 0; |
| const passRate = rows.length ? rows.filter(r => r.pass_fail === "Pass").length / rows.length : 0; |
| const meanStar = rows.length ? rows.reduce((s, r) => s + r.nhtsa_star_proxy, 0) / rows.length : 0; |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Validation Workflow</h3> |
| <p style="color:var(--muted)">Compare AI predictions with CAE and physical crash proxies aligned to Euro NCAP / FMVSS / IIHS.</p> |
| <div class="metrics"> |
| <div class="metric"><div class="label">Validation pairs</div><div class="value">${rows.length.toLocaleString()}</div></div> |
| <div class="metric"><div class="label">Mean AI–CAE error</div><div class="value">${num(meanErr)}%</div></div> |
| <div class="metric"><div class="label">Pass rate</div><div class="value">${pct(passRate)}</div></div> |
| <div class="metric"><div class="label">Mean NHTSA-star proxy</div><div class="value">${num(meanStar)}</div></div> |
| <div class="metric"><div class="label">Standards</div><div class="value" style="font-size:0.95rem">NCAP/FMVSS</div></div> |
| </div> |
| <div class="grid-2"> |
| <div class="card"><div id="v1"></div></div> |
| <div class="card"><div id="v2"></div></div> |
| </div> |
| <div class="note"><strong>Coupon tests:</strong> Tensile, compression, shear, strain-rate, fracture.</div> |
| <div class="note"><strong>Component tests:</strong> Bumper beam, crash box, rail, door beam, battery enclosure.</div> |
| <div class="note"><strong>CAE validation:</strong> Compare AI prediction with LS-DYNA / Abaqus / PAM-CRASH.</div> |
| <div class="note"><strong>Physical crash:</strong> Compare simulation with crash-test measurements.</div> |
| <div class="note"><strong>Certification:</strong> Euro NCAP, FMVSS, IIHS, OEM internal standards.</div> |
| ${tableHTML(rows.slice(0, 150), [ |
| {key:"family", label:"Family"}, |
| {key:"crash_scenario", label:"Scenario"}, |
| {key:"ai_crash_score", label:"AI"}, |
| {key:"cae_crash_score", label:"CAE"}, |
| {key:"physical_crash_score", label:"Physical"}, |
| {key:"ai_cae_error_pct", label:"Error %"}, |
| {key:"pass_fail", label:"Status"}, |
| {key:"nhtsa_star_proxy", label:"Stars", digits:0}, |
| {key:"standard", label:"Standard"} |
| ])} |
| `; |
| plotValidation("v1", "v2"); |
| return; |
| } |
| |
| |
| root.innerHTML = ` |
| <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Data Library & Export</h3> |
| <p style="color:var(--muted)">Public-style material and crash datasets used by the ranking and card-generation engines.</p> |
| <div class="controls-row"> |
| <div><label>Dataset</label> |
| <select id="libSet"> |
| <option value="table">materials</option> |
| <option value="validation">validation</option> |
| <option value="top_materials">top_materials</option> |
| <option value="scenario_kpi">scenario_kpi</option> |
| </select> |
| </div> |
| <div><label> </label><button id="dlCsv">Download CSV</button></div> |
| </div> |
| <div id="libTable"></div> |
| `; |
| const renderLib = () => { |
| const key = document.getElementById("libSet").value; |
| let rows = DATA[key]; |
| if (key === "table" || key === "validation") rows = rows.filter(r => !r.family || state.families.includes(r.family)); |
| const cols = Object.keys(rows[0] || {}).slice(0, 12).map(k => ({ key: k, label: k, digits: 3 })); |
| document.getElementById("libTable").innerHTML = tableHTML(rows.slice(0, 250), cols); |
| document.getElementById("dlCsv").onclick = () => { |
| const header = Object.keys(rows[0] || {}); |
| const csv = [header.join(",")].concat(rows.map(r => header.map(h => JSON.stringify(r[h] ?? "")).join(","))).join("\n"); |
| const a = document.createElement("a"); |
| a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" })); |
| a.download = `${key}.csv`; |
| a.click(); |
| }; |
| }; |
| document.getElementById("libSet").addEventListener("change", renderLib); |
| renderLib(); |
| } |
| |
| fetch("./data/dashboard_payload.json") |
| .then(r => { |
| if (!r.ok) throw new Error("Failed to load dashboard payload"); |
| return r.json(); |
| }) |
| .then(json => { |
| DATA = json; |
| state.families = json.meta.families.slice(); |
| state.scenario = json.meta.scenarios.includes("Side Impact") ? "Side Impact" : json.meta.scenarios[0]; |
| state.component = json.meta.components.includes("Battery Enclosure") ? "Battery Enclosure" : json.meta.components[0]; |
| renderShell(); |
| }) |
| .catch(err => { |
| document.getElementById("app").innerHTML = `<div class="loading" style="color:#C44536">Failed to load dashboard: ${err.message}</div>`; |
| }); |
| </script> |
| </body> |
| </html> |
|
|