mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-03-13 01:30:09 +01:00
HeartRateChart: Add weekly and monthly view
This commit is contained in:
parent
aa3f677d47
commit
3813e03eb2
@ -170,7 +170,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
case "sleep":
|
||||
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||
case "heartrate":
|
||||
return new HeartRateDailyFragment();
|
||||
return HeartRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||
case "hrvstatus":
|
||||
return new HRVStatusFragment();
|
||||
case "bodyenergy":
|
||||
|
@ -0,0 +1,44 @@
|
||||
/* Copyright (C) 2024 a0z, José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.adapter.HeartRateFragmentAdapter;
|
||||
|
||||
public class HeartRateCollectionFragment extends AbstractCollectionFragment {
|
||||
public HeartRateCollectionFragment() {
|
||||
|
||||
}
|
||||
|
||||
public static HeartRateCollectionFragment newInstance(final boolean allowSwipe) {
|
||||
final HeartRateCollectionFragment fragment = new HeartRateCollectionFragment();
|
||||
final Bundle args = new Bundle();
|
||||
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
||||
return new HeartRateFragmentAdapter(this, getChildFragmentManager());
|
||||
}
|
||||
}
|
@ -1,322 +0,0 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.LineChart;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
import com.github.mikephil.charting.components.LimitLine;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
import com.github.mikephil.charting.data.LineData;
|
||||
import com.github.mikephil.charting.data.LineDataSet;
|
||||
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class HeartRateDailyFragment extends AbstractChartFragment<HeartRateDailyFragment.HeartRateData> {
|
||||
|
||||
protected int HEARTRATE_COLOR;
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int BACKGROUND_COLOR;
|
||||
protected int DESCRIPTION_COLOR;
|
||||
protected int LEGEND_TEXT_COLOR;
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView hrResting;
|
||||
private TextView hrAverage;
|
||||
private TextView hrMinimum;
|
||||
private TextView hrMaximum;
|
||||
private LineChart hrLineChart;
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
|
||||
getChartsHost().enableSwipeRefresh(scrollY == 0);
|
||||
});
|
||||
}
|
||||
|
||||
mDateView = rootView.findViewById(R.id.hr_date_view);
|
||||
hrLineChart = rootView.findViewById(R.id.heart_rate_line_chart);
|
||||
hrResting = rootView.findViewById(R.id.hr_resting);
|
||||
hrAverage = rootView.findViewById(R.id.hr_average);
|
||||
hrMinimum = rootView.findViewById(R.id.hr_minimum);
|
||||
hrMaximum = rootView.findViewById(R.id.hr_maximum);
|
||||
final LinearLayout heartRateRestingWrapper = rootView.findViewById(R.id.hr_resting_wrapper);
|
||||
|
||||
setupChart();
|
||||
refresh();
|
||||
setupLegend(hrLineChart);
|
||||
|
||||
if (!supportsHeartRateRestingMeasurement()) {
|
||||
heartRateRestingWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public boolean supportsHeartRateRestingMeasurement() {
|
||||
final GBDevice device = getChartsHost().getDevice();
|
||||
return device.getDeviceCoordinator().supportsHeartRateRestingMeasurement(device);
|
||||
}
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.heart_rate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
|
||||
DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = GBApplication.getTextColor(getContext());
|
||||
if (prefs.getBoolean("chart_heartrate_color", false)) {
|
||||
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
|
||||
}else{
|
||||
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HeartRateDailyFragment.HeartRateData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(chartsHost.getEndDate());
|
||||
day.add(Calendar.DATE, 0);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
|
||||
List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
|
||||
|
||||
int restingHeartRate = -1;
|
||||
if (supportsHeartRateRestingMeasurement()) {
|
||||
restingHeartRate = device.getDeviceCoordinator()
|
||||
.getHeartRateRestingSampleProvider(device, db.getDaoSession())
|
||||
.getAllSamples(startTs * 1000L, endTs * 1000L)
|
||||
.stream()
|
||||
.max(Comparator.comparingLong(HeartRateSample::getTimestamp))
|
||||
.map(HeartRateSample::getHeartRate)
|
||||
.orElse(-1);
|
||||
}
|
||||
|
||||
return new HeartRateData(samples, restingHeartRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
hrLineChart.invalidate();
|
||||
}
|
||||
|
||||
private void setupChart() {
|
||||
hrLineChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
hrLineChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
hrLineChart.getDescription().setEnabled(false);
|
||||
|
||||
|
||||
XAxis x = hrLineChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
x.setPosition(XAxis.XAxisPosition.BOTTOM);
|
||||
x.setAxisMinimum(0f);
|
||||
x.setAxisMaximum(86400f);
|
||||
|
||||
YAxis y = hrLineChart.getAxisLeft();
|
||||
y.setDrawGridLines(false);
|
||||
y.setDrawTopYLabelEntry(true);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
y.setEnabled(true);
|
||||
y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
|
||||
y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
|
||||
|
||||
YAxis yAxisRight = hrLineChart.getAxisRight();
|
||||
yAxisRight.setDrawGridLines(false);
|
||||
yAxisRight.setDrawLabels(true);
|
||||
yAxisRight.setDrawTopYLabelEntry(true);
|
||||
yAxisRight.setTextColor(CHART_TEXT_COLOR);
|
||||
yAxisRight.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
|
||||
yAxisRight.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart<?> chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(1);
|
||||
LegendEntry hrEntry = new LegendEntry();
|
||||
hrEntry.label = getTitle();
|
||||
hrEntry.formColor = HEARTRATE_COLOR;
|
||||
legendEntries.add(hrEntry);
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
LegendEntry hrAverageEntry = new LegendEntry();
|
||||
hrAverageEntry.label = getString(R.string.hr_average);
|
||||
hrAverageEntry.formColor = Color.RED;
|
||||
legendEntries.add(hrAverageEntry);
|
||||
}
|
||||
|
||||
//if (supportsHeartRateRestingMeasurement()) {
|
||||
// LegendEntry hrRestingEntry = new LegendEntry();
|
||||
// hrRestingEntry.label = getString(R.string.hr_resting);
|
||||
// hrRestingEntry.formColor = Color.GRAY;
|
||||
// legendEntries.add(hrRestingEntry);
|
||||
//}
|
||||
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
chart.getLegend().setWordWrapEnabled(true);
|
||||
}
|
||||
|
||||
protected LineDataSet createHeartRateDataSet(final List<Entry> values) {
|
||||
LineDataSet dataSet = new LineDataSet(values, "Heart Rate");
|
||||
dataSet.setLineWidth(1.5f);
|
||||
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
|
||||
dataSet.setCubicIntensity(0.1f);
|
||||
dataSet.setDrawCircles(false);
|
||||
dataSet.setDrawValues(true);
|
||||
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
|
||||
dataSet.setColor(HEARTRATE_COLOR);
|
||||
dataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
return dataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HeartRateDailyFragment.HeartRateData data) {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(getEndDate());
|
||||
day.add(Calendar.DATE, 0);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000);
|
||||
int endTs = startTs + 24 * 60 * 60 - 1;
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
|
||||
final TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
final List<Entry> lineEntries = new ArrayList<>();
|
||||
List<? extends ActivitySample> samples = data.samples;
|
||||
final Accumulator accumulator = new Accumulator();
|
||||
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
int lastTsShorten = 0;
|
||||
for (int i =0; i < samples.size(); i++) {
|
||||
final ActivitySample sample = samples.get(i);
|
||||
final int tsShorten = tsTranslation.shorten(sample.getTimestamp());
|
||||
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()) {
|
||||
lineDataSets.add(createHeartRateDataSet(lineEntries));
|
||||
}
|
||||
|
||||
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 maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : -1;
|
||||
|
||||
hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
|
||||
hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
|
||||
hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
|
||||
hrResting.setText(data.restingHeartRate > 0 ? getString(R.string.bpm_value_unit, data.restingHeartRate) : "-");
|
||||
|
||||
if (minimum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
}
|
||||
if (maximum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMaximum(maximum + 30);
|
||||
hrLineChart.getAxisRight().setAxisMaximum(maximum + 30);
|
||||
}
|
||||
|
||||
hrLineChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||
hrLineChart.setData(new LineData(lineDataSets));
|
||||
|
||||
hrLineChart.getAxisLeft().removeAllLimitLines();
|
||||
|
||||
if (average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
final LimitLine averageLine = new LimitLine(average);
|
||||
averageLine.setLineWidth(1.5f);
|
||||
averageLine.enableDashedLine(15f, 10f, 0f);
|
||||
averageLine.setLineColor(Color.RED);
|
||||
hrLineChart.getAxisLeft().addLimitLine(averageLine);
|
||||
}
|
||||
|
||||
//if (data.restingHeartRate > 0) {
|
||||
// final LimitLine restingLine = new LimitLine(data.restingHeartRate);
|
||||
// restingLine.setLineWidth(1.5f);
|
||||
// restingLine.enableDashedLine(15f, 10f, 0f);
|
||||
// restingLine.setLineColor(Color.GRAY);
|
||||
// hrLineChart.getAxisLeft().addLimitLine(restingLine);
|
||||
//}
|
||||
}
|
||||
|
||||
protected static class HeartRateData extends ChartsData {
|
||||
public List<? extends ActivitySample> samples;
|
||||
public int restingHeartRate;
|
||||
|
||||
protected HeartRateData(List<? extends ActivitySample> samples, int restingHeartRate) {
|
||||
this.samples = samples;
|
||||
this.restingHeartRate = restingHeartRate;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,471 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
import com.github.mikephil.charting.charts.LineChart;
|
||||
import com.github.mikephil.charting.components.LegendEntry;
|
||||
import com.github.mikephil.charting.components.LimitLine;
|
||||
import com.github.mikephil.charting.components.XAxis;
|
||||
import com.github.mikephil.charting.components.YAxis;
|
||||
import com.github.mikephil.charting.data.Entry;
|
||||
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.interfaces.datasets.ILineDataSet;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.HeartRateUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class HeartRatePeriodFragment extends AbstractChartFragment<HeartRatePeriodFragment.HeartRatePeriodData> {
|
||||
|
||||
static int SEC_PER_DAY = 24 * 60 * 60;
|
||||
static int DATA_INVALID = -1;
|
||||
|
||||
protected int HEARTRATE_COLOR;
|
||||
protected int HEARTRATE_MIN_COLOR;
|
||||
protected int HEARTRATE_RESTING_COLOR;
|
||||
protected int HEARTRATE_MAX_COLOR;
|
||||
protected int CHART_TEXT_COLOR;
|
||||
protected int BACKGROUND_COLOR;
|
||||
protected int DESCRIPTION_COLOR;
|
||||
protected int LEGEND_TEXT_COLOR;
|
||||
|
||||
private TextView mDateView;
|
||||
private TextView hrResting;
|
||||
private TextView hrAverage;
|
||||
private TextView hrMinimum;
|
||||
private TextView hrMaximum;
|
||||
private LineChart hrLineChart;
|
||||
private int TOTAL_DAYS;
|
||||
|
||||
public static HeartRatePeriodFragment newInstance(int totalDays) {
|
||||
HeartRatePeriodFragment fragmentFirst = new HeartRatePeriodFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putInt("totalDays", totalDays);
|
||||
fragmentFirst.setArguments(args);
|
||||
return fragmentFirst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
TOTAL_DAYS = getArguments() != null ? getArguments().getInt("totalDays") : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_heart_rate, container, false);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
|
||||
getChartsHost().enableSwipeRefresh(scrollY == 0);
|
||||
});
|
||||
}
|
||||
|
||||
mDateView = rootView.findViewById(R.id.hr_date_view);
|
||||
hrLineChart = rootView.findViewById(R.id.heart_rate_line_chart);
|
||||
hrResting = rootView.findViewById(R.id.hr_resting);
|
||||
hrAverage = rootView.findViewById(R.id.hr_average);
|
||||
hrMinimum = rootView.findViewById(R.id.hr_minimum);
|
||||
hrMaximum = rootView.findViewById(R.id.hr_maximum);
|
||||
final LinearLayout heartRateRestingWrapper = rootView.findViewById(R.id.hr_resting_wrapper);
|
||||
|
||||
setupChart();
|
||||
refresh();
|
||||
setupLegend(hrLineChart);
|
||||
|
||||
if (!supportsHeartRateRestingMeasurement()) {
|
||||
heartRateRestingWrapper.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return rootView;
|
||||
}
|
||||
|
||||
public boolean supportsHeartRateRestingMeasurement() {
|
||||
final GBDevice device = getChartsHost().getDevice();
|
||||
return device.getDeviceCoordinator().supportsHeartRateRestingMeasurement(device);
|
||||
}
|
||||
|
||||
protected List<? extends AbstractActivitySample> getActivitySamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
|
||||
SampleProvider<? extends ActivitySample> provider = device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
|
||||
return provider.getAllActivitySamplesHighRes(tsFrom, tsTo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.heart_rate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
Prefs prefs = GBApplication.getPrefs();
|
||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(getContext());
|
||||
DESCRIPTION_COLOR = LEGEND_TEXT_COLOR = GBApplication.getTextColor(getContext());
|
||||
if (prefs.getBoolean("chart_heartrate_color", false)) {
|
||||
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_alternative);
|
||||
}else{
|
||||
HEARTRATE_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate);
|
||||
}
|
||||
HEARTRATE_MIN_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_minimum);
|
||||
HEARTRATE_MAX_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_maximum);
|
||||
HEARTRATE_RESTING_COLOR = ContextCompat.getColor(getContext(), R.color.chart_heartrate_resting);
|
||||
}
|
||||
|
||||
private HeartRateData fetchHeartRateDataForDay(ChartsHost chartsHost, DBHandler db, GBDevice device, int startTs) {
|
||||
int endTs = startTs + SEC_PER_DAY - 1;
|
||||
List<? extends ActivitySample> samples = getActivitySamples(db, device, startTs, endTs);
|
||||
|
||||
int restingHeartRate = DATA_INVALID;
|
||||
if (supportsHeartRateRestingMeasurement()) {
|
||||
restingHeartRate = device.getDeviceCoordinator()
|
||||
.getHeartRateRestingSampleProvider(device, db.getDaoSession())
|
||||
.getAllSamples(startTs * 1000L, endTs * 1000L)
|
||||
.stream()
|
||||
.max(Comparator.comparingLong(HeartRateSample::getTimestamp))
|
||||
.map(HeartRateSample::getHeartRate)
|
||||
.orElse(DATA_INVALID);
|
||||
}
|
||||
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
|
||||
final Accumulator accumulator = new Accumulator();
|
||||
for (int i = 0; i < samples.size(); i++) {
|
||||
final ActivitySample sample = samples.get(i);
|
||||
if (heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
accumulator.add(sample.getHeartRate());
|
||||
}
|
||||
}
|
||||
|
||||
final int average = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : DATA_INVALID;
|
||||
final int minimum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMin()) : DATA_INVALID;
|
||||
final int maximum = accumulator.getCount() > 0 ? (int) Math.round(accumulator.getMax()) : DATA_INVALID;
|
||||
|
||||
return new HeartRateData(samples, restingHeartRate, average, minimum, maximum);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HeartRatePeriodData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
|
||||
Pair<Integer, Integer> startAndEndTs = getStartAndEndTS();
|
||||
final int startTs = startAndEndTs.getKey();
|
||||
|
||||
List<HeartRateData> result = new ArrayList<HeartRateData>();
|
||||
for (int i = 0; i < TOTAL_DAYS; i++) {
|
||||
HeartRateData dayData = fetchHeartRateDataForDay(chartsHost, db, device, startTs + i * SEC_PER_DAY);
|
||||
result.add(dayData);
|
||||
}
|
||||
return new HeartRatePeriodData(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderCharts() {
|
||||
hrLineChart.invalidate();
|
||||
}
|
||||
|
||||
private void setupChart() {
|
||||
hrLineChart.setBackgroundColor(BACKGROUND_COLOR);
|
||||
hrLineChart.getDescription().setTextColor(DESCRIPTION_COLOR);
|
||||
hrLineChart.getDescription().setEnabled(false);
|
||||
|
||||
XAxis x = hrLineChart.getXAxis();
|
||||
x.setDrawLabels(true);
|
||||
x.setDrawGridLines(false);
|
||||
x.setEnabled(true);
|
||||
x.setTextColor(CHART_TEXT_COLOR);
|
||||
x.setDrawLimitLinesBehindData(true);
|
||||
x.setPosition(XAxis.XAxisPosition.BOTTOM);
|
||||
|
||||
YAxis yAxisLeft = hrLineChart.getAxisLeft();
|
||||
yAxisLeft.setEnabled(true);
|
||||
YAxis yAxisRight = hrLineChart.getAxisRight();
|
||||
yAxisRight.setDrawLabels(true);
|
||||
|
||||
YAxis[] yAxixArr = {yAxisLeft, yAxisRight};
|
||||
for (YAxis y : yAxixArr) {
|
||||
y.setAxisMaximum(HeartRateUtils.getInstance().getMaxHeartRate());
|
||||
y.setAxisMinimum(HeartRateUtils.getInstance().getMinHeartRate());
|
||||
y.setDrawGridLines(false);
|
||||
y.setDrawTopYLabelEntry(true);
|
||||
y.setTextColor(CHART_TEXT_COLOR);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupLegend(Chart<?> chart) {
|
||||
List<LegendEntry> legendEntries = new ArrayList<>(4);
|
||||
|
||||
if (TOTAL_DAYS == 1) {
|
||||
LegendEntry hrEntry = new LegendEntry();
|
||||
hrEntry.label = getTitle();
|
||||
hrEntry.formColor = HEARTRATE_COLOR;
|
||||
legendEntries.add(hrEntry);
|
||||
} else {
|
||||
LegendEntry hrMinEntry = new LegendEntry();
|
||||
hrMinEntry.label = getString(R.string.hr_minimum);
|
||||
hrMinEntry.formColor = HEARTRATE_MIN_COLOR;
|
||||
legendEntries.add(hrMinEntry);
|
||||
}
|
||||
|
||||
if (supportsHeartRateRestingMeasurement() && TOTAL_DAYS != 1) {
|
||||
LegendEntry hrRestingEntry = new LegendEntry();
|
||||
hrRestingEntry.label = getString(R.string.hr_resting);
|
||||
hrRestingEntry.formColor = HEARTRATE_RESTING_COLOR;
|
||||
legendEntries.add(hrRestingEntry);
|
||||
}
|
||||
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
LegendEntry hrAverageEntry = new LegendEntry();
|
||||
hrAverageEntry.label = getString(R.string.hr_average);
|
||||
hrAverageEntry.formColor = TOTAL_DAYS != 1 ? HEARTRATE_COLOR : Color.RED;
|
||||
legendEntries.add(hrAverageEntry);
|
||||
}
|
||||
|
||||
if (TOTAL_DAYS != 1) {
|
||||
LegendEntry hrMaxEntry = new LegendEntry();
|
||||
hrMaxEntry.label = getString(R.string.hr_maximum);
|
||||
hrMaxEntry.formColor = HEARTRATE_MAX_COLOR;
|
||||
legendEntries.add(hrMaxEntry);
|
||||
}
|
||||
|
||||
chart.getLegend().setCustom(legendEntries);
|
||||
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
|
||||
chart.getLegend().setWordWrapEnabled(true);
|
||||
}
|
||||
|
||||
protected LineDataSet createHeartRateDataSet(final List<Entry> values, int color) {
|
||||
LineDataSet dataSet = new LineDataSet(values, "Heart Rate");
|
||||
dataSet.setLineWidth(1.5f);
|
||||
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
|
||||
dataSet.setCubicIntensity(0.1f);
|
||||
dataSet.setDrawCircles(false);
|
||||
dataSet.setDrawValues(true);
|
||||
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
|
||||
dataSet.setColor(color);
|
||||
dataSet.setValueTextColor(CHART_TEXT_COLOR);
|
||||
return dataSet;
|
||||
}
|
||||
|
||||
private Pair<Integer, Integer> getStartAndEndTS() {
|
||||
Calendar day = Calendar.getInstance();
|
||||
day.setTime(getEndDate());
|
||||
day.add(Calendar.DATE, 0);
|
||||
day.set(Calendar.HOUR_OF_DAY, 0);
|
||||
day.set(Calendar.MINUTE, 0);
|
||||
day.set(Calendar.SECOND, 0);
|
||||
day.add(Calendar.HOUR, 0);
|
||||
int startTs = (int) (day.getTimeInMillis() / 1000) - SEC_PER_DAY * (TOTAL_DAYS - 1);
|
||||
int endTs = startTs + SEC_PER_DAY * TOTAL_DAYS - 1;
|
||||
return Pair.of(startTs, endTs);
|
||||
}
|
||||
|
||||
private void setStatistics(int average, int minimum, int maximum, int resting) {
|
||||
hrAverage.setText(average > 0 ? getString(R.string.bpm_value_unit, average) : "-");
|
||||
hrMinimum.setText(minimum > 0 ? getString(R.string.bpm_value_unit, minimum) : "-");
|
||||
hrMaximum.setText(maximum > 0 ? getString(R.string.bpm_value_unit, maximum) : "-");
|
||||
hrResting.setText(resting > 0 ? getString(R.string.bpm_value_unit, resting) : "-");
|
||||
|
||||
if (minimum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
hrLineChart.getAxisRight().setAxisMinimum(Math.max(minimum - 30, 0));
|
||||
}
|
||||
if (maximum > 0) {
|
||||
hrLineChart.getAxisLeft().setAxisMaximum(maximum + 30);
|
||||
hrLineChart.getAxisRight().setAxisMaximum(maximum + 30);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChartsnUIThread(HeartRatePeriodData data) {
|
||||
Pair<Integer, Integer> startAndEndTs = getStartAndEndTS();
|
||||
final int startTs = startAndEndTs.getKey();
|
||||
final int endTs = startAndEndTs.getValue();
|
||||
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
mDateView.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd()));
|
||||
final XAxis x = hrLineChart.getXAxis();
|
||||
if (TOTAL_DAYS == 1) {
|
||||
setOneDayData(data.samples.get(0), endTs);
|
||||
x.setAxisMinimum(0f);
|
||||
x.setAxisMaximum(86400f);
|
||||
} else {
|
||||
setMultipleDaysData(data, startTs, endTs);
|
||||
x.setAxisMinimum(0);
|
||||
// If the timestamp is used as XAxis, the chart library formats
|
||||
// the labels not at 0:00, which causes a shift in the labels
|
||||
x.setAxisMaximum(TOTAL_DAYS - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private void setOneDayData(HeartRateData data, int endTs) {
|
||||
Date date = new Date((long) endTs * 1000);
|
||||
String formattedDate = new SimpleDateFormat("E, MMM dd").format(date);
|
||||
mDateView.setText(formattedDate);
|
||||
|
||||
HeartRateUtils heartRateUtilsInstance = HeartRateUtils.getInstance();
|
||||
final List<Entry> lineEntries = new ArrayList<>();
|
||||
List<? extends ActivitySample> samples = data.samples;
|
||||
final TimestampTranslation tsTranslation = new TimestampTranslation();
|
||||
|
||||
final List<ILineDataSet> lineDataSets = new ArrayList<>();
|
||||
int lastTs = 0;
|
||||
for (int i = 0; i < samples.size(); i++) {
|
||||
final ActivitySample sample = samples.get(i);
|
||||
if (!heartRateUtilsInstance.isValidHeartRateValue(sample.getHeartRate())) {
|
||||
continue;
|
||||
}
|
||||
final int ts = sample.getTimestamp();
|
||||
if (lastTs == 0 || (ts - lastTs) <= 60 * HeartRateUtils.MAX_HR_MEASUREMENTS_GAP_MINUTES) {
|
||||
lineEntries.add(new Entry(tsTranslation.shorten(ts), sample.getHeartRate()));
|
||||
} else {
|
||||
if (!lineEntries.isEmpty()) {
|
||||
List<Entry> clone = new ArrayList<>(lineEntries.size());
|
||||
clone.addAll(lineEntries);
|
||||
lineDataSets.add(createHeartRateDataSet(clone, HEARTRATE_COLOR));
|
||||
lineEntries.clear();
|
||||
}
|
||||
lineEntries.add(new Entry(ts, sample.getHeartRate()));
|
||||
}
|
||||
lastTs = ts;
|
||||
}
|
||||
hrLineChart.getXAxis().setValueFormatter(new SampleXLabelFormatter(tsTranslation, "HH:mm"));
|
||||
if (!lineEntries.isEmpty()) {
|
||||
lineDataSets.add(createHeartRateDataSet(lineEntries, HEARTRATE_COLOR));
|
||||
}
|
||||
|
||||
setStatistics(data.average, data.minimum, data.maximum, data.restingHeartRate);
|
||||
|
||||
hrLineChart.setData(new LineData(lineDataSets));
|
||||
hrLineChart.getAxisLeft().removeAllLimitLines();
|
||||
|
||||
if (data.average > 0 && GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
final LimitLine averageLine = new LimitLine(data.average);
|
||||
averageLine.setLineWidth(1.5f);
|
||||
averageLine.enableDashedLine(15f, 10f, 0f);
|
||||
averageLine.setLineColor(Color.RED);
|
||||
hrLineChart.getAxisLeft().addLimitLine(averageLine);
|
||||
}
|
||||
|
||||
//if (data.restingHeartRate > 0) {
|
||||
// final LimitLine restingLine = new LimitLine(data.restingHeartRate);
|
||||
// restingLine.setLineWidth(1.5f);
|
||||
// restingLine.enableDashedLine(15f, 10f, 0f);
|
||||
// restingLine.setLineColor(HEARTRATE_RESTING_COLOR);
|
||||
// hrLineChart.getAxisLeft().addLimitLine(restingLine);
|
||||
//}
|
||||
}
|
||||
|
||||
private void setMultipleDaysData(HeartRatePeriodData data, int startTs, int endTs) {
|
||||
List<HeartRateData> samples = data.samples;
|
||||
final Accumulator avgAccumulator = new Accumulator();
|
||||
final Accumulator minAccumulator = new Accumulator();
|
||||
final Accumulator maxAccumulator = new Accumulator();
|
||||
final Accumulator restingAccumulator = new Accumulator();
|
||||
|
||||
final ArrayList<Entry> avgLineData = new ArrayList<>();
|
||||
final ArrayList<Entry> minLineData = new ArrayList<>();
|
||||
final ArrayList<Entry> maxLineData = new ArrayList<>();
|
||||
final ArrayList<Entry> restingLineData = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < samples.size(); i++) {
|
||||
final HeartRateData hrData = samples.get(i);
|
||||
if (hrData.average > 0) {
|
||||
avgAccumulator.add(hrData.average);
|
||||
avgLineData.add(new Entry(i, hrData.average));
|
||||
}
|
||||
if (hrData.minimum > 0) {
|
||||
minAccumulator.add(hrData.minimum);
|
||||
minLineData.add(new Entry(i, hrData.minimum));
|
||||
}
|
||||
if (hrData.maximum > 0) {
|
||||
maxAccumulator.add(hrData.maximum);
|
||||
maxLineData.add(new Entry(i, hrData.maximum));
|
||||
}
|
||||
if (hrData.restingHeartRate > 0) {
|
||||
restingAccumulator.add(hrData.restingHeartRate);
|
||||
restingLineData.add(new Entry(i, hrData.restingHeartRate));
|
||||
}
|
||||
}
|
||||
|
||||
final String fmt = TOTAL_DAYS == 7 ? "EEE" : "dd";
|
||||
SimpleDateFormat formatDay = new SimpleDateFormat(fmt, Locale.getDefault());
|
||||
ValueFormatter formatter = new ValueFormatter() {
|
||||
@Override
|
||||
public String getFormattedValue(float value) {
|
||||
int ts = startTs + SEC_PER_DAY * (int)value;
|
||||
return formatDay.format(new Date(ts * 1000L));
|
||||
}
|
||||
};
|
||||
hrLineChart.getXAxis().setValueFormatter(formatter);
|
||||
|
||||
final int average = avgAccumulator.getCount() > 0 ? (int) Math.round(avgAccumulator.getAverage()) : DATA_INVALID;
|
||||
final int minimum = minAccumulator.getCount() > 0 ? (int) Math.round(minAccumulator.getMin()) : DATA_INVALID;
|
||||
final int maximum = maxAccumulator.getCount() > 0 ? (int) Math.round(maxAccumulator.getMax()) : DATA_INVALID;
|
||||
final int restingAvg = restingAccumulator.getCount() > 0 ? (int) Math.round(restingAccumulator.getAverage()) : DATA_INVALID;
|
||||
setStatistics(average, minimum, maximum, restingAvg);
|
||||
|
||||
List<ILineDataSet> dataSets = new ArrayList<ILineDataSet>();
|
||||
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
|
||||
dataSets.add(createHeartRateDataSet(avgLineData, HEARTRATE_COLOR));
|
||||
}
|
||||
dataSets.add(createHeartRateDataSet(minLineData, HEARTRATE_MIN_COLOR));
|
||||
dataSets.add(createHeartRateDataSet(maxLineData, HEARTRATE_MAX_COLOR));
|
||||
dataSets.add(createHeartRateDataSet(restingLineData, HEARTRATE_RESTING_COLOR));
|
||||
|
||||
hrLineChart.setData(new LineData(dataSets));
|
||||
}
|
||||
|
||||
protected static class HeartRatePeriodData extends ChartsData {
|
||||
public List<HeartRateData> samples;
|
||||
|
||||
protected HeartRatePeriodData(List<HeartRateData> samples) {
|
||||
this.samples = samples;
|
||||
}
|
||||
}
|
||||
|
||||
protected static class HeartRateData extends ChartsData {
|
||||
public List<? extends ActivitySample> samples;
|
||||
public int restingHeartRate;
|
||||
public int average;
|
||||
public int minimum;
|
||||
public int maximum;
|
||||
|
||||
protected HeartRateData(List<? extends ActivitySample> samples, int restingHeartRate, int average, int minimum, int maximum) {
|
||||
this.samples = samples;
|
||||
this.restingHeartRate = restingHeartRate;
|
||||
this.average = average;
|
||||
this.minimum = minimum;
|
||||
this.maximum = maximum;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DaySleepChartFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.WeekSleepChartFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.HeartRatePeriodFragment;
|
||||
|
||||
public class HeartRateFragmentAdapter extends NestedFragmentAdapter {
|
||||
public HeartRateFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
|
||||
super(fragment, childFragmentManager);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0:
|
||||
return HeartRatePeriodFragment.newInstance(1);
|
||||
case 1:
|
||||
return HeartRatePeriodFragment.newInstance(7);
|
||||
case 2:
|
||||
return HeartRatePeriodFragment.newInstance(30);
|
||||
}
|
||||
return new HeartRatePeriodFragment();
|
||||
}
|
||||
}
|
@ -21,6 +21,9 @@
|
||||
<color name="chart_heartrate" type="color">#ffab40</color>
|
||||
<color name="chart_heartrate_alternative" type="color">#8B0000</color>
|
||||
<color name="chart_heartrate_fill" type="color">#fadab1</color>
|
||||
<color name="chart_heartrate_minimum" type="color">#3F84E5</color>
|
||||
<color name="chart_heartrate_resting" type="color">#01796F</color>
|
||||
<color name="chart_heartrate_maximum" type="color">#F42C04</color>
|
||||
|
||||
<color name="chart_deep_sleep_light" type="color">#0054a3</color>
|
||||
<color name="chart_deep_sleep_dark" type="color">#0054a3</color>
|
||||
|
Loading…
x
Reference in New Issue
Block a user