/* ===================================================================
KabelFlux v5 — BOM view + charts (L4 fix)
- Donut: wires by gauge
- Horizontal bars: cable-type distribution
- KPI strip + line-item table
=================================================================== */
/* — Reusable donut chart — */
const Donut = ({ data, size=180, thickness=22, label }) => {
const total = data.reduce((a, b) => a + b.value, 0);
const r = (size - thickness) / 2;
const c = 2 * Math.PI * r;
let acc = 0;
return (
{data.map((d, i) => (
{d.name}
{d.value}
{Math.round((d.value/total)*100)}%
))}
);
};
/* — Horizontal bars — */
const BarRows = ({ data, max, format=(v=>v), unit='' }) => {
const M = max || Math.max(...data.map(d => d.value));
return (
{data.map((d, i) => (
{d.name}
{format(d.value)}{unit}
))}
);
};
/* — BOM view — */
const BomView = ({ wires=[], connectors=[], nodes=[], bundles=[], realPid=null, onExport }) => {
const gaugeData = useMemo(() => {
const map = {};
wires.forEach(w => { map[w.gauge] = (map[w.gauge] || 0) + 1; });
const palette = ['#4a9eff','#00d4aa','#f5a623','#a78bfa','#ec4899','#ef4e4e'];
return Object.entries(map).sort((a,b) => b[1]-a[1]).map(([name, value], i) => ({ name, value, color: palette[i % palette.length] }));
}, [wires]);
const cableData = useMemo(() => {
const map = {};
wires.forEach(w => { map[w.cable] = (map[w.cable] || 0) + 1; });
return Object.entries(map).sort((a,b)=>b[1]-a[1]).map(([name, value]) => ({ name, value }));
}, [wires]);
const colorData = useMemo(() => {
const map = {};
wires.forEach(w => { map[w.color] = (map[w.color] || 0) + 1; });
return Object.entries(map).sort((a,b)=>b[1]-a[1]).slice(0,8).map(([name,value]) => ({ name, value, color: COLOR_HEX[name] }));
}, [wires]);
const lengthTotal = wires.reduce((a, w) => a + w.length, 0);
const totalShield = wires.filter(w => w.shield).length;
return (
Copy summary
onExport && onExport('bom')}>Export PDF
onExport && onExport('bom-xlsx')}>Export XLSX
>
}/>
{/* KPI strip */}
{/* Charts row */}
Wires by gauge
{wires.length} total
Cable type distribution
{cableData.length} types
Length by bundle
Active: L1
({ name: `${b.label} · ${b.count}w`, value: b.length, color: 'var(--info)' }))}
unit=" mm"/>
{/* Connector schedule + Node schedule */}
Connector schedule
{connectors.length}
| Ref | Type | Gender | Pins | Wired | Description |
{connectors.map(c => {
const wired = wires.filter(w => w.fromConn === c.id || w.toConn === c.id).length;
return (
| {c.id} |
{c.type} |
{c.gender === 'M' ? 'Male' : 'Female'} |
{c.pins} |
{wired}/{c.pins} |
{c.label.split(' — ')[1]} |
);
})}
Node schedule
{nodes.length}
| Ref | Type | Value | Coordinates |
{nodes.map(n => (
| {n.label} |
{n.type === 'splice' ? 'Splice' : n.type === 'fuse' ? 'Fuse' : 'Ground'} |
{n.value || '—'} |
{n.x}, {n.y} |
))}
);
};
const KPI = ({ label, value, sub, delta, tone='default' }) => (
{label}
{value}
{sub &&
{sub}
}
{delta &&
{delta}
}
);
const STYLE = `
.kf-bom-scroll { flex: 1; overflow-y: auto; padding: 18px; }
.kf-kpi-strip {
display: grid; grid-template-columns: repeat(6, 1fr); gap: 10px;
margin-bottom: 18px;
}
@media (max-width: 1200px) { .kf-kpi-strip { grid-template-columns: repeat(3, 1fr); } }
.kf-kpi {
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: var(--r-3);
padding: 14px 16px;
}
.kf-kpi-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1px; color: var(--t-3); font-weight: 700; margin-bottom: 8px; }
.kf-kpi-value { font-family: var(--f-display); font-size: 28px; font-weight: 700; color: var(--t-1); line-height: 1; letter-spacing: -.5px; }
.kf-kpi-sub { font-size: 11px; color: var(--t-3); margin-top: 6px; }
.kf-kpi-delta { font-size: 10.5px; color: var(--info); margin-top: 4px; font-family: var(--f-mono); }
.kf-kpi-info .kf-kpi-value { color: var(--info); }
.kf-kpi-success .kf-kpi-value { color: var(--success); }
.kf-kpi-violet .kf-kpi-value { color: var(--violet); }
.kf-kpi-warning .kf-kpi-value { color: var(--warning); }
.kf-bom-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
margin-bottom: 14px;
}
.kf-bom-grid.two { grid-template-columns: 1.4fr 1fr; }
@media (max-width: 1100px) { .kf-bom-grid, .kf-bom-grid.two { grid-template-columns: 1fr; } }
/* Donut */
.kf-donut { display: flex; gap: 24px; align-items: center; }
.kf-donut-legend { flex: 1; display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.kf-donut-legend-row {
display: grid;
grid-template-columns: 14px 1fr auto 44px;
align-items: center; gap: 10px;
font-size: 12px;
padding: 4px 0;
}
.kf-donut-dot { width: 10px; height: 10px; border-radius: 2px; }
.kf-donut-label { color: var(--t-2); }
.kf-donut-val { font-weight: 600; color: var(--t-1); }
.kf-donut-pct { color: var(--t-3); text-align: right; }
/* Bars */
.kf-bars { display: flex; flex-direction: column; gap: 8px; }
.kf-bar-row {
display: grid; grid-template-columns: 140px 1fr 60px;
gap: 10px; align-items: center;
font-size: 12px;
}
.kf-bar-name { color: var(--t-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kf-bar-track {
height: 14px;
background: var(--bg-3);
border-radius: 2px;
overflow: hidden;
position: relative;
}
.kf-bar-fill {
height: 100%;
border-radius: 2px;
transition: width .35s var(--ease);
}
.kf-bar-val { color: var(--t-1); font-weight: 600; text-align: right; }
`;
document.head.appendChild(Object.assign(document.createElement('style'), { textContent: STYLE }));
window.BomView = BomView;
window.Donut = Donut;
window.BarRows = BarRows;
window.KPI = KPI;