Heart Rate chart: time weighted avarage calculation

This commit is contained in:
Me7c7
2025-08-21 21:08:43 +02:00
committed by José Rebelo
parent 87491c90c7
commit 1c9290c3c3
2 changed files with 84 additions and 15 deletions
@@ -31,8 +31,9 @@ import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
@@ -41,15 +42,17 @@ 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.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.TimeWeightedAverageAccumulator;
public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeriodFragment.HeartRatePeriodData> { public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeriodFragment.HeartRatePeriodData> {
protected static final Logger LOG = LoggerFactory.getLogger(HeartRatePeriodFragment.class);
static int SEC_PER_DAY = 24 * 60 * 60; static int SEC_PER_DAY = 24 * 60 * 60;
static int DATA_INVALID = -1; static int DATA_INVALID = -1;
@@ -95,9 +98,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false); View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> { rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> getChartsHost().enableSwipeRefresh(scrollY == 0));
getChartsHost().enableSwipeRefresh(scrollY == 0);
});
} }
mDateView = rootView.findViewById(R.id.hr_date_view); mDateView = rootView.findViewById(R.id.hr_date_view);
@@ -141,7 +142,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = TEXT_COLOR = GBApplication.getTextColor(getContext()); DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = TEXT_COLOR = GBApplication.getTextColor(getContext());
if (prefs.getBoolean("chart_heartrate_color", false)) { if (prefs.getBoolean("chart_heartrate_color", false)) {
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative); HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
}else{ } else {
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate); HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
} }
HEARTRATE_MIN_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_minimum); HEARTRATE_MIN_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_minimum);
@@ -152,6 +153,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
private HeartRateData fetchHeartRateDataForDay(ChartsHost chartsHost, DBHandler db, GBDevice device, int startTs) { private HeartRateData fetchHeartRateDataForDay(ChartsHost chartsHost, DBHandler db, GBDevice device, int startTs) {
int endTs = startTs + SEC_PER_DAY - 1; int endTs = startTs + SEC_PER_DAY - 1;
List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs); List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
final HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
int restingHeartRate = DATA_INVALID; int restingHeartRate = DATA_INVALID;
if (supportsHeartRateRestingMeasurement()) { if (supportsHeartRateRestingMeasurement()) {
@@ -163,12 +165,12 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
.map(HeartRateSample::getHeartRate) .map(HeartRateSample::getHeartRate)
.orElse(DATA_INVALID); .orElse(DATA_INVALID);
} }
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
final Accumulator accumulator = new Accumulator(); final TimeWeightedAverageAccumulator accumulator = new TimeWeightedAverageAccumulator(60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES, 60);
for (int i = 0; i < samples.size(); i++) { for (int i = 0; i < samples.size(); i++) {
final ActivitySample sample = samples.get(i); final ActivitySample sample = samples.get(i);
if (heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) { if (heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
accumulator.add(sample.getHeartRate()); accumulator.add(sample.getTimestamp(), sample.getHeartRate());
} }
} }
@@ -184,7 +186,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
Pair<Integer, Integer> startAndEndTs = getStartAndEndTS(); Pair<Integer, Integer> startAndEndTs = getStartAndEndTS();
final int startTs = startAndEndTs.getKey(); final int startTs = startAndEndTs.getKey();
List<HeartRateData> result = new ArrayList<HeartRateData>(); List<HeartRateData> result = new ArrayList<>();
for (int i = 0; i < TOTAL_DAYS; i++) { for (int i = 0; i < TOTAL_DAYS; i++) {
HeartRateData dayData = fetchHeartRateDataForDay(chartsHost, db, device, startTs + i * SEC_PER_DAY); HeartRateData dayData = fetchHeartRateDataForDay(chartsHost, db, device, startTs + i * SEC_PER_DAY);
result.add(dayData); result.add(dayData);
@@ -215,8 +217,8 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
YAxis yAxisRight = hrLineChart.getAxisRight(); YAxis yAxisRight = hrLineChart.getAxisRight();
yAxisRight.setDrawLabels(true); yAxisRight.setDrawLabels(true);
YAxis[] yAxixArr = {yAxisLeft, yAxisRight}; YAxis[] yAxisArr = {yAxisLeft, yAxisRight};
for (YAxis y : yAxixArr) { for (YAxis y : yAxisArr) {
y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate()); y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate()); y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
y.setDrawGridLines(false); y.setDrawGridLines(false);
@@ -318,7 +320,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
final int startTs = startAndEndTs.getKey(); final int startTs = startAndEndTs.getKey();
final int endTs = startAndEndTs.getValue(); final int endTs = startAndEndTs.getValue();
Date date = new Date((long) endTs * 1000); //Date date = new Date((long) endTs * 1000);
mDateView.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd())); mDateView.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd()));
final XAxis x = hrLineChart.getXAxis(); final XAxis x = hrLineChart.getXAxis();
if (TOTAL_DAYS == 1) { if (TOTAL_DAYS == 1) {
@@ -430,7 +432,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
ValueFormatter formatter = new ValueFormatter() { ValueFormatter formatter = new ValueFormatter() {
@Override @Override
public String getFormattedValue(float value) { public String getFormattedValue(float value) {
int ts = startTs + SEC_PER_DAY * (int)value; int ts = startTs + SEC_PER_DAY * (int) value;
return formatDay.format(new Date(ts * 1000L)); return formatDay.format(new Date(ts * 1000L));
} }
}; };
@@ -442,7 +444,7 @@ public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeri
final int restingAvg = restingAccumulator.getCount() > 0 ? (int) Math.round(restingAccumulator.getAverage()) : DATA_INVALID; final int restingAvg = restingAccumulator.getCount() > 0 ? (int) Math.round(restingAccumulator.getAverage()) : DATA_INVALID;
setStatistics(average, minimum, maximum, restingAvg); setStatistics(average, minimum, maximum, restingAvg);
List<ILineDataSet> dataSets = new ArrayList<ILineDataSet>(); List<ILineDataSet> dataSets = new ArrayList<>();
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) { if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
dataSets.add(createHeartRateDataSet(avgLineData, HEARTRATE_COLOR)); dataSets.add(createHeartRateDataSet(avgLineData, HEARTRATE_COLOR));
} }
@@ -0,0 +1,67 @@
package nodomain.freeyourgadget.gadgetbridge.util;
public class TimeWeightedAverageAccumulator {
private long lastTimestamp;
private int count;
private double min = Double.MAX_VALUE;
private double max = -Double.MAX_VALUE;
private double weightedSum = 0.0;
private long totalWeight = 0;
private boolean first = true;
private final long defaultTimeDiff;
private final long maxTimeDiff;
public TimeWeightedAverageAccumulator(long maxTimeDiff, long defaultTimeDiff) {
this.maxTimeDiff = maxTimeDiff;
this.defaultTimeDiff = defaultTimeDiff;
this.count = 0;
}
public void add(long timestamp, double value) {
if (value > max) {
max = value;
}
if (value < min) {
min = value;
}
if (first) {
if (defaultTimeDiff > 0) {
weightedSum = value * defaultTimeDiff;
totalWeight = defaultTimeDiff;
} else {
weightedSum = 0;
totalWeight = 0;
}
lastTimestamp = timestamp;
first = false;
return;
}
long timeDiff = timestamp - lastTimestamp;
if (maxTimeDiff > 0 && defaultTimeDiff > 0 && timeDiff > maxTimeDiff) {
timeDiff = defaultTimeDiff;
}
weightedSum += value * timeDiff;
totalWeight += timeDiff;
lastTimestamp = timestamp;
count++;
}
public double getAverage() {
return totalWeight == 0 ? 0.0 : weightedSum / totalWeight;
}
public double getMin() {
return min;
}
public double getMax() {
return max;
}
public int getCount() {
return count;
}
}