Files
HomeAssistantVS/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts
T
2026-07-08 10:43:39 -04:00

188 lines
9.0 KiB
TypeScript

/** Weibull reliability analysis renderers. */
import { html, svg, nothing } from "lit";
import { t } from "../styles";
import type { MaintenanceTask } from "../types";
export function renderWeibullSection(task: MaintenanceTask, lang: string) {
const analysis = task.interval_analysis;
const beta = analysis?.weibull_beta;
const eta = analysis?.weibull_eta;
if (beta == null || eta == null || eta <= 0) return nothing;
const currentInterval = task.interval_days ?? 0;
const rec = task.suggested_interval ?? currentInterval;
return html`
<div class="weibull-section">
<div class="weibull-title">
<ha-svg-icon aria-hidden="true" path="M3,14L3.5,14.07L8.07,9.5C7.89,8.85 8.06,8.11 8.59,7.59C9.37,6.8 10.63,6.8 11.41,7.59C11.94,8.11 12.11,8.85 11.93,9.5L14.5,12.07L15,12C15.18,12 15.35,12 15.5,12.07L19.07,8.5C19,8.35 19,8.18 19,8A2,2 0 0,1 21,6A2,2 0 0,1 23,8A2,2 0 0,1 21,10C20.82,10 20.65,10 20.5,9.93L16.93,13.5C17,13.65 17,13.82 17,14A2,2 0 0,1 15,16A2,2 0 0,1 13,14L13.07,13.5L10.5,10.93C10.18,11 9.82,11 9.5,10.93L4.93,15.5L5,16A2,2 0 0,1 3,18A2,2 0 0,1 1,16A2,2 0 0,1 3,14Z"></ha-svg-icon>
${t("weibull_reliability_curve", lang)}
${renderBetaBadge(beta, lang)}
</div>
${renderWeibullChart(beta, eta, currentInterval, rec, lang)}
${renderWeibullInfo(analysis!, lang)}
${analysis?.confidence_interval_low != null ? renderConfidenceInterval(analysis!, task, lang) : nothing}
</div>
`;
}
function renderBetaBadge(beta: number, lang: string) {
let cls: string;
let icon: string;
let key: string;
if (beta < 0.8) {
cls = "early_failures";
icon = "M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z";
key = "beta_early_failures";
} else if (beta <= 1.2) {
cls = "random_failures";
icon = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,17H11V15H13V17M13,13H11V7H13V13Z";
key = "beta_random_failures";
} else if (beta <= 3.5) {
cls = "wear_out";
icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12H12V6Z";
key = "beta_wear_out";
} else {
cls = "highly_predictable";
icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z";
key = "beta_highly_predictable";
}
return html`
<span class="beta-badge ${cls}">
<ha-svg-icon path="${icon}"></ha-svg-icon>
${t(key, lang)} (\u03B2=${beta.toFixed(2)})
</span>
`;
}
function renderWeibullChart(beta: number, eta: number, currentInterval: number, recommended: number, lang: string) {
const W = 300, H = 160;
const PAD_L = 32, PAD_R = 8, PAD_T = 8, PAD_B = 24;
const chartW = W - PAD_L - PAD_R;
const chartH = H - PAD_T - PAD_B;
const maxT = Math.max(currentInterval, recommended, eta, 1) * 1.3;
const N = 50;
const points: Array<[number, number]> = [];
for (let i = 0; i <= N; i++) {
const t_val = (i / N) * maxT;
const cdf = 1.0 - Math.exp(-Math.pow(t_val / eta, beta));
const x = PAD_L + (t_val / maxT) * chartW;
const y = PAD_T + chartH - cdf * chartH;
points.push([x, y]);
}
const polyline = points.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" ");
const areaPath = `M${PAD_L},${PAD_T + chartH} ` +
points.map(([x, y]) => `L${x.toFixed(1)},${y.toFixed(1)}`).join(" ") +
` L${points[N][0].toFixed(1)},${PAD_T + chartH} Z`;
const curX = PAD_L + (currentInterval / maxT) * chartW;
const curCdf = 1.0 - Math.exp(-Math.pow(currentInterval / eta, beta));
const curY = PAD_T + chartH - curCdf * chartH;
const reliability = ((1.0 - curCdf) * 100).toFixed(0);
const recX = PAD_L + (recommended / maxT) * chartW;
const yTicks = [0, 0.25, 0.5, 0.75, 1.0];
return html`
<div class="weibull-chart">
<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="${t("chart_weibull", lang)}">
${yTicks.map(tick => {
const y = PAD_T + chartH - tick * chartH;
return svg`
<line x1="${PAD_L}" y1="${y.toFixed(1)}" x2="${W - PAD_R}" y2="${y.toFixed(1)}"
stroke="var(--divider-color)" stroke-width="0.5" stroke-dasharray="${tick === 0.5 ? '4,3' : nothing}" />
<text x="${PAD_L - 4}" y="${(y + 3).toFixed(1)}" fill="var(--secondary-text-color)"
font-size="8" text-anchor="end">${(tick * 100).toFixed(0)}%</text>
`;
})}
<text x="${PAD_L}" y="${H - 4}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">0</text>
<text x="${(PAD_L + W - PAD_R) / 2}" y="${H - 4}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(maxT / 2)}</text>
<text x="${W - PAD_R}" y="${H - 4}" fill="var(--secondary-text-color)" font-size="8" text-anchor="middle">${Math.round(maxT)}</text>
<path d="${areaPath}" fill="var(--primary-color, #03a9f4)" opacity="0.08" />
<polyline points="${polyline}" fill="none"
stroke="var(--primary-color, #03a9f4)" stroke-width="2" />
${currentInterval > 0 ? svg`
<line x1="${curX.toFixed(1)}" y1="${PAD_T}" x2="${curX.toFixed(1)}" y2="${(PAD_T + chartH).toFixed(1)}"
stroke="var(--primary-color, #03a9f4)" stroke-width="1.5" stroke-dasharray="4,3" />
<circle cx="${curX.toFixed(1)}" cy="${curY.toFixed(1)}" r="3"
fill="var(--primary-color, #03a9f4)" />
<text x="${(curX + 4).toFixed(1)}" y="${(curY - 6).toFixed(1)}" fill="var(--primary-color, #03a9f4)"
font-size="9" font-weight="600">R=${reliability}%</text>
` : nothing}
${recommended > 0 && recommended !== currentInterval ? svg`
<line x1="${recX.toFixed(1)}" y1="${PAD_T}" x2="${recX.toFixed(1)}" y2="${(PAD_T + chartH).toFixed(1)}"
stroke="var(--success-color, #4caf50)" stroke-width="1.5" stroke-dasharray="4,3" />
` : nothing}
<line x1="${PAD_L}" y1="${PAD_T}" x2="${PAD_L}" y2="${PAD_T + chartH}"
stroke="var(--secondary-text-color)" stroke-width="1" />
<line x1="${PAD_L}" y1="${PAD_T + chartH}" x2="${W - PAD_R}" y2="${PAD_T + chartH}"
stroke="var(--secondary-text-color)" stroke-width="1" />
</svg>
</div>
<div class="chart-legend">
<span class="legend-item"><span class="legend-swatch" style="background:var(--primary-color, #03a9f4)"></span> ${t("weibull_failure_probability", lang)}</span>
${currentInterval > 0 ? html`<span class="legend-item"><span class="legend-swatch" style="background:var(--primary-color, #03a9f4); opacity:0.5"></span> ${t("current_interval_marker", lang)}</span>` : nothing}
${recommended > 0 && recommended !== currentInterval ? html`<span class="legend-item"><span class="legend-swatch" style="background:var(--success-color, #4caf50)"></span> ${t("recommended_marker", lang)}</span>` : nothing}
</div>
`;
}
function renderWeibullInfo(analysis: NonNullable<MaintenanceTask["interval_analysis"]>, lang: string) {
return html`
<div class="weibull-info-row">
<div class="weibull-info-item">
<span>${t("characteristic_life", lang)}</span>
<span class="weibull-info-value">${Math.round(analysis.weibull_eta!)} ${t("days", lang)}</span>
</div>
${analysis.weibull_r_squared != null ? html`
<div class="weibull-info-item">
<span>${t("weibull_r_squared", lang)}</span>
<span class="weibull-info-value">${analysis.weibull_r_squared!.toFixed(3)}</span>
</div>
` : nothing}
</div>
`;
}
function renderConfidenceInterval(analysis: NonNullable<MaintenanceTask["interval_analysis"]>, task: MaintenanceTask, lang: string) {
const low = analysis.confidence_interval_low!;
const high = analysis.confidence_interval_high!;
const rec = task.suggested_interval ?? task.interval_days ?? 0;
const current = task.interval_days ?? 0;
const barMin = Math.max(0, low - 5);
const barMax = high + 5;
const range = barMax - barMin;
const fillLeft = ((low - barMin) / range) * 100;
const fillWidth = ((high - low) / range) * 100;
const recPos = ((rec - barMin) / range) * 100;
const curPos = current > 0 ? ((current - barMin) / range) * 100 : -1;
return html`
<div class="confidence-range">
<div class="confidence-range-title">
${t("confidence_interval", lang)}: ${rec} ${t("days", lang)} (${low}\u2013${high})
</div>
<div class="confidence-bar">
<div class="confidence-fill" style="left:${fillLeft.toFixed(1)}%;width:${fillWidth.toFixed(1)}%"></div>
${curPos >= 0 ? html`<div class="confidence-marker current" style="left:${curPos.toFixed(1)}%"></div>` : nothing}
<div class="confidence-marker recommended" style="left:${recPos.toFixed(1)}%"></div>
</div>
<div class="confidence-labels">
<span class="confidence-text low">${t("confidence_conservative", lang)} (${low}${t("days", lang).charAt(0)})</span>
<span class="confidence-text high">${t("confidence_aggressive", lang)} (${high}${t("days", lang).charAt(0)})</span>
</div>
</div>
`;
}