Skip to content

Commit bfdcbe1

Browse files
committed
feat(spot): touch UX + current-hour readout, wider content
- touch: select by column (index mode) so short bars are easy to hit; pin the tooltip above the fingertip; kill the iOS tap-highlight blink. - today header shows lowest – now – highest c/kWh for live context. - widen desktop content column 720→800.
1 parent 76f5e5d commit bfdcbe1

2 files changed

Lines changed: 65 additions & 10 deletions

File tree

‎frontend/src/App.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ const App = () => {
190190
</button>
191191
<div
192192
css={{
193-
width: 720,
193+
width: 800,
194194
flexShrink: 0,
195195
padding: "16px 0",
196196
[mq[0]]: {

‎frontend/src/components/Energy/SpotPrice.tsx‎

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import {
44
CategoryScale,
55
Chart as ChartJS,
66
ChartOptions,
7+
ChartType,
78
LinearScale,
89
Tooltip,
10+
TooltipPositionerFunction,
911
} from "chart.js";
1012
import { memo, useEffect, useState } from "react";
1113
import { Bar } from "react-chartjs-2";
@@ -16,6 +18,19 @@ import { HourPrice, SpotResponse } from "../../types/spot";
1618

1719
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip);
1820

21+
declare module "chart.js" {
22+
interface TooltipPositionerMap {
23+
aboveCursor: TooltipPositionerFunction<ChartType>;
24+
}
25+
}
26+
27+
// Pin the tooltip to the top of the chart at the pointer's x — on touch it then
28+
// rides above the fingertip (and slides along while scrubbing) instead of
29+
// hiding under it.
30+
Tooltip.positioners.aboveCursor = function (_items, evt) {
31+
return { x: evt.x, y: this.chart.chartArea.top };
32+
};
33+
1934
const HOUR_MS = 3_600_000;
2035
const SCALE_KEY = "halo.spot.showScale";
2136

@@ -80,9 +95,15 @@ const PriceChart: React.FC<{
8095
responsive: true,
8196
maintainAspectRatio: false,
8297
animation: { duration: 0 },
98+
// Select by column (x), not by hitting the bar itself — short bars are easy
99+
// to tap, and dragging scrubs hour-to-hour.
100+
interaction: { mode: "index", intersect: false },
83101
plugins: {
84102
legend: { display: false },
85103
tooltip: {
104+
position: "aboveCursor",
105+
caretSize: 0,
106+
displayColors: false,
86107
callbacks: {
87108
title: (items) => `klo ${items[0].label}`,
88109
label: (ctx) => `${(ctx.parsed.y as number).toFixed(2)} ${unit}`,
@@ -104,7 +125,15 @@ const PriceChart: React.FC<{
104125
};
105126

106127
return (
107-
<div css={{ position: "relative", height: 120 }}>
128+
<div
129+
css={{
130+
position: "relative",
131+
height: 120,
132+
touchAction: "pan-y", // let the page scroll vertically; chart scrubs horizontally
133+
userSelect: "none",
134+
WebkitTapHighlightColor: "transparent", // no grey flash on tap (iOS Safari)
135+
}}
136+
>
108137
<Bar data={data} options={options} />
109138
</div>
110139
);
@@ -140,19 +169,23 @@ const ScaleLegend: React.FC<{ unit: string }> = ({ unit }) => {
140169
);
141170
};
142171

143-
// Day label + that day's min–max range — conveys the price scale with no
144-
// chart-area cost, so the y-axis can stay hidden.
172+
// Day label + price scale: that day's min–max, and for the day containing the
173+
// current hour a "lowest – now – highest" readout so the live price has context.
174+
// Costs no chart area, so the y-axis can stay hidden.
145175
const DayHeader: React.FC<{
146176
label: string;
147177
points: HourPrice[];
148178
unit: string;
149-
}> = ({ label, points, unit }) => {
179+
now: number;
180+
}> = ({ label, points, unit, now }) => {
150181
const theme = useTheme();
151182
const sub = { ...theme.typography.caption, color: theme.colors.text.muted };
152183
const ps = points.map((p) => p.price);
153-
const range = ps.length
154-
? `${Math.min(...ps).toFixed(1)}–${Math.max(...ps).toFixed(1)} ${unit}`
155-
: "";
184+
const current = points.find((p) => {
185+
const start = new Date(p.hour).getTime();
186+
return start <= now && now < start + HOUR_MS;
187+
})?.price;
188+
156189
return (
157190
<div
158191
css={{
@@ -164,7 +197,23 @@ const DayHeader: React.FC<{
164197
}}
165198
>
166199
<span>{label}</span>
167-
<span>{range}</span>
200+
{ps.length > 0 && (
201+
<span>
202+
{Math.min(...ps).toFixed(1)}
203+
{current !== undefined ? (
204+
<>
205+
{" – "}
206+
<span css={{ color: theme.colors.text.main, fontWeight: 600 }}>
207+
{current.toFixed(1)}
208+
</span>
209+
{" – "}
210+
</>
211+
) : (
212+
"–"
213+
)}
214+
{Math.max(...ps).toFixed(1)} {unit}
215+
</span>
216+
)}
168217
</div>
169218
);
170219
};
@@ -288,7 +337,12 @@ const SpotPrice: React.FC<{ className?: string }> = ({ className }) => {
288337
intrinsic width, so the charts scale on narrow phones instead
289338
of overflowing. */}
290339
<div css={{ minWidth: 0 }}>
291-
<DayHeader label="tänään" points={data.today} unit={data.unit} />
340+
<DayHeader
341+
label="tänään"
342+
points={data.today}
343+
unit={data.unit}
344+
now={now}
345+
/>
292346
<PriceChart
293347
points={data.today}
294348
unit={data.unit}
@@ -302,6 +356,7 @@ const SpotPrice: React.FC<{ className?: string }> = ({ className }) => {
302356
label="huomenna"
303357
points={data.tomorrow}
304358
unit={data.unit}
359+
now={now}
305360
/>
306361
{data.tomorrow.length ? (
307362
<PriceChart

0 commit comments

Comments
 (0)