Crash-Intelligence / index.html
shrut27's picture
Upload index.html with huggingface_hub
62e9d8d verified
Raw
History Blame Contribute Delete
42.8 kB
<!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>&nbsp;</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;
}
// library
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>&nbsp;</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>