diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/DefaultChartsProvider.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/DefaultChartsProvider.kt
index 262557d8d0..461120b5bd 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/DefaultChartsProvider.kt
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/DefaultChartsProvider.kt
@@ -133,7 +133,7 @@ open class DefaultChartsProvider : DeviceChartsProvider {
"activitylist" -> ActivityListingChartFragment()
"sleep" -> SleepCollectionFragment.newInstance(allowSwipe)
"heartrate" -> HeartRateCollectionFragment.newInstance(allowSwipe)
- "hrvstatus" -> HRVStatusFragment()
+ "hrvstatus" -> HrvStatusCollectionFragment.newInstance(allowSwipe)
"bodyenergy" -> BodyEnergyCollectionFragment.newInstance(allowSwipe)
"vo2max" -> VO2MaxFragment()
"load" -> LoadFragment()
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HRVStatusFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HRVStatusFragment.java
index 05b444d25c..66959397f0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HRVStatusFragment.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/HRVStatusFragment.java
@@ -16,23 +16,35 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
+import android.graphics.Paint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import android.widget.Toast;
import com.github.mikephil.charting.charts.Chart;
-import com.github.mikephil.charting.charts.LineChart;
+import com.github.mikephil.charting.charts.CombinedChart;
+import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.LegendEntry;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
+import com.github.mikephil.charting.data.BarData;
+import com.github.mikephil.charting.data.BarDataSet;
+import com.github.mikephil.charting.data.BarEntry;
+import com.github.mikephil.charting.data.CandleData;
+import com.github.mikephil.charting.data.CandleDataSet;
+import com.github.mikephil.charting.data.CandleEntry;
+import com.github.mikephil.charting.data.CombinedData;
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 com.google.android.material.chip.Chip;
+import com.google.android.material.chip.ChipGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -52,7 +64,9 @@ import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.DashboardHrvWid
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
@@ -62,15 +76,42 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class HRVStatusFragment extends AbstractChartFragment {
protected static final Logger LOG = LoggerFactory.getLogger(HRVStatusFragment.class);
- protected final int TOTAL_DAYS = 7;
+ private static final String ARG_VIEW_MODE = "view_mode";
+ private static final String ARG_TOTAL_DAYS = "total_days";
+ private static final int DEFAULT_TOTAL_DAYS = 7;
+ private static final float LEGEND_FORM_SIZE = 10f;
+ private static final float LEGEND_X_ENTRY_SPACE = 10f;
+ private static final float LEGEND_Y_ENTRY_SPACE = 4f;
+ private static final float LEGEND_FORM_TO_TEXT_SPACE = 5f;
+
+ private enum ViewMode {
+ LAST_NIGHT,
+ PERIOD
+ }
+
+ private enum PeriodDataType {
+ DAILY_AVERAGE,
+ LAST_NIGHT_AVERAGE,
+ DAILY_RANGE
+ }
+
+ private static final PeriodDataType[] PERIOD_DATA_TYPE_ORDER = {
+ PeriodDataType.DAILY_AVERAGE,
+ PeriodDataType.LAST_NIGHT_AVERAGE,
+ PeriodDataType.DAILY_RANGE
+ };
+ private static final PeriodDataType DEFAULT_PERIOD_DATA_TYPE = PeriodDataType.DAILY_AVERAGE;
protected GaugeDrawer gaugeDrawer;
private ImageView mHRVStatusGauge;
- private LineChart mWeeklyHRVStatusChart;
+ private CombinedChart mWeeklyHRVStatusChart;
+ private ChipGroup mHRVStatusDataTypeGroup;
private TextView mHRVStatusSevenDaysAvg;
private TextView mHRVStatusSevenDaysAvgStatus; // Balanced, Unbalanced, Low
private TextView mHRVStatusLastNight;
+ private TextView mHRVStatusLastNightLabel;
private TextView mHRVStatusLastNight5MinHighest;
+ private TextView mHRVStatusLastNight5MinHighestLabel;
private TextView mHRVStatusDayAvg;
private TextView mHRVStatusBaseline;
private TextView mDateView;
@@ -79,6 +120,49 @@ public class HRVStatusFragment extends AbstractChartFragment {
+ if (checkedIds.isEmpty()) {
+ Toast.makeText(requireContext(), R.string.charts_at_least_one_item, Toast.LENGTH_SHORT).show();
+ final Chip defaultChip = findPeriodDataTypeChip(DEFAULT_PERIOD_DATA_TYPE);
+ if (defaultChip != null) {
+ defaultChip.setChecked(true);
+ }
+ return;
+ }
+
+ final boolean selectedDailyAverage = isPeriodDataTypeSelected(PeriodDataType.DAILY_AVERAGE);
+ final boolean selectedLastNightAverage = isPeriodDataTypeSelected(PeriodDataType.LAST_NIGHT_AVERAGE);
+ final boolean selectedDailyRange = isPeriodDataTypeSelected(PeriodDataType.DAILY_RANGE);
+ if (showDailyAverage == selectedDailyAverage
+ && showLastNightAverage == selectedLastNightAverage
+ && showDailyRange == selectedDailyRange) {
+ return;
+ }
+
+ showDailyAverage = selectedDailyAverage;
+ showLastNightAverage = selectedLastNightAverage;
+ showDailyRange = selectedDailyRange;
+ refresh();
+ });
+ }
+
+ private void addPeriodDataTypeChip(final LayoutInflater inflater, final PeriodDataType dataType) {
+ final Chip chip = (Chip) inflater.inflate(R.layout.layout_chart_chip, mHRVStatusDataTypeGroup, false);
+ chip.setId(View.generateViewId());
+ chip.setText(getString(getPeriodDataTypeLabel(dataType)));
+ chip.setTag(dataType);
+ mHRVStatusDataTypeGroup.addView(chip);
+ chip.setChecked(isPeriodDataTypeVisible(dataType));
+ }
+
+ private boolean isPeriodDataTypeSelected(final PeriodDataType dataType) {
+ final Chip chip = findPeriodDataTypeChip(dataType);
+ return chip != null && chip.isChecked();
+ }
+
+ private boolean isPeriodDataTypeVisible(final PeriodDataType dataType) {
+ switch (dataType) {
+ case DAILY_AVERAGE:
+ return showDailyAverage;
+ case LAST_NIGHT_AVERAGE:
+ return showLastNightAverage;
+ case DAILY_RANGE:
+ return showDailyRange;
+ default:
+ throw new IllegalArgumentException("Unknown period data type: " + dataType);
+ }
+ }
+
+ private Chip findPeriodDataTypeChip(final PeriodDataType dataType) {
+ for (int i = 0; i < mHRVStatusDataTypeGroup.getChildCount(); i++) {
+ final View child = mHRVStatusDataTypeGroup.getChildAt(i);
+ if (child instanceof Chip && child.getTag() == dataType) {
+ return (Chip) child;
+ }
+ }
+ return null;
+ }
+
+ private int getPeriodDataTypeLabel(final PeriodDataType dataType) {
+ switch (dataType) {
+ case DAILY_AVERAGE:
+ return R.string.hrv_status_day_avg_legend;
+ case LAST_NIGHT_AVERAGE:
+ return R.string.hrv_status_nightly_avg_legend;
+ case DAILY_RANGE:
+ return R.string.hrv_status_daily_range_legend;
+ default:
+ throw new IllegalArgumentException("Unknown period data type: " + dataType);
+ }
+ }
+
@Override
public String getTitle() {
return getString(R.string.pref_header_hrv_status);
@@ -117,6 +299,10 @@ public class HRVStatusFragment extends AbstractChartFragment weeklyData = getWeeklyData(db, day, device);
- return new HRVStatusWeeklyData(weeklyData);
+ final List periodData = getPeriodData(db, day, device);
+ final LastNightData lastNightData = viewMode == ViewMode.LAST_NIGHT ?
+ getLastNightData(db, device, day) :
+ new LastNightData(new ArrayList<>(), getLastNightSearchStart(day), getLastNightSearchEnd(day));
+ return new HRVStatusWeeklyData(periodData, lastNightData.samples, lastNightData.start, lastNightData.end);
}
@Override
@@ -133,18 +322,17 @@ public class HRVStatusFragment extends AbstractChartFragment values) {
- final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_day_avg));
- lineDataSet.setColor(getResources().getColor(R.color.hrv_status_char_line_color));
- lineDataSet.setDrawCircles(false);
- lineDataSet.setLineWidth(2f);
+ protected LineDataSet createAverageDataSet(final List values) {
+ final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_day_avg_legend));
+ lineDataSet.setColor(HRV_AVERAGE_COLOR);
+ lineDataSet.setLineWidth(2.5f);
lineDataSet.setFillAlpha(255);
lineDataSet.setCircleRadius(5f);
lineDataSet.setDrawCircles(true);
lineDataSet.setDrawCircleHole(false);
- lineDataSet.setCircleColor(getResources().getColor(R.color.hrv_status_char_line_color));
+ lineDataSet.setCircleColor(HRV_AVERAGE_COLOR);
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
- lineDataSet.setDrawValues(true);
+ lineDataSet.setDrawValues(false);
lineDataSet.setValueTextSize(10f);
lineDataSet.setValueTextColor(TEXT_COLOR);
lineDataSet.setValueFormatter(new ValueFormatter() {
@@ -156,61 +344,75 @@ public class HRVStatusFragment extends AbstractChartFragment values) {
+ final BarDataSet barDataSet = new BarDataSet(values, getString(R.string.hrv_status_baseline_label));
+ barDataSet.setDrawValues(false);
+ barDataSet.setDrawIcons(false);
+ barDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
+ barDataSet.setColors(0x00000000, HRV_BASELINE_FILL_COLOR);
+ barDataSet.setHighLightAlpha(0);
+ barDataSet.setHighlightEnabled(false);
+ return barDataSet;
+ }
+
+ protected CandleDataSet createRangeDataSet(final List values) {
+ final CandleDataSet candleDataSet = new CandleDataSet(values, getString(R.string.hrv_status_daily_range_legend));
+ candleDataSet.setDrawValues(false);
+ candleDataSet.setDrawIcons(false);
+ candleDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
+ candleDataSet.setShadowColor(HRV_RANGE_COLOR);
+ candleDataSet.setShadowWidth(2f);
+ candleDataSet.setDecreasingColor(HRV_RANGE_COLOR);
+ candleDataSet.setDecreasingPaintStyle(Paint.Style.FILL);
+ candleDataSet.setIncreasingColor(HRV_RANGE_COLOR);
+ candleDataSet.setIncreasingPaintStyle(Paint.Style.FILL);
+ candleDataSet.setNeutralColor(HRV_RANGE_COLOR);
+ candleDataSet.setBarSpace(0.35f);
+ candleDataSet.setShowCandleBar(true);
+ candleDataSet.setHighlightEnabled(false);
+ return candleDataSet;
+ }
+
+ protected LineDataSet createLastNightAverageDataSet(final List values) {
+ final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_nightly_avg_legend));
+ lineDataSet.setColor(HRV_LAST_NIGHT_COLOR);
+ lineDataSet.setLineWidth(2f);
+ lineDataSet.setCircleRadius(4f);
+ lineDataSet.setDrawCircles(true);
+ lineDataSet.setDrawCircleHole(false);
+ lineDataSet.setCircleColor(HRV_LAST_NIGHT_COLOR);
+ lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
+ lineDataSet.setDrawValues(false);
+ lineDataSet.setHighlightEnabled(false);
+ return lineDataSet;
+ }
+
+ protected LineDataSet createRawHrvDataSet(final List values) {
+ final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_last_night_legend));
+ lineDataSet.setColor(HRV_AVERAGE_COLOR);
+ lineDataSet.setDrawCircles(false);
+ lineDataSet.setLineWidth(2f);
+ lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
+ lineDataSet.setDrawValues(false);
+ lineDataSet.setHighlightEnabled(false);
+ return lineDataSet;
+ }
+
@Override
protected void updateChartsnUIThread(HRVStatusWeeklyData weeklyData) {
- String formattedDate = new SimpleDateFormat("E, MMM dd").format(getEndDate());
- mDateView.setText(formattedDate);
-
mWeeklyHRVStatusChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
- List lineEntries = new ArrayList<>();
- final List lineDataSets = new ArrayList<>();
- final Accumulator dailyAccumulator = new Accumulator();
- weeklyData.getDaysData().forEach((HRVStatusDayData day) -> {
- if (day.dayAvg > 0) {
- dailyAccumulator.add(day.dayAvg);
- lineEntries.add(new Entry(day.i, day.dayAvg));
- } else {
- if (!lineEntries.isEmpty()) {
- List clone = new ArrayList<>(lineEntries.size());
- clone.addAll(lineEntries);
- lineDataSets.add(createDataSet(clone));
- lineEntries.clear();
- }
- }
- });
- if (!lineEntries.isEmpty()) {
- lineDataSets.add(createDataSet(lineEntries));
- }
-
- List legendEntries = new ArrayList<>(1);
- LegendEntry activityEntry = new LegendEntry();
- activityEntry.label = getString(R.string.hrv_status_day_avg_legend);
- activityEntry.formColor = getResources().getColor(R.color.hrv_status_char_line_color);
- legendEntries.add(activityEntry);
- mWeeklyHRVStatusChart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
- mWeeklyHRVStatusChart.getLegend().setCustom(legendEntries);
-
- final LineData lineData = new LineData(lineDataSets);
- mWeeklyHRVStatusChart.setData(lineData);
-
- if (dailyAccumulator.getCount() > 0) {
- mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(Math.round(dailyAccumulator.getMax()) + 15);
- mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(Math.max(0, Math.round(dailyAccumulator.getMin()) - 15));
+ if (viewMode == ViewMode.LAST_NIGHT) {
+ updateLastNightChart(weeklyData);
} else {
- mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(120);
- mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(0);
+ updatePeriodChart(weeklyData);
}
- final XAxis x = mWeeklyHRVStatusChart.getXAxis();
- x.setValueFormatter(getHRVStatusChartDayValueFormatter(weeklyData));
-
HRVStatusDayData today = weeklyData.getCurrentDay();
// Show weekly average even if we don't have full 7 days - it's computed from available data
mHRVStatusSevenDaysAvg.setText(today.weeklyAvg > 0 ? getString(R.string.hrv_status_unit, today.weeklyAvg) :
- (today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : "-"));
- mHRVStatusLastNight.setText(today.lastNight > 0 ? getString(R.string.hrv_status_unit, today.lastNight) : "-");
- mHRVStatusLastNight5MinHighest.setText(today.lastNight5MinHigh > 0 ? getString(R.string.hrv_status_unit, today.lastNight5MinHigh) : "-");
- mHRVStatusDayAvg.setText(today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : "-");
+ (today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : getString(R.string.stats_empty_value)));
+ updateNightlySummaryStats(weeklyData, today);
+ mHRVStatusDayAvg.setText(formatHrvStatusValue(today.dayAvg));
mHRVStatusBaseline.setText(today.baseLineBalancedLower > 0 && today.baseLineBalancedUpper > 0 ? getString(R.string.hrv_status_baseline, today.baseLineBalancedLower, today.baseLineBalancedUpper) : "-");
switch (today.status) {
case NONE:
@@ -250,12 +452,304 @@ public class HRVStatusFragment extends AbstractChartFragment getWeeklyData(DBHandler db, Calendar day, GBDevice device) {
+ private void updateNightlySummaryStats(final HRVStatusWeeklyData weeklyData, final HRVStatusDayData today) {
+ updateNightlySummaryLabels();
+ if (viewMode == ViewMode.LAST_NIGHT) {
+ mHRVStatusLastNight.setText(formatHrvStatusValue(today.lastNight));
+ mHRVStatusLastNight5MinHighest.setText(formatHrvStatusValue(today.lastNight5MinHigh));
+ return;
+ }
+
+ mHRVStatusLastNight.setText(formatHrvStatusValue(weeklyData.getPeriodNightlyAverage()));
+ mHRVStatusLastNight5MinHighest.setText(formatHrvStatusValue(weeklyData.getHighestNightlyAverage()));
+ }
+
+ private void updateNightlySummaryLabels() {
+ if (viewMode == ViewMode.LAST_NIGHT) {
+ mHRVStatusLastNightLabel.setText(R.string.hrv_status_last_night);
+ mHRVStatusLastNight5MinHighestLabel.setText(R.string.hrv_status_last_night_highest_5);
+ return;
+ }
+
+ mHRVStatusLastNightLabel.setText(R.string.hrv_status_nightly_avg);
+ mHRVStatusLastNight5MinHighestLabel.setText(R.string.hrv_status_highest_nightly_avg);
+ }
+
+ private String formatHrvStatusValue(final int value) {
+ return value > 0 ? getString(R.string.hrv_status_unit, value) : getString(R.string.stats_empty_value);
+ }
+
+ private void updatePeriodChart(HRVStatusWeeklyData weeklyData) {
+ mDateView.setText(DateTimeUtils.formatDaysUntil(totalDays, getTSEnd()));
+ final List rangeEntries = new ArrayList<>();
+ final List averageEntries = new ArrayList<>();
+ final List lastNightEntries = new ArrayList<>();
+ final List baselineEntries = new ArrayList<>();
+ final List lineDataSets = new ArrayList<>();
+ final List lastNightDataSets = new ArrayList<>();
+ final Accumulator axisAccumulator = new Accumulator();
+ boolean hasAverageData = false;
+ boolean hasLastNightData = false;
+
+ for (final HRVStatusDayData day : weeklyData.getDaysData()) {
+ if (showDailyAverage) {
+ if (day.dayAvg > 0) {
+ hasAverageData = true;
+ axisAccumulator.add(day.dayAvg);
+ averageEntries.add(new Entry(day.i, day.dayAvg));
+ } else {
+ addAverageDataSet(lineDataSets, averageEntries);
+ }
+ }
+
+ if (showLastNightAverage) {
+ if (day.lastNight > 0) {
+ hasLastNightData = true;
+ axisAccumulator.add(day.lastNight);
+ lastNightEntries.add(new Entry(day.i, day.lastNight));
+ } else {
+ addLastNightAverageDataSet(lastNightDataSets, lastNightEntries);
+ }
+ }
+
+ if (showDailyRange && day.dayMin > 0 && day.dayMax > 0) {
+ axisAccumulator.add(day.dayMin);
+ axisAccumulator.add(day.dayMax);
+ rangeEntries.add(new CandleEntry(day.i, day.dayMax, day.dayMin, day.dayMin, day.dayMax));
+ }
+
+ if (day.baseLineBalancedLower > 0 && day.baseLineBalancedUpper > day.baseLineBalancedLower) {
+ axisAccumulator.add(day.baseLineBalancedLower);
+ axisAccumulator.add(day.baseLineBalancedUpper);
+ baselineEntries.add(new BarEntry(day.i, new float[]{
+ day.baseLineBalancedLower,
+ day.baseLineBalancedUpper - day.baseLineBalancedLower
+ }));
+ }
+ }
+
+ if (showDailyAverage) {
+ addAverageDataSet(lineDataSets, averageEntries);
+ }
+ if (showLastNightAverage) {
+ addLastNightAverageDataSet(lastNightDataSets, lastNightEntries);
+ }
+ lineDataSets.addAll(lastNightDataSets);
+
+ final boolean hasChartData = !rangeEntries.isEmpty() || !lineDataSets.isEmpty() || !baselineEntries.isEmpty();
+ if (hasChartData) {
+ final CombinedData combinedData = new CombinedData();
+ if (!baselineEntries.isEmpty()) {
+ final BarData barData = new BarData(createBaselineRangeDataSet(baselineEntries));
+ barData.setBarWidth(1f);
+ combinedData.setData(barData);
+ }
+ if (!rangeEntries.isEmpty()) {
+ combinedData.setData(new CandleData(createRangeDataSet(rangeEntries)));
+ }
+ if (!lineDataSets.isEmpty()) {
+ combinedData.setData(new LineData(lineDataSets));
+ }
+ mWeeklyHRVStatusChart.setData(combinedData);
+ } else {
+ mWeeklyHRVStatusChart.setData(null);
+ }
+ if (axisAccumulator.getCount() > 0) {
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(Math.round(axisAccumulator.getMax()) + 15);
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(Math.max(0, Math.round(axisAccumulator.getMin()) - 15));
+ } else {
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(120);
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(0);
+ }
+
+ final XAxis x = mWeeklyHRVStatusChart.getXAxis();
+ x.setValueFormatter(getHRVStatusChartDayValueFormatter(weeklyData));
+ x.setAxisMinimum(0f);
+ x.setAxisMaximum(totalDays - 1f);
+ x.setLabelCount(Math.min(totalDays, 7), false);
+ setupPeriodLegend(!rangeEntries.isEmpty(), hasAverageData, hasLastNightData, !baselineEntries.isEmpty());
+ }
+
+ private void updateLastNightChart(HRVStatusWeeklyData weeklyData) {
+ final String formattedDate = new SimpleDateFormat("E, MMM dd", Locale.getDefault()).format(getEndDate());
+ mDateView.setText(formattedDate);
+
+ final List rawEntries = new ArrayList<>();
+ final Accumulator axisAccumulator = new Accumulator();
+ final long startMillis = weeklyData.lastNightStart.getTimeInMillis();
+ float minX = Float.MAX_VALUE;
+ float maxX = -Float.MAX_VALUE;
+ for (HrvValueSample sample : weeklyData.lastNightSamples) {
+ if (sample.getValue() <= 0) {
+ continue;
+ }
+
+ final float x = (sample.getTimestamp() - startMillis) / 1000f;
+ rawEntries.add(new Entry(x, sample.getValue()));
+ minX = Math.min(minX, x);
+ maxX = Math.max(maxX, x);
+ axisAccumulator.add(sample.getValue());
+ }
+ rawEntries.sort(Comparator.comparingDouble(Entry::getX));
+
+ if (!rawEntries.isEmpty()) {
+ final CombinedData combinedData = new CombinedData();
+ combinedData.setData(new LineData(createRawHrvDataSet(rawEntries)));
+ mWeeklyHRVStatusChart.setData(combinedData);
+ } else {
+ mWeeklyHRVStatusChart.setData(null);
+ }
+
+ if (axisAccumulator.getCount() > 0) {
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(Math.round(axisAccumulator.getMax()) + 15);
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(Math.max(0, Math.round(axisAccumulator.getMin()) - 15));
+ } else {
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(120);
+ mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(0);
+ }
+
+ final XAxis x = mWeeklyHRVStatusChart.getXAxis();
+ x.setValueFormatter(getLastNightChartValueFormatter(weeklyData.lastNightStart));
+ if (rawEntries.isEmpty()) {
+ x.setAxisMinimum(0f);
+ x.setAxisMaximum((weeklyData.lastNightEnd.getTimeInMillis() - weeklyData.lastNightStart.getTimeInMillis()) / 1000f);
+ } else if (minX == maxX) {
+ x.setAxisMinimum(minX - 1f);
+ x.setAxisMaximum(maxX + 1f);
+ } else {
+ x.setAxisMinimum(minX);
+ x.setAxisMaximum(maxX);
+ }
+ x.setLabelCount(7, true);
+ setupLastNightLegend(!rawEntries.isEmpty());
+ }
+
+ private void addAverageDataSet(final List lineDataSets, final List averageEntries) {
+ if (averageEntries.isEmpty()) {
+ return;
+ }
+
+ lineDataSets.add(createAverageDataSet(new ArrayList<>(averageEntries)));
+ averageEntries.clear();
+ }
+
+ private void addLastNightAverageDataSet(final List lineDataSets, final List lastNightEntries) {
+ if (lastNightEntries.isEmpty()) {
+ return;
+ }
+
+ lineDataSets.add(createLastNightAverageDataSet(new ArrayList<>(lastNightEntries)));
+ lastNightEntries.clear();
+ }
+
+ private void setupPeriodLegend(final boolean hasRange,
+ final boolean hasDailyAverage,
+ final boolean hasLastNightAverage,
+ final boolean hasBaseline) {
+ final List legendEntries = new ArrayList<>(4);
+
+ for (final PeriodDataType dataType : PERIOD_DATA_TYPE_ORDER) {
+ if (hasPeriodDataTypeData(dataType, hasRange, hasDailyAverage, hasLastNightAverage)) {
+ legendEntries.add(createPeriodDataTypeLegendEntry(dataType));
+ }
+ }
+
+ if (hasBaseline) {
+ legendEntries.add(createSquareLegendEntry(
+ getString(R.string.hrv_status_baseline_label),
+ HRV_BASELINE_FILL_COLOR));
+ }
+
+ setupHRVStatusLegend(legendEntries);
+ }
+
+ private void setupLastNightLegend(final boolean hasRawHrv) {
+ final List legendEntries = new ArrayList<>(1);
+ if (hasRawHrv) {
+ legendEntries.add(createLineLegendEntry(getString(R.string.hrv_status_last_night_legend), HRV_AVERAGE_COLOR));
+ }
+
+ setupHRVStatusLegend(legendEntries);
+ }
+
+ private boolean hasPeriodDataTypeData(final PeriodDataType dataType,
+ final boolean hasRange,
+ final boolean hasDailyAverage,
+ final boolean hasLastNightAverage) {
+ switch (dataType) {
+ case DAILY_AVERAGE:
+ return hasDailyAverage;
+ case LAST_NIGHT_AVERAGE:
+ return hasLastNightAverage;
+ case DAILY_RANGE:
+ return hasRange;
+ default:
+ throw new IllegalArgumentException("Unknown period data type: " + dataType);
+ }
+ }
+
+ private LegendEntry createPeriodDataTypeLegendEntry(final PeriodDataType dataType) {
+ switch (dataType) {
+ case DAILY_AVERAGE:
+ return createLineLegendEntry(getString(getPeriodDataTypeLabel(dataType)), HRV_AVERAGE_COLOR);
+ case LAST_NIGHT_AVERAGE:
+ return createLineLegendEntry(getString(getPeriodDataTypeLabel(dataType)), HRV_LAST_NIGHT_COLOR);
+ case DAILY_RANGE:
+ return createSquareLegendEntry(getString(getPeriodDataTypeLabel(dataType)), HRV_RANGE_COLOR);
+ default:
+ throw new IllegalArgumentException("Unknown period data type: " + dataType);
+ }
+ }
+
+ private LegendEntry createLineLegendEntry(final String label, final int color) {
+ final LegendEntry legendEntry = new LegendEntry();
+ legendEntry.label = label;
+ legendEntry.form = Legend.LegendForm.LINE;
+ legendEntry.formSize = LEGEND_FORM_SIZE;
+ legendEntry.formLineWidth = 2f;
+ legendEntry.formColor = color;
+ return legendEntry;
+ }
+
+ private LegendEntry createSquareLegendEntry(final String label, final int color) {
+ final LegendEntry legendEntry = new LegendEntry();
+ legendEntry.label = label;
+ legendEntry.form = Legend.LegendForm.SQUARE;
+ legendEntry.formSize = LEGEND_FORM_SIZE;
+ legendEntry.formColor = color;
+ return legendEntry;
+ }
+
+ private void setupHRVStatusLegend(final List legendEntries) {
+ final Legend legend = mWeeklyHRVStatusChart.getLegend();
+ legend.resetCustom();
+ if (legendEntries.isEmpty()) {
+ legend.setEnabled(false);
+ mWeeklyHRVStatusChart.notifyDataSetChanged();
+ mWeeklyHRVStatusChart.requestLayout();
+ return;
+ }
+
+ legend.setEnabled(true);
+ legend.setTextColor(LEGEND_TEXT_COLOR);
+ legend.setWordWrapEnabled(true);
+ legend.setHorizontalAlignment(Legend.LegendHorizontalAlignment.LEFT);
+ legend.setOrientation(Legend.LegendOrientation.HORIZONTAL);
+ legend.setDrawInside(false);
+ legend.setXEntrySpace(LEGEND_X_ENTRY_SPACE);
+ legend.setYEntrySpace(LEGEND_Y_ENTRY_SPACE);
+ legend.setFormToTextSpace(LEGEND_FORM_TO_TEXT_SPACE);
+ legend.setCustom(legendEntries);
+ mWeeklyHRVStatusChart.notifyDataSetChanged();
+ mWeeklyHRVStatusChart.requestLayout();
+ }
+
+ private List getPeriodData(DBHandler db, Calendar day, GBDevice device) {
day = DateTimeUtils.dayStart(day);
- day.add(Calendar.DATE, -TOTAL_DAYS + 1);
+ day.add(Calendar.DATE, -totalDays + 1);
List weeklyData = new ArrayList<>();
- for (int counter = 0; counter < TOTAL_DAYS; counter++) {
+ for (int counter = 0; counter < totalDays; counter++) {
int startTs = (int) (day.getTimeInMillis() / 1000);
int endTs = startTs + 24 * 60 * 60 - 1;
Optional extends HrvSummarySample> latestSummarySample = getSamples(db, device, startTs, endTs)
@@ -263,7 +757,16 @@ public class HRVStatusFragment extends AbstractChartFragment valueSamples = getHrvValueSamples(db, device, startTs, endTs);
- int avgHRV = (int) valueSamples.stream().mapToInt(HrvValueSample::getValue).average().orElse(0);
+ final Accumulator dayAccumulator = new Accumulator();
+ for (HrvValueSample valueSample : valueSamples) {
+ if (valueSample.getValue() > 0) {
+ dayAccumulator.add(valueSample.getValue());
+ }
+ }
+
+ int avgHRV = dayAccumulator.getCount() > 0 ? (int) Math.round(dayAccumulator.getAverage()) : 0;
+ int minHRV = dayAccumulator.getCount() > 0 ? (int) Math.round(dayAccumulator.getMin()) : 0;
+ int maxHRV = dayAccumulator.getCount() > 0 ? (int) Math.round(dayAccumulator.getMax()) : 0;
if (latestSummarySample.isPresent()) {
final HrvSummarySample sample = latestSummarySample.get();
Calendar finalDay = (Calendar) day.clone();
@@ -272,6 +775,8 @@ public class HRVStatusFragment extends AbstractChartFragment sampleProvider =
+ device.getDeviceCoordinator().getSampleProvider(device, db.getDaoSession());
+ final List extends AbstractActivitySample> activitySamples =
+ sampleProvider.getAllActivitySamples(startTs, endTs);
+
+ if (activitySamples.isEmpty()) {
+ return new LastNightData(new ArrayList<>(), searchStart, searchEnd);
+ }
+
+ final SleepAnalysis sleepAnalysis = new SleepAnalysis();
+ final List sleepSessions =
+ sleepAnalysis.calculateSleepSessions(activitySamples);
+ if (sleepSessions.isEmpty()) {
+ return new LastNightData(new ArrayList<>(), searchStart, searchEnd);
+ }
+
+ final SleepAnalysis.SleepSession lastSession = sleepSessions.get(sleepSessions.size() - 1);
+ final Calendar sleepStart = Calendar.getInstance();
+ sleepStart.setTime(lastSession.getSleepStart());
+ final Calendar sleepEnd = Calendar.getInstance();
+ sleepEnd.setTime(lastSession.getSleepEnd());
+
+ final List extends HrvValueSample> samples = getHrvValueSamples(
+ db,
+ device,
+ (int) (sleepStart.getTimeInMillis() / 1000),
+ (int) (sleepEnd.getTimeInMillis() / 1000));
+ return new LastNightData(samples, sleepStart, sleepEnd);
+ }
+
+ private Calendar getLastNightSearchStart(final Calendar day) {
+ final Calendar lastNightStart = DateTimeUtils.dayStart((Calendar) day.clone());
+ lastNightStart.add(Calendar.DATE, -1);
+ lastNightStart.add(Calendar.HOUR_OF_DAY, 12);
+ return lastNightStart;
+ }
+
+ private Calendar getLastNightSearchEnd(final Calendar day) {
+ final Calendar lastNightEnd = DateTimeUtils.dayStart((Calendar) day.clone());
+ lastNightEnd.add(Calendar.HOUR_OF_DAY, 12);
+ return lastNightEnd;
+ }
+
private void setupLineChart() {
mWeeklyHRVStatusChart.getDescription().setEnabled(false);
+ mWeeklyHRVStatusChart.setDrawOrder(new CombinedChart.DrawOrder[]{
+ CombinedChart.DrawOrder.BAR,
+ CombinedChart.DrawOrder.CANDLE,
+ CombinedChart.DrawOrder.LINE
+ });
mWeeklyHRVStatusChart.setTouchEnabled(false);
mWeeklyHRVStatusChart.setPinchZoom(false);
mWeeklyHRVStatusChart.setDoubleTapToZoomEnabled(false);
@@ -335,8 +896,10 @@ public class HRVStatusFragment extends AbstractChartFragment= weeklyData.getDaysData().size()) {
+ return "";
+ }
- SimpleDateFormat formatLetterDay = new SimpleDateFormat("EEE", Locale.getDefault());
+ HRVStatusDayData day = weeklyData.getDay(dayIndex);
+
+ final String formatPattern = totalDays == DEFAULT_TOTAL_DAYS ? "EEE" : "dd";
+ SimpleDateFormat formatLetterDay = new SimpleDateFormat(formatPattern, Locale.getDefault());
return formatLetterDay.format(new Date(day.day.getTimeInMillis()));
}
+ ValueFormatter getLastNightChartValueFormatter(final Calendar lastNightStart) {
+ return new ValueFormatter() {
+ @Override
+ public String getFormattedValue(float value) {
+ return formatLastNightChartValue(value, lastNightStart);
+ }
+ };
+ }
+
+ protected String formatLastNightChartValue(final float value, final Calendar lastNightStart) {
+ final long timestamp = lastNightStart.getTimeInMillis() + (long) (value * 1000);
+ return new SimpleDateFormat("HH:mm", Locale.getDefault()).format(new Date(timestamp));
+ }
+
@Override
protected void setupLegend(Chart> chart) {}
protected static class HRVStatusWeeklyData extends ChartsData {
private final List data;
+ private final List extends HrvValueSample> lastNightSamples;
+ private final Calendar lastNightStart;
+ private final Calendar lastNightEnd;
- public HRVStatusWeeklyData(final List chartsData) {
+ public HRVStatusWeeklyData(final List chartsData,
+ final List extends HrvValueSample> lastNightSamples,
+ final Calendar lastNightStart,
+ final Calendar lastNightEnd) {
this.data = chartsData;
+ this.lastNightSamples = lastNightSamples;
+ this.lastNightStart = lastNightStart;
+ this.lastNightEnd = lastNightEnd;
}
public HRVStatusDayData getDay(int i) {
@@ -391,6 +983,40 @@ public class HRVStatusFragment extends AbstractChartFragment getDaysData() {
return data;
}
+
+ public int getPeriodNightlyAverage() {
+ final Accumulator accumulator = new Accumulator();
+ for (final HRVStatusDayData day : data) {
+ if (day.lastNight > 0) {
+ accumulator.add(day.lastNight);
+ }
+ }
+ return accumulator.getCount() > 0 ? (int) Math.round(accumulator.getAverage()) : 0;
+ }
+
+ public int getHighestNightlyAverage() {
+ int highestNightlyAverage = 0;
+ for (final HRVStatusDayData day : data) {
+ if (day.lastNight > highestNightlyAverage) {
+ highestNightlyAverage = day.lastNight;
+ }
+ }
+ return highestNightlyAverage;
+ }
+ }
+
+ protected static class LastNightData {
+ private final List extends HrvValueSample> samples;
+ private final Calendar start;
+ private final Calendar end;
+
+ public LastNightData(final List extends HrvValueSample> samples,
+ final Calendar start,
+ final Calendar end) {
+ this.samples = samples;
+ this.start = start;
+ this.end = end;
+ }
}
protected static class HRVStatusDayData {
@@ -400,6 +1026,8 @@ public class HRVStatusFragment extends AbstractChartFragment {
+ switch (position) {
+ case 0:
+ tab.setText(getString(R.string.calendar_day));
+ break;
+ case 1:
+ tab.setText(getString(R.string.calendar_week));
+ break;
+ case 2:
+ tab.setText(getString(R.string.calendar_month));
+ break;
+ }
+ }).attach();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HrvStatusFragmentAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HrvStatusFragmentAdapter.java
new file mode 100644
index 0000000000..d3c0a16622
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/HrvStatusFragmentAdapter.java
@@ -0,0 +1,26 @@
+package nodomain.freeyourgadget.gadgetbridge.adapter;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+import nodomain.freeyourgadget.gadgetbridge.activities.charts.HRVStatusFragment;
+
+public class HrvStatusFragmentAdapter extends NestedFragmentAdapter {
+ public HrvStatusFragmentAdapter(final Fragment fragment) {
+ super(fragment);
+ }
+
+ @NonNull
+ @Override
+ public Fragment createFragment(final int position) {
+ switch (position) {
+ case 0:
+ return HRVStatusFragment.newLastNightInstance();
+ case 1:
+ return HRVStatusFragment.newPeriodInstance(7);
+ case 2:
+ return HRVStatusFragment.newPeriodInstance(30);
+ }
+ return HRVStatusFragment.newPeriodInstance(7);
+ }
+}
diff --git a/app/src/main/res/layout/fragment_hrv_status.xml b/app/src/main/res/layout/fragment_hrv_status.xml
index 8c09f0bdb8..cd4613df93 100644
--- a/app/src/main/res/layout/fragment_hrv_status.xml
+++ b/app/src/main/res/layout/fragment_hrv_status.xml
@@ -1,4 +1,5 @@
-
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index e5b422164b..b247adfda1 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -57,6 +57,10 @@
#fc5203
#be03fc
#d12a2a
+ #99d12a2a
+ #3f7dff
+ #0c8a03
+ #660c8a03
#FFF
#1d2436
#8847a1
@@ -120,4 +124,4 @@
@color/accent
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f167ddcd9..589f908d6b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1842,11 +1842,16 @@
Low
Poor
Last night
+ Last night (ms)
Last night 5-min max avg
+ Nightly average
+ Nightly average (ms)
+ Highest nightly avg
7-day average
%1$d ms
%1$d – %2$d ms
Baseline
+ Daily range
Acute Load
Chronic Load
This week total