feat: avg pace over moving time (fixes elapsed-based pace on paused activities); show mid-activity pauses on Body Battery band via active_spans
This commit is contained in:
@@ -159,6 +159,21 @@ function ActivityRefLabel({ viewBox, sport }) {
|
||||
)
|
||||
}
|
||||
|
||||
// A small ⏸ glyph drawn over the gap between two active spans, marking where
|
||||
// the recording was paused mid-activity (e.g. a long lunch break on a ride).
|
||||
function PauseRefLabel({ viewBox }) {
|
||||
if (!viewBox) return null
|
||||
const { x, y, width = 0, height = 0 } = viewBox
|
||||
const cx = x + width / 2, cy = y + height / 2
|
||||
const barW = 2, barH = 8, gap = 1.5
|
||||
return (
|
||||
<g style={{ pointerEvents: 'none' }}>
|
||||
<rect x={cx - gap - barW} y={cy - barH / 2} width={barW} height={barH} fill="#fff" opacity={0.85} />
|
||||
<rect x={cx + gap} y={cy - barH / 2} width={barW} height={barH} fill="#fff" opacity={0.85} />
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
// Activity time spans are drawn as a solid coloured band in a reserved strip
|
||||
// *below* the battery bars (the negative Y region) so they don't obscure the data.
|
||||
const ACTIVITY_BAND_BOTTOM = -18
|
||||
@@ -232,17 +247,42 @@ function BodyBatteryChart({ bb, hiresValues, sleepStart, sleepEnd, activities })
|
||||
<Cell key={i} fill={BB_INFERRED_COLOR[d.type]} />
|
||||
))}
|
||||
</Bar>
|
||||
{(activities || []).map(a => {
|
||||
const start = new Date(a.start_time).getTime()
|
||||
const end = a.duration_s ? start + a.duration_s * 1000 : start
|
||||
const x1 = nearestT(start), x2 = nearestT(end)
|
||||
if (x1 == null || x2 == null) return null
|
||||
{(activities || []).flatMap(a => {
|
||||
const color = sportColor(a.sport_type)
|
||||
return (
|
||||
<ReferenceArea key={`area-${a.id}`} x1={x1} x2={x2} y1={0} y2={ACTIVITY_BAND_BOTTOM}
|
||||
fill={color} fillOpacity={0.9} stroke={color} strokeOpacity={1}
|
||||
label={<ActivityRefLabel sport={a.sport_type} />} />
|
||||
)
|
||||
const start = new Date(a.start_time).getTime()
|
||||
const fullEnd = a.duration_s ? start + a.duration_s * 1000 : start
|
||||
// active_spans (set only when a long pause splits the recording)
|
||||
// draws one band per moving span with the pause shown as a gap;
|
||||
// otherwise one continuous band as before.
|
||||
const spans = (a.active_spans && a.active_spans.length > 1)
|
||||
? a.active_spans
|
||||
: [[start, fullEnd]]
|
||||
// Carry the sport icon on the longest span only.
|
||||
let labelIdx = 0, labelLen = -1
|
||||
spans.forEach((s, i) => { const l = s[1] - s[0]; if (l > labelLen) { labelLen = l; labelIdx = i } })
|
||||
const els = []
|
||||
spans.forEach((s, i) => {
|
||||
const x1 = nearestT(s[0]), x2 = nearestT(s[1])
|
||||
if (x1 != null && x2 != null) {
|
||||
els.push(
|
||||
<ReferenceArea key={`area-${a.id}-${i}`} x1={x1} x2={x2} y1={0} y2={ACTIVITY_BAND_BOTTOM}
|
||||
fill={color} fillOpacity={0.9} stroke={color} strokeOpacity={1}
|
||||
label={i === labelIdx ? <ActivityRefLabel sport={a.sport_type} /> : undefined} />
|
||||
)
|
||||
}
|
||||
// Mark the paused stretch before the next span with a faint dashed strip + ⏸.
|
||||
if (i < spans.length - 1) {
|
||||
const g1 = nearestT(s[1]), g2 = nearestT(spans[i + 1][0])
|
||||
if (g1 != null && g2 != null && g1 !== g2) {
|
||||
els.push(
|
||||
<ReferenceArea key={`pause-${a.id}-${i}`} x1={g1} x2={g2} y1={0} y2={ACTIVITY_BAND_BOTTOM}
|
||||
fill={color} fillOpacity={0.12} stroke={color} strokeOpacity={0.7} strokeDasharray="3 3"
|
||||
label={<PauseRefLabel />} />
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
return els
|
||||
})}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
Reference in New Issue
Block a user