/** Cost/duration chart renderers (task detail). * * Completions are plotted on a true time axis (a completion 8 months ago sits * visibly further away than one from last week), with round y-ticks per axis * and year-aware date labels once the span crosses years — "Jun 3 '25" cannot * be misread as coming after "Feb 21 '26". */ import { html, svg, nothing } from "lit"; import { t } from "../styles"; import { niceTicks, fmtNum, fmtDateTick, timeTicks, needsYear } from "./chart-utils"; import type { MaintenanceTask } from "../types"; const COST_CHART_H = 200; const PAD_T = 10; const PAD_B = 22; export function renderCostDurationCard( task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both", setToggle: (val: "cost" | "duration" | "both") => void, ) { const completedEntries = task.history.filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)); if (completedEntries.length < 2) return nothing; const anyCost = completedEntries.some((h) => (h.cost ?? 0) > 0); const anyDuration = completedEntries.some((h) => (h.duration ?? 0) > 0); if (!anyCost && !anyDuration) return nothing; return html`

${t("cost_duration_chart", lang)}

${anyCost ? html`` : nothing} ${anyCost && anyDuration ? html`` : nothing} ${anyDuration ? html`` : nothing}
${renderHistoryChart(task, lang, toggle)}
`; } function renderHistoryChart(task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both") { const entries = task.history .filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)) .map((h) => ({ ts: new Date(h.timestamp).getTime(), cost: h.cost ?? 0, duration: h.duration ?? 0 })) .sort((a, b) => a.ts - b.ts); if (entries.length < 2) return nothing; const dataCost = entries.some((e) => e.cost > 0); const dataDuration = entries.some((e) => e.duration > 0); if (!dataCost && !dataDuration) return nothing; const hasCost = toggle !== "duration" && dataCost; const hasDuration = toggle !== "cost" && dataDuration; const showCost = hasCost || (!hasDuration && dataCost); const showDuration = hasDuration || (!hasCost && dataDuration); const W = 640; // wide viewBox; the container scales it to full card width const H = COST_CHART_H; const PAD_L = showCost ? 44 : 12; const PAD_R = showDuration ? 44 : 12; const plotW = W - PAD_L - PAD_R; const plotB = H - PAD_B; const plotH = plotB - PAD_T; // True time axis with a padded domain so edge bars don't clip. const tsMin = entries[0].ts; const tsMax = entries[entries.length - 1].ts; const tsPad = (tsMax - tsMin || 86400000) * 0.05; const t0 = tsMin - tsPad; const t1 = tsMax + tsPad; const withYear = needsYear(tsMin, tsMax); const toX = (ts: number) => PAD_L + ((ts - t0) / (t1 - t0)) * plotW; const costAxis = niceTicks(0, Math.max(...entries.map((e) => e.cost)) || 1, 3); const durAxis = niceTicks(0, Math.max(...entries.map((e) => e.duration)) || 1, 3); const costY = (v: number) => PAD_T + (1 - v / (costAxis.niceMax || 1)) * plotH; const durY = (v: number) => PAD_T + (1 - v / (durAxis.niceMax || 1)) * plotH; // Bars keep a readable width even when completions crowd together. const minGap = entries.length > 1 ? Math.min(...entries.slice(1).map((e, i) => toX(e.ts) - toX(entries[i].ts))) : plotW; const barW = Math.max(6, Math.min(22, minGap * 0.55)); const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(4, entries.length))); return html`
${showCost ? costAxis.ticks.map((v) => { const y = costY(v); if (y < PAD_T - 1 || y > plotB + 1) return nothing; return svg` ${fmtNum(v)}€`; }) : nothing} ${showDuration ? durAxis.ticks.map((v) => { const y = durY(v); if (y < PAD_T - 1 || y > plotB + 1) return nothing; return svg`${fmtNum(v)}m`; }) : nothing} ${showCost ? entries.filter((e) => e.cost > 0).map((e) => svg` ${fmtDateTick(e.ts, lang, true)}: ${e.cost.toLocaleString(lang)}€${e.duration ? ` · ${e.duration}m` : ""} `) : nothing} ${showDuration ? svg` ${entries.map((e) => svg` ${fmtDateTick(e.ts, lang, true)}: ${e.duration}m${e.cost ? ` · ${e.cost.toLocaleString(lang)}€` : ""} `)} ` : nothing} ${xTicks.map((ts, i) => { const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle"; return svg`${fmtDateTick(ts, lang, withYear)}`; })}
${showCost ? html`${t("cost", lang)}` : nothing} ${showDuration ? html`${t("duration", lang)}` : nothing}
`; }