fix(kpi): reserve label headroom in monthly result bars so the endpoint value never clips (#2092)

The Resultat per manad pane scaled positive bars to 55% of the chart
height with a fixed 62%-height baseline whenever any month was negative,
leaving ~8px above the tallest bar: the value label drawn 5px above it
rendered half outside the viewBox. A dominant negative month overflowed
the bottom edge the same way.

Replace the fixed baseline with one px-per-krona scale for both signs
and place the baseline from the actual positive/negative maxima, with
16px reserved on any side that can carry the endpoint label.


Claude-Session: https://claude.ai/code/session_01KtnsULW2jszA9uBnkMGnad

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-09-01 09:52:54 +02:00
committed by GitHub
parent a47d54f1d7
commit 08dbfdc5c4
+11 -4
View File
@@ -76,10 +76,17 @@ function ResultBarsPane({ report }: { report: KPIReport }) {
const W = 320
const H = 120
const maxAbs = Math.max(...months.map((m) => Math.abs(m.net)), 1)
// Baseline sits lower when no month is negative, so positive bars get the room.
const hasNegative = months.some((m) => m.net < 0)
const baseline = hasNegative ? H * 0.62 : H - 4
// Fixed headroom above (and below, when negatives exist) keeps the endpoint
// label inside the viewBox even when a single month dominates the scale.
const topPad = 16
const bottomPad = hasNegative ? 16 : 4
const maxPos = Math.max(...months.map((m) => Math.max(0, m.net)), 0)
const maxNeg = Math.max(...months.map((m) => Math.max(0, -m.net)), 0)
// One px-per-krona scale for both signs, so bar heights stay comparable.
const pxPerKr = (H - topPad - bottomPad) / Math.max(maxPos + maxNeg, 1)
// All-zero months keep the baseline at the bottom instead of the top.
const baseline = maxPos + maxNeg === 0 ? H - bottomPad : topPad + maxPos * pxPerKr
const slot = W / months.length
const barW = Math.min(30, slot * 0.62)
@@ -95,7 +102,7 @@ function ResultBarsPane({ report }: { report: KPIReport }) {
})}
>
{months.map((m, i) => {
const scaled = (Math.abs(m.net) / maxAbs) * (hasNegative ? H * 0.55 : H - 26)
const scaled = Math.abs(m.net) * pxPerKr
const h = m.net === 0 ? 2 : Math.max(3, scaled)
const x = i * slot + (slot - barW) / 2
const y = m.net >= 0 ? baseline - h : baseline