Skip to content

Commit 8c7f609

Browse files
malysclaude
andcommitted
fix(chargepilot): eco coach speaks on calm drives, full-screen graphs get axes
The coach showed "a few minutes of driving are needed" for a whole drive whenever nothing crossed an advice threshold. After a minute of movement it now states the measured share of hard acceleration, and the trip review keeps that share as a finding, so a calm drive without a trained model is no longer "nothing measurable". The consumption trace, in full screen and on the large overview graph, draws both axes, their units and a legend for above, below and the reference line: full screen leaves the text beside the graph behind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 35ac454 commit 8c7f609

8 files changed

Lines changed: 91 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.
44

55
## [Unreleased]
66

7+
### Fixed
8+
9+
- **The eco coach no longer waits forever on a calm drive.** It showed "a few minutes of driving
10+
are needed" for a whole drive whenever nothing crossed an advice threshold. After a minute of
11+
movement it now states the measured share of hard acceleration. The trip review keeps that
12+
share as a finding too, so a calm drive without a trained model is no longer "nothing
13+
measurable".
14+
- **Full-screen consumption graphs carry their axes and legend.** The trace draws both axes, the
15+
kWh/100 km and "km before now" units, and a legend for above, below and the reference line,
16+
since full screen leaves the text beside the graph behind.
17+
718
### Added
819

920
- **Speed comparison and energy breakdown answer on urban trips.** When no model can answer, both

‎app/src/main/java/com/evsuite/chargepilot/ConsumptionTraceView.kt‎

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ import kotlin.math.min
1919
* The line takes the warning colour above the reference and the accent below it, and the
2020
* reference is a labelled dashed rule, so the colour is never the only thing saying which side
2121
* a stretch is on. Missing stretches break the line. A view wider than [DETAILED_MIN_DP] —
22-
* the large graph, or any graph opened full screen — also writes its scales; a card's
23-
* thumbnail, about as tall on this panel, stays a bare line.
22+
* the large graph, or any graph opened full screen — also draws its axes, their units and a
23+
* legend inside the plot, because full screen moves the graph away from the text beside it;
24+
* a card's thumbnail, about as tall on this panel, stays a bare line.
2425
*/
2526
class ConsumptionTraceView @JvmOverloads constructor(
2627
context: Context,
@@ -53,6 +54,11 @@ class ConsumptionTraceView @JvmOverloads constructor(
5354
color = context.getColor(R.color.ev_text_secondary)
5455
textSize = resources.getDimension(R.dimen.text_label)
5556
}
57+
private val axisPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
58+
color = context.getColor(R.color.ev_outline)
59+
strokeWidth = density
60+
style = Paint.Style.STROKE
61+
}
5662

5763
/**
5864
* @param trace kWh/100 km per equal stretch, oldest first; null where nothing was driven.
@@ -75,8 +81,9 @@ class ConsumptionTraceView @JvmOverloads constructor(
7581
val gutter = if (detailed) GUTTER_DP * density else 0f
7682
val left = inset + gutter
7783
val right = width - inset - gutter
78-
val top = inset
79-
val bottom = height - inset - if (detailed) labelPaint.textSize * 1.6f else 0f
84+
// Detailed: a legend row above the plot, tick labels and the distance title below it.
85+
val top = inset + if (detailed) labelPaint.textSize * 2f else 0f
86+
val bottom = height - inset - if (detailed) labelPaint.textSize * 3f else 0f
8087
if (right <= left || bottom <= top) return
8188

8289
val low = min(0f, min(known.min(), referenceKwhPer100Km))
@@ -117,6 +124,9 @@ class ConsumptionTraceView @JvmOverloads constructor(
117124
referenceY: Float,
118125
) {
119126
val baseline = labelPaint.textSize / 3f
127+
canvas.drawLine(left, top, left, bottom, axisPaint)
128+
canvas.drawLine(left, bottom, right, bottom, axisPaint)
129+
drawLegend(canvas, top - labelPaint.textSize)
120130
labelPaint.textAlign = Paint.Align.RIGHT
121131
for (step in 0..4) {
122132
val y = bottom - (bottom - top) * step / 4f
@@ -141,6 +151,35 @@ class ConsumptionTraceView @JvmOverloads constructor(
141151
labelPaint,
142152
)
143153
}
154+
canvas.drawText(
155+
context.getString(R.string.trace_axis_distance),
156+
(left + right) / 2f,
157+
bottom + labelPaint.textSize * 2.8f,
158+
labelPaint,
159+
)
160+
}
161+
162+
/** The vertical unit, then one swatch per line style, on one row above the plot. */
163+
private fun drawLegend(canvas: Canvas, baselineY: Float) {
164+
labelPaint.textAlign = Paint.Align.LEFT
165+
val swatch = 20f * density
166+
val gap = 8f * density
167+
val swatchY = baselineY - labelPaint.textSize / 3f
168+
var x = 8f * density
169+
val unit = context.getString(R.string.trace_axis_consumption)
170+
canvas.drawText(unit, x, baselineY, labelPaint)
171+
x += labelPaint.measureText(unit) + 3 * gap
172+
listOf(
173+
abovePaint to R.string.trace_legend_above,
174+
belowPaint to R.string.trace_legend_below,
175+
referencePaint to R.string.trace_legend_reference,
176+
).forEach { (paint, label) ->
177+
canvas.drawLine(x, swatchY, x + swatch, swatchY, paint)
178+
x += swatch + gap
179+
val text = context.getString(label)
180+
canvas.drawText(text, x, baselineY, labelPaint)
181+
x += labelPaint.measureText(text) + 3 * gap
182+
}
144183
}
145184

146185
private fun format(pattern: String, value: Number) =

‎app/src/main/java/com/evsuite/chargepilot/EcoCoach.kt‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,15 @@ class EcoCoach(context: Context) {
105105
* an app that ignored the switch, so the enabled-but-silent case says which it is — the same
106106
* rule every other unavailable value on this screen follows. It stays a statement about the
107107
* measurement rather than a promise: the levers need either a fitted model or a few minutes
108-
* of movement, and neither is a thing the driver can press.
108+
* of movement, and neither is a thing the driver can press. Once that minute of movement is
109+
* there and nothing crossed a threshold, the line says so with the figure it measured —
110+
* a calm drive kept "a few minutes are needed" on screen for the whole of it.
109111
*/
110112
fun line(verdict: EcoVerdict): String? {
111113
if (!adviceEnabled(app)) return null
112-
val advice = verdict.advice ?: return app.getString(R.string.eco_advice_waiting)
113-
return sentence(advice) ?: app.getString(R.string.eco_advice_waiting)
114+
verdict.advice?.let(::sentence)?.let { return it }
115+
val share = monitor.harshSharePercent() ?: return app.getString(R.string.eco_advice_waiting)
116+
return app.getString(R.string.eco_advice_calm, share.roundToInt())
114117
}
115118

116119
/**

‎app/src/main/java/com/evsuite/chargepilot/EcoTripReview.kt‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,13 @@ object EcoTripReviewer {
191191
)
192192
}
193193

194+
/**
195+
* Whatever the share, once the track holds a minute of movement. A calm drive is a result
196+
* too: below the live advice threshold it used to drop out, and a calm drive with no fitted
197+
* model yet then read "nothing measurable" although its whole track had been measured.
198+
*/
194199
private fun steadiness(monitor: EcoDrivingMonitor): EcoFinding.Steadiness? =
195-
monitor.steadiness()?.harshSharePercent?.let(EcoFinding::Steadiness)
200+
monitor.harshSharePercent()?.let(EcoFinding::Steadiness)
196201

197202
private fun format(value: Double): String = String.format(Locale.ROOT, "%.2f", value)
198203
}

‎app/src/main/res/values-fr/strings.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ Consommation %6$s %% par km</string>
483483
<string name="eco_advice_cruise">À %1$d plutôt que %2$d km/h, cette route coûte environ %3$d %% de moins.</string>
484484
<string name="eco_advice_steadiness">Accélérations franches sur %1$d %% de ce trajet ; la même route à vitesse stable coûte moins.</string>
485485
<string name="eco_advice_waiting">Rien à dire pour l’instant — il faut quelques minutes de route avant qu’un conseil apparaisse ici.</string>
486+
<string name="eco_advice_calm">Rien à signaler : accélérations fortes sur %1$d %% de la conduite récente.</string>
486487
<string name="eco_settings_title">Assistant d’éco-conduite</string>
487488
<string name="eco_settings_advice">Afficher les conseils de conduite</string>
488489
<string name="eco_settings_voice">Énoncer les conseils</string>
@@ -516,6 +517,11 @@ Consommation %6$s %% par km</string>
516517
<string name="trip_overview_no_driving">Aucune conduite avec vitesse et puissance enregistrées sur cette fenêtre.</string>
517518
<string name="trip_overview_legend">Pointillés : votre référence, %1$s. Ambre au-dessus, bleu en dessous. Double tap sur un graphique pour le plein écran.</string>
518519
<string name="trip_overview_trace_description">Consommation nette des derniers %1$s comparée à votre référence</string>
520+
<string name="trace_axis_consumption">kWh/100 km</string>
521+
<string name="trace_axis_distance">km avant maintenant</string>
522+
<string name="trace_legend_above">Au-dessus</string>
523+
<string name="trace_legend_below">En dessous</string>
524+
<string name="trace_legend_reference">Référence</string>
519525
<string name="trip_card_current">Trajet actuel</string>
520526
<string name="trip_card_since_charge">Depuis la recharge</string>
521527
<string name="trip_card_trip_a">Trajet A</string>

‎app/src/main/res/values/strings.xml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ Spending %6$s %% per km</string>
493493
<string name="eco_advice_cruise">At %1$d rather than %2$d km/h, this road costs about %3$d %% less.</string>
494494
<string name="eco_advice_steadiness">Hard acceleration on %1$d %% of this drive; the same road at a steady speed costs less.</string>
495495
<string name="eco_advice_waiting">Nothing to say yet — a few minutes of driving are needed before a line appears here.</string>
496+
<string name="eco_advice_calm">Nothing to point out: hard acceleration on %1$d %% of recent driving.</string>
496497
<string name="eco_settings_title">Eco-driving coach</string>
497498
<string name="eco_settings_advice">Show driving advice</string>
498499
<string name="eco_settings_voice">Speak the advice</string>
@@ -527,6 +528,11 @@ Spending %6$s %% per km</string>
527528
<string name="trip_overview_no_driving">No driving with speed and power recorded in this window yet.</string>
528529
<string name="trip_overview_legend">Dashed line: your reference, %1$s. Amber above it, blue below. Double-tap a graph for full screen.</string>
529530
<string name="trip_overview_trace_description">Net consumption over the last %1$s against your reference</string>
531+
<string name="trace_axis_consumption">kWh/100 km</string>
532+
<string name="trace_axis_distance">km before now</string>
533+
<string name="trace_legend_above">Above reference</string>
534+
<string name="trace_legend_below">Below reference</string>
535+
<string name="trace_legend_reference">Reference</string>
530536
<string name="trip_card_current">Current trip</string>
531537
<string name="trip_card_since_charge">Since last charge</string>
532538
<string name="trip_card_trip_a">Trip A</string>

‎app/src/test/java/com/evsuite/chargepilot/EcoTripReviewTest.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ class EcoTripReviewTest {
236236
@Test
237237
fun `nothing measurable is a stated reason rather than an empty review`() {
238238
val result = review(
239-
trip = trip(endSoc = 89f, samples = track(speedKmh = 30f)),
239+
trip = trip(endSoc = 89f, samples = null),
240240
socModel = null,
241241
attribution = null,
242242
whatIf = null,
@@ -247,6 +247,17 @@ class EcoTripReviewTest {
247247
)
248248
}
249249

250+
@Test
251+
fun `a calm drive is reviewed even before any model is trained`() {
252+
val result = review(
253+
trip = trip(endSoc = 89f, samples = track(speedKmh = 30f)),
254+
socModel = null,
255+
attribution = null,
256+
whatIf = null,
257+
) as EcoTripReview.Ready
258+
assertEquals(listOf(EcoFinding.Steadiness(0.0)), result.findings)
259+
}
260+
250261
@Test
251262
fun `describe carries the verdict and every finding it kept`() {
252263
val lines = EcoTripReviewer.describe(review(trip = trip(samples = harshTrack())))

0 commit comments

Comments
 (0)