fix(management): drop 95% cost line from income chart; purple/green cumulative result
Remove the planned-cost line from the actual vs forecast income chart and colour the Turnover Overview cumulative result purple (planned) or green (actual). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6036506d70
commit
2b411404da
2 changed files with 7 additions and 10 deletions
|
|
@ -104,7 +104,7 @@ export function IncomeCostChart({ months, title = "Income & cost", incomeLabel =
|
|||
description={description ?? "Teal shows income left after costs, excluding retention payouts. Values below zero are red."} />;
|
||||
}
|
||||
|
||||
export function ResultChart({ months, title = "Cumulative planned cash result from zero; not a bank balance" }: { months: { label: string; cumulativeResult: number | null }[]; title?: string }) {
|
||||
export function ResultChart({ months, title = "Cumulative planned cash result from zero; not a bank balance", color = chartColors.result }: { months: { label: string; cumulativeResult: number | null }[]; title?: string; color?: string }) {
|
||||
const gradientId = useId().replace(/:/g, "");
|
||||
const max = Math.max(0, ...months.map((m) => m.cumulativeResult ?? 0));
|
||||
const min = Math.min(0, ...months.map((m) => m.cumulativeResult ?? 0));
|
||||
|
|
@ -112,8 +112,8 @@ export function ResultChart({ months, title = "Cumulative planned cash result fr
|
|||
return <div className="mgmt-chart mgmt-chart-small" role="img" aria-label={title}><ResponsiveContainer width="100%" height="100%"><AreaChart data={months} margin={{ top: 15, right: 8, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 5" /><XAxis dataKey="label" {...axis} /><YAxis {...axis} width={54} tickFormatter={currencyTick} />
|
||||
<ReferenceLine y={0} stroke="var(--muted-foreground)" strokeDasharray="4 4" /><Tooltip content={<FinancialTooltip />} cursor={{ stroke: "var(--muted-foreground)", strokeDasharray: "3 5" }} />
|
||||
<defs><linearGradient id={`${gradientId}-result`} x1="0" y1="0" x2="0" y2="1"><stop offset={`${offset * 100}%`} stopColor={chartColors.result} /><stop offset={`${offset * 100}%`} stopColor={chartColors.negative} /></linearGradient></defs>
|
||||
<Area dataKey="cumulativeResult" name="Cumulative result" type="monotone" stroke={`url(#${gradientId}-result)`} fill={`url(#${gradientId}-result)`} fillOpacity={.13} strokeWidth={2.5} dot={months.length === 1 ? { r: 4, fill: (months[0].cumulativeResult ?? 0) < 0 ? chartColors.negative : chartColors.result } : false} />
|
||||
<defs><linearGradient id={`${gradientId}-result`} x1="0" y1="0" x2="0" y2="1"><stop offset={`${offset * 100}%`} stopColor={color} /><stop offset={`${offset * 100}%`} stopColor={chartColors.negative} /></linearGradient></defs>
|
||||
<Area dataKey="cumulativeResult" name="Cumulative result" type="monotone" stroke={`url(#${gradientId}-result)`} fill={`url(#${gradientId}-result)`} fillOpacity={.13} strokeWidth={2.5} dot={months.length === 1 ? { r: 4, fill: (months[0].cumulativeResult ?? 0) < 0 ? chartColors.negative : color } : false} />
|
||||
</AreaChart></ResponsiveContainer></div>;
|
||||
}
|
||||
|
||||
|
|
@ -145,7 +145,6 @@ function IncomeForecastTooltip({ active, payload, label }: { active?: boolean; p
|
|||
return <div className="mgmt-tooltip">
|
||||
<strong>{label}</strong>
|
||||
<div><span><i style={{ background: chartColors.forecast }} />Forecast</span><b>{money(point.forecast)}</b></div>
|
||||
<div><span><i style={{ background: chartColors.cost }} />Planned cost (95% of forecast)</span><b>{money(point.forecastCost)}</b></div>
|
||||
<div><span><i style={{ background: chartColors.payout }} />Actual / invoiced</span><b>{money(point.actual)}</b></div>
|
||||
<div><span><i style={{ background: notStartedColor }} />Not yet started</span><b>{money(point.notStarted)}</b></div>
|
||||
{variance !== null && <div><span><i style={{ background: chartColors.result }} />Variance</span><b>{money(variance)}</b></div>}
|
||||
|
|
@ -162,7 +161,6 @@ export function IncomeForecastChart({ data }: { data: IncomeSeriesPoint[] }) {
|
|||
const rows = data.map((point) => ({
|
||||
label: incomeMonthLabel(point.index),
|
||||
forecast: point.forecast,
|
||||
forecastCost: point.forecastCost,
|
||||
forecastRed: point.redOnActual ? null : point.notStarted,
|
||||
actual: point.actual,
|
||||
actualRed: point.redOnActual ? point.notStarted : null,
|
||||
|
|
@ -171,9 +169,9 @@ export function IncomeForecastChart({ data }: { data: IncomeSeriesPoint[] }) {
|
|||
const tooltipSpace = 156;
|
||||
return <div className="mgmt-paired-chart" style={{ "--mgmt-tooltip-space": `${tooltipSpace}px` } as CSSProperties}>
|
||||
<div className="mgmt-chart-toolbar">
|
||||
<Legend items={[{ label: "Forecast", color: chartColors.forecast }, { label: "Planned cost · 95% of forecast", color: chartColors.cost }, { label: "Actual / invoiced", color: chartColors.payout }, { label: "Not yet started (awarded)", color: notStartedColor }]} />
|
||||
<Legend items={[{ label: "Forecast", color: chartColors.forecast }, { label: "Actual / invoiced", color: chartColors.payout }, { label: "Not yet started (awarded)", color: notStartedColor }]} />
|
||||
</div>
|
||||
<div className="mgmt-chart" role="img" aria-label="Monthly forecast and invoiced income, with planned cost at 95% of forecast income excluding awarded jobs that have not started yet">
|
||||
<div className="mgmt-chart" role="img" aria-label="Monthly forecast and invoiced income">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<ComposedChart data={rows} margin={{ top: tooltipSpace + 15, right: 8, bottom: 0, left: 0 }} barGap={6} barCategoryGap="18%" accessibilityLayer>
|
||||
<CartesianGrid vertical={false} stroke="var(--border)" strokeDasharray="3 5" />
|
||||
|
|
@ -186,10 +184,9 @@ export function IncomeForecastChart({ data }: { data: IncomeSeriesPoint[] }) {
|
|||
<Bar dataKey="forecastRed" name="Not yet started" stackId="forecast" fill={notStartedColor} radius={[4, 4, 0, 0]} maxBarSize={26} isAnimationActive={false} />
|
||||
<Bar dataKey="actual" name="Actual / invoiced" stackId="actual" fill={chartColors.payout} radius={[4, 4, 0, 0]} maxBarSize={26} isAnimationActive={false} />
|
||||
<Bar dataKey="actualRed" name="Not yet started" stackId="actual" fill={notStartedColor} radius={[4, 4, 0, 0]} maxBarSize={26} isAnimationActive={false} />
|
||||
<Line dataKey="forecastCost" name="Planned cost (95% of forecast)" stroke={chartColors.cost} strokeWidth={2} strokeDasharray="5 4" dot={{ r: 3 }} connectNulls={false} isAnimationActive={false} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<p className="mgmt-source-note">Purple: forecast. Grey dashed line: planned cost at 95% of the purple forecast income only, excluding red amounts. Green: invoiced actuals. Red: awarded jobs not yet started, stacked on the invoiced bar up to the current month and on the forecast bar after it.</p>
|
||||
<p className="mgmt-source-note">Purple: forecast. Green: invoiced actuals. Red: awarded jobs not yet started, stacked on the invoiced bar up to the current month and on the forecast bar after it.</p>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function TurnoverSection({ data, periodLabel, part }: { data: TurnoverSectionDat
|
|||
</table></div>
|
||||
</Panel>;
|
||||
if (part === "result") return <Panel title={`Cumulative ${title.toLowerCase()} result`} subtitle="Starts from zero · excludes retention payouts">
|
||||
<ResultChart months={data.months} title={`Cumulative ${title.toLowerCase()} result`} />
|
||||
<ResultChart months={data.months} title={`Cumulative ${title.toLowerCase()} result`} color={planned ? chartColors.forecast : chartColors.payout} />
|
||||
<div className="mgmt-result-caption"><span>Selected period result</span><b className={data.result === null ? undefined : data.result < 0 ? "is-negative" : "is-positive"}>{amount(data.result)}</b></div>
|
||||
</Panel>;
|
||||
return <Panel title={`Largest ${title.toLowerCase()} income schedules`} subtitle={planned ? "Ranked by actual + forecast income" : "Ranked by invoiced income only"}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue