Weekly actuals: utility trend charts use projected figure for the in-progress month

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-10-23 12:35:04 +00:00
parent fdc1f5c176
commit 14d70c5bb3
2 changed files with 37 additions and 15 deletions

View file

@ -10,7 +10,7 @@ import {
CartesianGrid,
} from 'recharts'
import { getWeeklyActual } from '../api'
import type { WeeklyActualData, WeekSummaryRow, MonthSplitEntry, MonthDailyEntry, UtilityConsumptionBasis, UtilityTrend } from '../types'
import type { WeeklyActualData, WeekSummaryRow, MonthSplitEntry, MonthDailyEntry, UtilityConsumptionBasis, UtilityTrend, UtilityTrendMonth, UtilityEstimate } from '../types'
// ── Formatters ─────────────────────────────────────────────────────────────
@ -487,12 +487,32 @@ function fmtBasis(status: UtilityConsumptionBasis, asOfDate: string | null): str
return asOfDate ? `As of ${fmtShortDate(asOfDate)}` : 'Partial'
}
function UtilitiesCostTrendChart({ trend }: { trend: UtilityTrend }) {
const chartData = trend.this_year.map((ty, i) => ({
label: ty.label,
'This Year': ty.total_pence / 100,
'Last Year': (trend.last_year[i]?.total_pence ?? 0) / 100,
}))
// Builds the "This Year" series, swapping the in-progress month's partial
// actuals for the utilities estimate. The projected month is emitted on a
// separate dashed series (anchored on the previous month's actual so the line
// is continuous) and left null on the solid one.
function withProjection(
trend: UtilityTrend,
estimate: UtilityEstimate | null,
pick: (m: UtilityTrendMonth, est: UtilityEstimate | null) => number,
) {
const lastIdx = trend.this_year.length - 1
const last = trend.this_year[lastIdx]
const projecting = !!estimate && !!last && last.year === estimate.period.year && last.month === estimate.period.month
return trend.this_year.map((ty, i) => {
const isProjected = projecting && i === lastIdx
const value = pick(ty, isProjected ? estimate : null)
return {
label: ty.label,
'This Year': isProjected ? null : value,
'Projected': isProjected ? value : (projecting && i === lastIdx - 1 ? value : null),
'Last Year': pick(trend.last_year[i], null),
}
})
}
function UtilitiesCostTrendChart({ trend, estimate }: { trend: UtilityTrend; estimate: UtilityEstimate | null }) {
const chartData = withProjection(trend, estimate, (m, est) => (est ? est.totals.total_pence : (m?.total_pence ?? 0)) / 100)
return (
<div className="wa-chart-card">
@ -505,6 +525,7 @@ function UtilitiesCostTrendChart({ trend }: { trend: UtilityTrend }) {
<Tooltip formatter={fmtChartCcy} />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="This Year" stroke={CHART_COLORS.thisWeek} strokeWidth={2.5} dot={false} />
<Line type="monotone" dataKey="Projected" name="This Month (projected)" stroke={CHART_COLORS.thisWeek} strokeWidth={2.5} strokeDasharray="2 3" dot={{ r: 3 }} connectNulls />
<Line type="monotone" dataKey="Last Year" stroke={CHART_COLORS.budget} strokeWidth={2} dot={false} strokeDasharray="5 3" />
</LineChart>
</ResponsiveContainer>
@ -512,15 +533,14 @@ function UtilitiesCostTrendChart({ trend }: { trend: UtilityTrend }) {
)
}
function UtilitiesConsumptionCharts({ trend }: { trend: UtilityTrend }) {
function UtilitiesConsumptionCharts({ trend, estimate }: { trend: UtilityTrend; estimate: UtilityEstimate | null }) {
return (
<div className="wa-charts-row">
{trend.categories.map(cat => {
const chartData = trend.this_year.map((ty, i) => ({
label: ty.label,
'This Year': ty.by_category[cat.category]?.consumption ?? 0,
'Last Year': trend.last_year[i]?.by_category[cat.category]?.consumption ?? 0,
}))
const chartData = withProjection(trend, estimate, (m, est) =>
est
? est.categories.find(c => c.category === cat.category)?.projected_consumption ?? 0
: m?.by_category[cat.category]?.consumption ?? 0)
return (
<div className="wa-chart-card" key={cat.category}>
<h4>{cat.category_name} Consumption ({cat.unit_label}) — Last 12 Months vs Prior Year</h4>
@ -532,6 +552,7 @@ function UtilitiesConsumptionCharts({ trend }: { trend: UtilityTrend }) {
<Tooltip />
<Legend iconSize={10} wrapperStyle={{ fontSize: 11 }} />
<Line type="monotone" dataKey="This Year" stroke={CHART_COLORS.rooms} strokeWidth={2.5} dot={false} />
<Line type="monotone" dataKey="Projected" name="This Month (projected)" stroke={CHART_COLORS.rooms} strokeWidth={2.5} strokeDasharray="2 3" dot={{ r: 3 }} connectNulls />
<Line type="monotone" dataKey="Last Year" stroke={CHART_COLORS.budget} strokeWidth={2} dot={false} strokeDasharray="5 3" />
</LineChart>
</ResponsiveContainer>
@ -618,8 +639,8 @@ function UtilitiesSection({ utilities }: { utilities: WeeklyActualData['utilitie
)}
{trend && trend.this_year.length > 0 && (
<div style={{ marginTop: 16 }}>
<UtilitiesCostTrendChart trend={trend} />
<UtilitiesConsumptionCharts trend={trend} />
<UtilitiesCostTrendChart trend={trend} estimate={estimate} />
<UtilitiesConsumptionCharts trend={trend} estimate={estimate} />
</div>
)}
</>

View file

@ -200,6 +200,7 @@ export interface UtilityCosts {
export interface UtilityEstimate {
period: { year: number; month: number; start: string; end: string; remaining_days: number }
categories: { category: string; category_name: string; unit_label: string; projected_consumption: number; total_pence: number }[]
meters: UtilityMeterEstimate[]
totals: { total_pence: number; projected_consumption: number }
}