Skip to content

Commit 1787591

Browse files
author
=
committed
Show energy sessions in timeline and fix mobile overflow
1 parent cf83ac7 commit 1787591

7 files changed

Lines changed: 178 additions & 5 deletions

File tree

‎apps/predbat/frontend.zip‎

906 Bytes
Binary file not shown.

‎frontend/src/App.css‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@
158158
}
159159

160160
@media (max-width: 700px) {
161+
.app-shell {
162+
width: 100%;
163+
max-width: 100%;
164+
overflow-x: clip;
165+
}
166+
161167
.app-content,
162168
.app-shell.navigation-collapsed .app-content {
163169
margin-left: 0;

‎frontend/src/components/PlanVisual.css‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,81 @@
403403
color: var(--color-text);
404404
}
405405

406+
/* Energy-session colours take priority over the underlying battery action. */
407+
.plan-timeline-event.timeline-rate-event-power-down {
408+
border-color: var(--color-danger);
409+
background: var(--color-danger);
410+
color: var(--color-semantic-contrast);
411+
}
412+
413+
.plan-timeline-event.timeline-rate-event-power-up {
414+
border-color: var(--color-success);
415+
background: var(--color-success);
416+
color: var(--color-semantic-contrast);
417+
}
418+
419+
.plan-timeline-event.timeline-rate-event-axle-import {
420+
border-color: var(--color-flow-grid);
421+
background: var(--color-flow-grid);
422+
color: var(--color-semantic-contrast);
423+
}
424+
425+
.plan-timeline-event.timeline-rate-event-axle-export {
426+
border-color: var(--color-export);
427+
background: var(--color-export);
428+
color: var(--color-semantic-contrast);
429+
}
430+
431+
.plan-timeline-event.timeline-rate-event-axle-event {
432+
border-color: var(--color-warning);
433+
background: var(--color-warning);
434+
color: var(--color-semantic-contrast);
435+
}
436+
437+
.plan-timeline-event.timeline-rate-event-generic {
438+
border-color: var(--color-text-muted);
439+
background: var(--color-text-muted);
440+
color: var(--color-semantic-contrast);
441+
}
442+
443+
.plan-timeline-rate-event {
444+
display: inline-flex;
445+
align-items: center;
446+
gap: 0.25rem;
447+
margin-left: auto;
448+
padding: 0.2rem 0.4rem;
449+
border: 1px solid currentColor;
450+
border-radius: 999px;
451+
font-size: 0.58rem;
452+
font-weight: 700;
453+
line-height: 1;
454+
white-space: nowrap;
455+
}
456+
457+
.plan-timeline-rate-event.is-power-down {
458+
color: var(--color-danger);
459+
}
460+
461+
.plan-timeline-rate-event.is-power-up {
462+
color: var(--color-success);
463+
}
464+
465+
.plan-timeline-rate-event.is-axle-import {
466+
color: var(--color-flow-grid);
467+
}
468+
469+
.plan-timeline-rate-event.is-axle-export {
470+
color: var(--color-export);
471+
}
472+
473+
.plan-timeline-rate-event.is-axle-event {
474+
color: var(--color-warning);
475+
}
476+
477+
.plan-timeline-rate-event.is-generic {
478+
color: var(--color-text-muted);
479+
}
480+
406481
/*
407482
* SOC heading.
408483
*/

‎frontend/src/components/PlanVisual.tsx‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,25 @@ type TimelineEvent = {
7272
carbonChange: number
7373

7474
explanation?: string
75+
76+
rateEventType?: NonNullable<PlanRow['rate_event_type']>
77+
}
78+
79+
const RATE_EVENT_DETAILS: Record<NonNullable<PlanRow['rate_event_type']>, { label: string; colour: string }> = {
80+
octopus_power_down: { label: 'Power Down', colour: 'power-down' },
81+
octopus_power_up: { label: 'Power Up', colour: 'power-up' },
82+
octopus_happy_hour: { label: 'Happy Hour', colour: 'power-up' },
83+
axle_import: { label: 'Axle import', colour: 'axle-import' },
84+
axle_export: { label: 'Axle export', colour: 'axle-export' },
85+
axle_event: { label: 'Axle event', colour: 'axle-event' },
86+
energy_event: { label: 'Energy event', colour: 'generic' }
87+
}
88+
89+
function getRateEventType(row: PlanRow) {
90+
const isEnergyEvent =
91+
row.import_rate_adjust_type === 'saving' || row.export_rate_adjust_type === 'saving'
92+
93+
return isEnergyEvent ? (row.rate_event_type ?? 'energy_event') : undefined
7594
}
7695

7796
type TimelineTick = {
@@ -409,6 +428,8 @@ function buildTimelineEvents(
409428

410429
let currentAction = getTimelineAction(rows[0].state)
411430

431+
let currentRateEventType = getRateEventType(rows[0])
432+
412433
function addEvent(endIndex: number) {
413434
const eventRows = rows.slice(startIndex, endIndex + 1)
414435

@@ -533,19 +554,25 @@ function buildTimelineEvents(
533554

534555
carCharging,
535556

536-
explanation
557+
explanation,
558+
559+
rateEventType: currentRateEventType
537560
})
538561
}
539562

540563
for (let index = 1; index < rows.length; index += 1) {
541564
const action = getTimelineAction(rows[index].state)
542565

543-
if (action !== currentAction) {
566+
const rateEventType = getRateEventType(rows[index])
567+
568+
if (action !== currentAction || rateEventType !== currentRateEventType) {
544569
addEvent(index - 1)
545570

546571
startIndex = index
547572

548573
currentAction = action
574+
575+
currentRateEventType = rateEventType
549576
}
550577
}
551578

@@ -897,6 +924,10 @@ export default function PlanVisual({ plan }: PlanVisualProps) {
897924
? formatTargetSoc(activeEvent.targetSocStart, activeEvent.targetSocEnd)
898925
: null
899926

927+
const activeRateEvent = activeEvent?.rateEventType
928+
? RATE_EVENT_DETAILS[activeEvent.rateEventType]
929+
: null
930+
900931
const ticks = buildTimeTicks(timelineStart, timelineEnd)
901932

902933
/*
@@ -1025,6 +1056,14 @@ export default function PlanVisual({ plan }: PlanVisualProps) {
10251056

10261057
<span>{formatEventRange(activeEvent.start, activeEvent.end)}</span>
10271058
</div>
1059+
1060+
{activeRateEvent && (
1061+
<span className={`plan-timeline-rate-event is-${activeRateEvent.colour}`}>
1062+
<FontAwesomeIcon icon={faBolt} aria-hidden="true" />
1063+
1064+
{activeRateEvent.label}
1065+
</span>
1066+
)}
10281067
</div>
10291068

10301069
<div className="plan-timeline-detail-values">
@@ -1156,18 +1195,23 @@ export default function PlanVisual({ plan }: PlanVisualProps) {
11561195

11571196
const isActive = activeEvent?.id === event.id
11581197

1198+
const rateEvent = event.rateEventType
1199+
? RATE_EVENT_DETAILS[event.rateEventType]
1200+
: null
1201+
11591202
return (
11601203
<button
11611204
key={event.id}
11621205
type="button"
1163-
className={`plan-timeline-event timeline-action-${event.action} ${isCurrent ? 'is-current' : ''} ${isActive ? 'is-active' : ''}`}
1206+
className={`plan-timeline-event timeline-action-${event.action} ${rateEvent ? `timeline-rate-event-${rateEvent.colour}` : ''} ${isCurrent ? 'is-current' : ''} ${isActive ? 'is-active' : ''}`}
11641207
style={{
11651208
left: `${startX}px`,
11661209

11671210
width: `${eventWidth}px`
11681211
}}
11691212
aria-label={
1170-
`${getEventTitle(event)}, ` + `${formatEventRange(event.start, event.end)}`
1213+
`${rateEvent ? `${rateEvent.label}, ` : ''}${getEventTitle(event)}, ` +
1214+
`${formatEventRange(event.start, event.end)}`
11711215
}
11721216
aria-pressed={selectedEventId === event.id}
11731217
onMouseEnter={() => {

‎frontend/src/index.css‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,16 @@ body {
8787
min-height: 100dvh;
8888
}
8989

90+
@media (max-width: 700px) {
91+
html,
92+
body,
93+
#root {
94+
width: 100%;
95+
max-width: 100%;
96+
overflow-x: clip;
97+
}
98+
}
99+
90100
@media (prefers-reduced-motion: reduce) {
91101
*,
92102
*::before,

‎frontend/src/pages/ConfigPage.css‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
.config-table td.is-changed { color: var(--color-warning); font-weight: 700; }
7171
.config-table select, .config-table input[type='number'] { width: min(16rem, 100%); padding: 0.42rem 0.5rem; border: 1px solid var(--color-border); border-radius: 6px; background: var(--color-bg); color: var(--color-text); font: inherit; }
7272
.config-switch {
73+
position: relative;
7374
display: inline-flex;
7475
align-items: center;
7576
gap: 7px;
@@ -80,6 +81,11 @@
8081

8182
.config-switch input {
8283
position: absolute;
84+
top: 0;
85+
left: 0;
86+
width: 1px;
87+
height: 1px;
88+
margin: 0;
8389
opacity: 0;
8490
pointer-events: none;
8591
}

‎frontend/tests/plan-timeline.test.mjs‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const currencyModule = { exports: {} }
1717
vm.runInNewContext(compile(currencySource), { module: currencyModule, exports: currencyModule.exports })
1818

1919
const module = { exports: {} }
20-
vm.runInNewContext(compile(source), {
20+
vm.runInNewContext(compile(source.replace('function buildTimelineEvents(', 'export function buildTimelineEvents(')), {
2121
module,
2222
exports: module.exports,
2323
require(specifier) {
@@ -46,3 +46,35 @@ test('SOC hover interpolates the plotted value at the pointer time', () => {
4646
assert.equal(module.exports.interpolateSoc(points, 45), 45)
4747
assert.equal(module.exports.interpolateSoc(points, 90), 40)
4848
})
49+
50+
test('timeline splits a continuous charge action around a Power Up session', () => {
51+
const row = (time, rateEventType) => ({
52+
time,
53+
state: 'Chrg',
54+
state_target: 80,
55+
soc_percent: 50,
56+
import_rate: 0,
57+
export_rate: 12,
58+
car_charging: 0,
59+
carbon_change: 0,
60+
reasons: [],
61+
import_rate_adjust_type: rateEventType ? 'saving' : undefined,
62+
rate_event_type: rateEventType
63+
})
64+
65+
const events = module.exports.buildTimelineEvents({
66+
rows: [
67+
row('2026-10-11T10:30:00+01:00'),
68+
row('2026-10-11T11:00:00+01:00', 'octopus_power_up'),
69+
row('2026-10-11T11:30:00+01:00', 'octopus_power_up'),
70+
row('2026-10-11T12:00:00+01:00')
71+
],
72+
currency_symbols: ['£', 'p'],
73+
reason_templates: {}
74+
}, 30 * 60 * 1000, 50)
75+
76+
assert.equal(events.length, 3)
77+
assert.equal(events[1].rateEventType, 'octopus_power_up')
78+
assert.equal(events[1].start.toISOString(), '2026-10-11T10:00:00.000Z')
79+
assert.equal(events[1].end.toISOString(), '2026-10-11T11:00:00.000Z')
80+
})

0 commit comments

Comments
 (0)