Charts: Determine HR sample gaps dynamically

This commit is contained in:
José Rebelo 2025-01-06 21:14:23 +00:00
parent c0883de546
commit 699e91d8a0
4 changed files with 84 additions and 73 deletions

View File

@ -32,7 +32,6 @@ import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.Entry; import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
@ -41,7 +40,6 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -57,6 +55,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint; import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils;
public class ActivitySummariesChartFragment extends AbstractActivityChartFragment<ChartsData> { public class ActivitySummariesChartFragment extends AbstractActivityChartFragment<ChartsData> {
@ -267,27 +266,12 @@ public class ActivitySummariesChartFragment extends AbstractActivityChartFragmen
tsTranslation = new TimestampTranslation(); tsTranslation = new TimestampTranslation();
} }
final List<Entry> heartRateEntries = new ArrayList<>(activityPoints.size()); final List<Entry> heartRateEntries = activityPoints.stream()
final List<ILineDataSet> heartRateDataSets = new ArrayList<>(); .filter(ap -> HeartRateUtils.getInstance().isValidHeartRateValue(ap.getHeartRate()))
int lastTsShorten = 0; .map(ap -> new Entry(tsTranslation.shorten((int) (ap.getTime().getTime() / 1000)), ap.getHeartRate()))
for (final ActivityPoint activityPoint : activityPoints) { .collect(Collectors.toList());
int tsShorten = tsTranslation.shorten((int) (activityPoint.getTime().getTime() / 1000));
if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { final List<ILineDataSet> heartRateDataSets = ChartUtils.findGaps(heartRateEntries, l -> createHeartrateSet(l, "Heart Rate"));
heartRateEntries.add(new Entry(tsShorten, activityPoint.getHeartRate()));
} else {
if (!heartRateEntries.isEmpty()) {
List<Entry> clone = new ArrayList<>(heartRateEntries.size());
clone.addAll(heartRateEntries);
heartRateDataSets.add(createHeartrateSet(clone, "Heart Rate"));
heartRateEntries.clear();
}
}
lastTsShorten = tsShorten;
heartRateEntries.add(new Entry(tsShorten, activityPoint.getHeartRate()));
}
if (!heartRateEntries.isEmpty()) {
heartRateDataSets.add(createHeartrateSet(heartRateEntries, "Heart Rate"));
}
if (activitySamplesData != null) { if (activitySamplesData != null) {
// if we have activity samples, replace the heart rate dataset // if we have activity samples, replace the heart rate dataset

View File

@ -35,6 +35,7 @@ import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.GregorianCalendar; import java.util.GregorianCalendar;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
@ -46,6 +47,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public abstract class AbstractActivityChartFragment<D extends ChartsData> extends AbstractChartFragment<D> { public abstract class AbstractActivityChartFragment<D extends ChartsData> extends AbstractChartFragment<D> {
@ -270,33 +272,20 @@ public abstract class AbstractActivityChartFragment<D extends ChartsData> extend
} }
boolean hr = supportsHeartrate(gbDevice); boolean hr = supportsHeartrate(gbDevice);
final List<Entry> heartRateLineEntries = new ArrayList<>(); final List<ILineDataSet> heartRateDataSets;
final List<ILineDataSet> heartRateDataSets = new ArrayList<>();
int lastTsShorten = 0;
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
// Currently only for HR // Currently only for HR
if (hr) { if (hr) {
for (ActivitySample sample : highResSamples) { final List<Entry> heartRateLineEntries = highResSamples.stream()
if (sample.getKind() != ActivityKind.NOT_WORN && heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { .filter(s -> s.getKind() != ActivityKind.NOT_WORN)
int tsShorten = tsTranslation.shorten(sample.getTimestamp()); .filter(s -> heartRateUtilsInstance.isValidHeartRateValue(s.getHeartRate()))
if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) { .map(s -> new Entry(tsTranslation.shorten(s.getTimestamp()), s.getHeartRate()))
heartRateLineEntries.add(new Entry(tsShorten, sample.getHeartRate())); .collect(Collectors.toList());
heartRateDataSets = ChartUtils.findGaps(heartRateLineEntries, l -> createHeartrateSet(l, "Heart Rate"));
} else { } else {
if (!heartRateLineEntries.isEmpty()) { heartRateDataSets = new ArrayList<>();
List<Entry> clone = new ArrayList<>(heartRateLineEntries.size());
clone.addAll(heartRateLineEntries);
heartRateDataSets.add(createHeartrateSet(clone, "Heart Rate"));
heartRateLineEntries.clear();
}
}
lastTsShorten = tsShorten;
heartRateLineEntries.add(new Entry(tsShorten, sample.getHeartRate()));
}
}
}
if (!heartRateLineEntries.isEmpty()) {
heartRateDataSets.add(createHeartrateSet(heartRateLineEntries, "Heart Rate"));
} }
// convert Entry Lists to Datasets // convert Entry Lists to Datasets

View File

@ -28,6 +28,7 @@ import java.util.Calendar;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
@ -36,10 +37,10 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator; import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
import nodomain.freeyourgadget.gadgetbridge.util.ChartUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDailyFragment.HeartRateData> { public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDailyFragment.HeartRateData> {
@ -239,36 +240,16 @@ public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDaily
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance(); HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
final TimestampTranslation tsTranslation = new TimestampTranslation(); final TimestampTranslation tsTranslation = new TimestampTranslation();
final List<Entry> lineEntries = new ArrayList<>();
List<? extends ActivitySample> samples = data.samples; List<? extends ActivitySample> samples = data.samples;
final Accumulator accumulator = new Accumulator(); final Accumulator accumulator = new Accumulator();
final List<ILineDataSet> lineDataSets = new ArrayList<>(); final List<Entry> heartRateLineEntries = samples.stream()
int lastTsShorten = 0; .filter(s -> heartRateUtilsInstance.isValidHeartRateValue(s.getHeartRate()))
for (int i =0; i < samples.size(); i++) { .peek(s -> accumulator.add(s.getHeartRate()))
final ActivitySample sample = samples.get(i); .map(s -> new Entry(tsTranslation.shorten(s.getTimestamp()), s.getHeartRate()))
final int tsShorten = tsTranslation.shorten(sample.getTimestamp()); .collect(Collectors.toList());
if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
continue;
}
if (lastTsShorten == 0 || (tsShorten - lastTsShorten) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
lineEntries.add(new Entry(tsShorten, sample.getHeartRate()));
} else {
if (!lineEntries.isEmpty()) {
List<Entry> clone = new ArrayList<>(lineEntries.size());
clone.addAll(lineEntries);
lineDataSets.add(createHeartRateDataSet(clone));
lineEntries.clear();
}
}
lastTsShorten = tsShorten;
lineEntries.add(new Entry(tsShorten, sample.getHeartRate()));
accumulator.add(sample.getHeartRate());
}
if (!lineEntries.isEmpty()) { final List<ILineDataSet> lineDataSets = ChartUtils.findGaps(heartRateLineEntries, this::createHeartRateDataSet);
lineDataSets.add(createHeartRateDataSet(lineEntries));
}
final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1; final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : -1;
final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1; final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : -1;

View File

@ -0,0 +1,57 @@
package nodomain.freeyourgadget.gadgetbridge.util;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
public final class ChartUtils {
private ChartUtils() {
// utility class
}
/**
* Find the gaps in a list of values with potentially varying sample rates, and create separate
* datasets for each.
*/
public static List<ILineDataSet> findGaps(final List<Entry> values,
final Function<List<Entry>, ILineDataSet> datasetCreator) {
if (values.isEmpty()) {
return Collections.emptyList();
} else if (values.size() == 1) {
return Collections.singletonList(datasetCreator.apply(values));
}
final List<ILineDataSet> ret = new ArrayList<>();
int lastStart = 0;
float lastGap = -1;
for (int i = 0; i < values.size() - 1; i++) {
final float gapToNext = values.get(i + 1).getX() - values.get(i).getX();
if (lastGap >= 0) {
if (gapToNext >= lastGap * 5 && i > lastStart) {
// sample rate decreased - insert a gap
ret.add(datasetCreator.apply(values.subList(lastStart, i + 1)));
lastGap = -1;
lastStart = i + 1;
continue;
} else if (gapToNext < lastGap / 5 && i - 1 > lastStart) {
// sample rate increased drastically (workout start?) - insert a gap
ret.add(datasetCreator.apply(values.subList(lastStart, i)));
lastStart = i;
}
}
lastGap = gapToNext;
}
if (lastStart < values.size()) {
ret.add(datasetCreator.apply(values.subList(lastStart, values.size())));
}
return ret;
}
}