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:
parent
fdc1f5c176
commit
14d70c5bb3
2 changed files with 37 additions and 15 deletions
|
|
@ -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) => ({
|
||||
// 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': ty.total_pence / 100,
|
||||
'Last Year': (trend.last_year[i]?.total_pence ?? 0) / 100,
|
||||
}))
|
||||
'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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue