Skip to content

Commit 3b020fa

Browse files
committed
Improve PV bar label placement and hide partial-day forecasts
- Flip anchor/align when the rotated label doesn't fit inside its bar: fits → centered inside (white), overflows → drawn fully above the bar top (amber). The previous behavior left small-bar labels split across the bar boundary, which was visually noisy. - Drop chart entries for days with fewer than 20 hourly forecast points. The 66h FMI window almost always begins or ends mid-day, which used to render as a misleading 0-ish kWh bar at the edge of the chart.
1 parent e74090a commit 3b020fa

2 files changed

Lines changed: 34 additions & 18 deletions

File tree

‎frontend/src/components/FmiWeatherBox.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,27 @@ const WeatherBox: React.FC<WeatherBoxProps> = ({ className }) => {
7777
return null;
7878
}
7979

80-
const pvByDate = new Map<string, number>();
80+
// Aggregate hourly PV forecast points to per-day kWh + hour count.
81+
// Days with partial coverage (e.g. first/last day of the 66h window) are
82+
// dropped from the chart to avoid showing misleading near-zero totals.
83+
const FULL_DAY_HOURS = 20;
84+
const pvByDate = new Map<string, { kwh: number; hours: number }>();
8185
for (const p of pvForecast?.points ?? []) {
8286
const key = p.time.slice(0, 10);
83-
pvByDate.set(key, (pvByDate.get(key) ?? 0) + p.outputW / 1000);
87+
const cur = pvByDate.get(key) ?? { kwh: 0, hours: 0 };
88+
pvByDate.set(key, {
89+
kwh: cur.kwh + p.outputW / 1000,
90+
hours: cur.hours + 1,
91+
});
8492
}
8593

8694
const chartData = daily.map((d) => {
8795
const key = d.date.slice(0, 10);
96+
const pv = pvByDate.get(key);
8897
return {
8998
temp: d.temperatureMax,
9099
rain: d.precipitation,
91-
pvKwh: pvByDate.has(key) ? pvByDate.get(key)! : null,
100+
pvKwh: pv && pv.hours >= FULL_DAY_HOURS ? pv.kwh : null,
92101
label: `${format(new Date(d.date), "EEEEEE", {
93102
locale: fi,
94103
})}`,

‎frontend/src/components/WeatherChart.tsx‎

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ type WeatherChartProps = {
3838
days?: number;
3939
};
4040

41+
const fitsInsideBar = (ctx: Context, charPx: number): boolean => {
42+
const value = ctx.dataset.data[ctx.dataIndex] as number | null;
43+
if (value == null) return true;
44+
const scale = ctx.chart.scales.yPv;
45+
if (!scale) return true;
46+
const barHeight = Math.abs(
47+
scale.getPixelForValue(value) - scale.getPixelForValue(0),
48+
);
49+
const text = `${value.toFixed(0)} kWh`;
50+
return barHeight >= text.length * charPx + 4;
51+
};
52+
4153
const WeatherChart: React.FC<WeatherChartProps> = ({
4254
data: unfilteredData,
4355
days,
@@ -85,26 +97,21 @@ const WeatherChart: React.FC<WeatherChartProps> = ({
8597
categoryPercentage: 0.7,
8698
datalabels: {
8799
display: true,
88-
anchor: "center",
89-
align: "center",
90100
rotation: -90,
91101
clip: false,
92102
font: { size: labelFontSize, weight: "bold" },
93103
formatter: (v: number | null) =>
94104
v == null ? "" : `${v.toFixed(0)} kWh`,
95-
color: (ctx: Context) => {
96-
const value = ctx.dataset.data[ctx.dataIndex] as number | null;
97-
if (value == null) return "#ffffff";
98-
const scale = ctx.chart.scales.yPv;
99-
if (!scale) return "#ffffff";
100-
const barHeight = Math.abs(
101-
scale.getPixelForValue(value) - scale.getPixelForValue(0),
102-
);
103-
const text = `${value.toFixed(0)} kWh`;
104-
const labelPx = text.length * labelCharPx + 4;
105-
if (barHeight >= labelPx) return "#ffffff";
106-
return theme.mode === "dark" ? "#f5a524" : "#a35a00";
107-
},
105+
anchor: (ctx: Context) =>
106+
fitsInsideBar(ctx, labelCharPx) ? "center" : "end",
107+
align: (ctx: Context) =>
108+
fitsInsideBar(ctx, labelCharPx) ? "center" : "top",
109+
color: (ctx: Context) =>
110+
fitsInsideBar(ctx, labelCharPx)
111+
? "#ffffff"
112+
: theme.mode === "dark"
113+
? "#f5a524"
114+
: "#a35a00",
108115
},
109116
});
110117
}

0 commit comments

Comments
 (0)