HeartRateChart: Add weekly and monthly view

This commit is contained in:
Simon Brand 2025-01-02 05:28:56 +00:00
parent aa3f677d47
commit 3813e03eb2
6 changed files with 550 additions and 323 deletions

View File

@ -170,7 +170,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "sleep": case "sleep":
return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1); return SleepCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "heartrate": case "heartrate":
return new HeartRateDailyFragment(); return HeartRateCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "hrvstatus": case "hrvstatus":
return new HRVStatusFragment(); return new HRVStatusFragment();
case "bodyenergy": case "bodyenergy":

View File

@ -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());
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}

View File

@ -21,6 +21,9 @@
<color name="chart_heartrate" type="color">#ffab40</color> <color name="chart_heartrate" type="color">#ffab40</color>
<color name="chart_heartrate_alternative" type="color">#8B0000</color> <color name="chart_heartrate_alternative" type="color">#8B0000</color>
<color name="chart_heartrate_fill" type="color">#fadab1</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_light" type="color">#0054a3</color>
<color name="chart_deep_sleep_dark" type="color">#0054a3</color> <color name="chart_deep_sleep_dark" type="color">#0054a3</color>