Improve HRV status chart views

This commit is contained in:
trentsuzuki
2026-06-19 21:20:06 +02:00
committed by trentsuzuki
parent 7c21dbe819
commit 78ed1d2d1a
7 changed files with 802 additions and 72 deletions
@@ -133,7 +133,7 @@ open class DefaultChartsProvider : DeviceChartsProvider {
"activitylist" -> ActivityListingChartFragment() "activitylist" -> ActivityListingChartFragment()
"sleep" -> SleepCollectionFragment.newInstance(allowSwipe) "sleep" -> SleepCollectionFragment.newInstance(allowSwipe)
"heartrate" -> HeartRateCollectionFragment.newInstance(allowSwipe) "heartrate" -> HeartRateCollectionFragment.newInstance(allowSwipe)
"hrvstatus" -> HRVStatusFragment() "hrvstatus" -> HrvStatusCollectionFragment.newInstance(allowSwipe)
"bodyenergy" -> BodyEnergyCollectionFragment.newInstance(allowSwipe) "bodyenergy" -> BodyEnergyCollectionFragment.newInstance(allowSwipe)
"vo2max" -> VO2MaxFragment() "vo2max" -> VO2MaxFragment()
"load" -> LoadFragment() "load" -> LoadFragment()
@@ -16,23 +16,35 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */ along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts; package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.graphics.Paint;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
import com.github.mikephil.charting.charts.Chart; 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.LegendEntry;
import com.github.mikephil.charting.components.XAxis; import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis; import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.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.Entry;
import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet; import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; import com.github.mikephil.charting.interfaces.datasets.ILineDataSet;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.activities.dashboard.GaugeDrawer;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample; import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
@@ -62,15 +76,42 @@ import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.HRVStatusWeeklyData> { public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.HRVStatusWeeklyData> {
protected static final Logger LOG = LoggerFactory.getLogger(HRVStatusFragment.class); 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; protected GaugeDrawer gaugeDrawer;
private ImageView mHRVStatusGauge; private ImageView mHRVStatusGauge;
private LineChart mWeeklyHRVStatusChart; private CombinedChart mWeeklyHRVStatusChart;
private ChipGroup mHRVStatusDataTypeGroup;
private TextView mHRVStatusSevenDaysAvg; private TextView mHRVStatusSevenDaysAvg;
private TextView mHRVStatusSevenDaysAvgStatus; // Balanced, Unbalanced, Low private TextView mHRVStatusSevenDaysAvgStatus; // Balanced, Unbalanced, Low
private TextView mHRVStatusLastNight; private TextView mHRVStatusLastNight;
private TextView mHRVStatusLastNightLabel;
private TextView mHRVStatusLastNight5MinHighest; private TextView mHRVStatusLastNight5MinHighest;
private TextView mHRVStatusLastNight5MinHighestLabel;
private TextView mHRVStatusDayAvg; private TextView mHRVStatusDayAvg;
private TextView mHRVStatusBaseline; private TextView mHRVStatusBaseline;
private TextView mDateView; private TextView mDateView;
@@ -79,6 +120,49 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
protected int CHART_TEXT_COLOR; protected int CHART_TEXT_COLOR;
protected int LEGEND_TEXT_COLOR; protected int LEGEND_TEXT_COLOR;
protected int TEXT_COLOR; protected int TEXT_COLOR;
protected int HRV_AVERAGE_COLOR;
protected int HRV_RANGE_COLOR;
protected int HRV_LAST_NIGHT_COLOR;
protected int HRV_BASELINE_FILL_COLOR;
private ViewMode viewMode = ViewMode.PERIOD;
private int totalDays = DEFAULT_TOTAL_DAYS;
private boolean showDailyAverage = true;
private boolean showLastNightAverage = true;
private boolean showDailyRange = false;
public static HRVStatusFragment newLastNightInstance() {
final HRVStatusFragment fragment = new HRVStatusFragment();
final Bundle args = new Bundle();
args.putString(ARG_VIEW_MODE, ViewMode.LAST_NIGHT.name());
fragment.setArguments(args);
return fragment;
}
public static HRVStatusFragment newPeriodInstance(final int totalDays) {
final HRVStatusFragment fragment = new HRVStatusFragment();
final Bundle args = new Bundle();
args.putString(ARG_VIEW_MODE, ViewMode.PERIOD.name());
args.putInt(ARG_TOTAL_DAYS, totalDays);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(final Bundle savedInstanceState) {
if (getArguments() != null) {
final String viewModeArg = getArguments().getString(ARG_VIEW_MODE, ViewMode.PERIOD.name());
viewMode = ViewMode.valueOf(viewModeArg);
totalDays = getArguments().getInt(ARG_TOTAL_DAYS, DEFAULT_TOTAL_DAYS);
}
super.onCreate(savedInstanceState);
}
@Override
protected boolean isSingleDay() {
return viewMode == ViewMode.LAST_NIGHT;
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@@ -90,23 +174,121 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
mWeeklyHRVStatusChart = rootView.findViewById(R.id.hrv_weekly_line_chart); mWeeklyHRVStatusChart = rootView.findViewById(R.id.hrv_weekly_line_chart);
mHRVStatusLastNight = rootView.findViewById(R.id.hrv_status_last_night); mHRVStatusLastNight = rootView.findViewById(R.id.hrv_status_last_night);
mHRVStatusLastNightLabel = rootView.findViewById(R.id.hrv_status_last_night_label);
mHRVStatusSevenDaysAvg = rootView.findViewById(R.id.hrv_status_seven_days_avg); mHRVStatusSevenDaysAvg = rootView.findViewById(R.id.hrv_status_seven_days_avg);
mHRVStatusSevenDaysAvgStatus = rootView.findViewById(R.id.hrv_status_seven_days_avg_rate); mHRVStatusSevenDaysAvgStatus = rootView.findViewById(R.id.hrv_status_seven_days_avg_rate);
mHRVStatusLastNight5MinHighest = rootView.findViewById(R.id.hrv_status_last_night_highest_5); mHRVStatusLastNight5MinHighest = rootView.findViewById(R.id.hrv_status_last_night_highest_5);
mHRVStatusLastNight5MinHighestLabel = rootView.findViewById(R.id.hrv_status_last_night_highest_5_label);
mHRVStatusDayAvg = rootView.findViewById(R.id.hrv_status_day_avg); mHRVStatusDayAvg = rootView.findViewById(R.id.hrv_status_day_avg);
mHRVStatusBaseline = rootView.findViewById(R.id.hrv_status_baseline); mHRVStatusBaseline = rootView.findViewById(R.id.hrv_status_baseline);
mDateView = rootView.findViewById(R.id.hrv_status_date_view); mDateView = rootView.findViewById(R.id.hrv_status_date_view);
mHRVStatusGauge = rootView.findViewById(R.id.hrv_status_gauge_bar); mHRVStatusGauge = rootView.findViewById(R.id.hrv_status_gauge_bar);
mHRVGaugeValue = rootView.findViewById(R.id.hrv_gauge_value); mHRVGaugeValue = rootView.findViewById(R.id.hrv_gauge_value);
mHRVGaugeStatus = rootView.findViewById(R.id.hrv_gauge_status); mHRVGaugeStatus = rootView.findViewById(R.id.hrv_gauge_status);
mHRVStatusDataTypeGroup = rootView.findViewById(R.id.hrv_status_chart_data_type_group);
gaugeDrawer = new GaugeDrawer(); gaugeDrawer = new GaugeDrawer();
updateNightlySummaryLabels();
setupPeriodDataTypeChips(inflater);
setupLineChart(); setupLineChart();
refresh(); refresh();
return rootView; return rootView;
} }
private void setupPeriodDataTypeChips(final LayoutInflater inflater) {
if (mHRVStatusDataTypeGroup == null) {
return;
}
if (viewMode == ViewMode.LAST_NIGHT) {
mHRVStatusDataTypeGroup.setVisibility(View.GONE);
return;
}
mHRVStatusDataTypeGroup.setVisibility(View.VISIBLE);
mHRVStatusDataTypeGroup.removeAllViews();
mHRVStatusDataTypeGroup.setSingleSelection(false);
mHRVStatusDataTypeGroup.setSelectionRequired(true);
for (final PeriodDataType dataType : PERIOD_DATA_TYPE_ORDER) {
addPeriodDataTypeChip(inflater, dataType);
}
mHRVStatusDataTypeGroup.setOnCheckedStateChangeListener((group, checkedIds) -> {
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 @Override
public String getTitle() { public String getTitle() {
return getString(R.string.pref_header_hrv_status); return getString(R.string.pref_header_hrv_status);
@@ -117,6 +299,10 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
TEXT_COLOR = GBApplication.getTextColor(requireContext()); TEXT_COLOR = GBApplication.getTextColor(requireContext());
LEGEND_TEXT_COLOR = GBApplication.getTextColor(requireContext()); LEGEND_TEXT_COLOR = GBApplication.getTextColor(requireContext());
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext()); CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
HRV_AVERAGE_COLOR = getResources().getColor(R.color.hrv_status_char_line_color);
HRV_RANGE_COLOR = getResources().getColor(R.color.hrv_status_range_color);
HRV_LAST_NIGHT_COLOR = getResources().getColor(R.color.hrv_status_last_night_color);
HRV_BASELINE_FILL_COLOR = getResources().getColor(R.color.hrv_status_baseline_fill_color);
} }
@Override @Override
@@ -124,8 +310,11 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
Calendar day = Calendar.getInstance(); Calendar day = Calendar.getInstance();
day.setTime(getEndDate()); day.setTime(getEndDate());
List<HRVStatusDayData> weeklyData = getWeeklyData(db, day, device); final List<HRVStatusDayData> periodData = getPeriodData(db, day, device);
return new HRVStatusWeeklyData(weeklyData); 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 @Override
@@ -133,18 +322,17 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
mWeeklyHRVStatusChart.invalidate(); mWeeklyHRVStatusChart.invalidate();
} }
protected LineDataSet createDataSet(final List<Entry> values) { protected LineDataSet createAverageDataSet(final List<Entry> values) {
final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_day_avg)); final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.hrv_status_day_avg_legend));
lineDataSet.setColor(getResources().getColor(R.color.hrv_status_char_line_color)); lineDataSet.setColor(HRV_AVERAGE_COLOR);
lineDataSet.setDrawCircles(false); lineDataSet.setLineWidth(2.5f);
lineDataSet.setLineWidth(2f);
lineDataSet.setFillAlpha(255); lineDataSet.setFillAlpha(255);
lineDataSet.setCircleRadius(5f); lineDataSet.setCircleRadius(5f);
lineDataSet.setDrawCircles(true); lineDataSet.setDrawCircles(true);
lineDataSet.setDrawCircleHole(false); 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.setAxisDependency(YAxis.AxisDependency.LEFT);
lineDataSet.setDrawValues(true); lineDataSet.setDrawValues(false);
lineDataSet.setValueTextSize(10f); lineDataSet.setValueTextSize(10f);
lineDataSet.setValueTextColor(TEXT_COLOR); lineDataSet.setValueTextColor(TEXT_COLOR);
lineDataSet.setValueFormatter(new ValueFormatter() { lineDataSet.setValueFormatter(new ValueFormatter() {
@@ -156,61 +344,75 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
return lineDataSet; return lineDataSet;
} }
protected BarDataSet createBaselineRangeDataSet(final List<BarEntry> 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<CandleEntry> 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<Entry> 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<Entry> 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 @Override
protected void updateChartsnUIThread(HRVStatusWeeklyData weeklyData) { 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 mWeeklyHRVStatusChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
List<Entry> lineEntries = new ArrayList<>(); if (viewMode == ViewMode.LAST_NIGHT) {
final List<ILineDataSet> lineDataSets = new ArrayList<>(); updateLastNightChart(weeklyData);
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 { } else {
if (!lineEntries.isEmpty()) { updatePeriodChart(weeklyData);
List<Entry> clone = new ArrayList<>(lineEntries.size());
clone.addAll(lineEntries);
lineDataSets.add(createDataSet(clone));
lineEntries.clear();
} }
}
});
if (!lineEntries.isEmpty()) {
lineDataSets.add(createDataSet(lineEntries));
}
List<LegendEntry> 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));
} else {
mWeeklyHRVStatusChart.getAxisLeft().setAxisMaximum(120);
mWeeklyHRVStatusChart.getAxisLeft().setAxisMinimum(0);
}
final XAxis x = mWeeklyHRVStatusChart.getXAxis();
x.setValueFormatter(getHRVStatusChartDayValueFormatter(weeklyData));
HRVStatusDayData today = weeklyData.getCurrentDay(); HRVStatusDayData today = weeklyData.getCurrentDay();
// Show weekly average even if we don't have full 7 days - it's computed from available data // 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) : mHRVStatusSevenDaysAvg.setText(today.weeklyAvg > 0 ? getString(R.string.hrv_status_unit, today.weeklyAvg) :
(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)));
mHRVStatusLastNight.setText(today.lastNight > 0 ? getString(R.string.hrv_status_unit, today.lastNight) : "-"); updateNightlySummaryStats(weeklyData, today);
mHRVStatusLastNight5MinHighest.setText(today.lastNight5MinHigh > 0 ? getString(R.string.hrv_status_unit, today.lastNight5MinHigh) : "-"); mHRVStatusDayAvg.setText(formatHrvStatusValue(today.dayAvg));
mHRVStatusDayAvg.setText(today.dayAvg > 0 ? getString(R.string.hrv_status_unit, today.dayAvg) : "-");
mHRVStatusBaseline.setText(today.baseLineBalancedLower > 0 && today.baseLineBalancedUpper > 0 ? getString(R.string.hrv_status_baseline, today.baseLineBalancedLower, today.baseLineBalancedUpper) : "-"); mHRVStatusBaseline.setText(today.baseLineBalancedLower > 0 && today.baseLineBalancedUpper > 0 ? getString(R.string.hrv_status_baseline, today.baseLineBalancedLower, today.baseLineBalancedUpper) : "-");
switch (today.status) { switch (today.status) {
case NONE: case NONE:
@@ -250,12 +452,304 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
gaugeDrawer.drawSegmentedGauge(mHRVStatusGauge, DashboardHrvWidget.getColors(), DashboardHrvWidget.getSegments(), value, false, true); gaugeDrawer.drawSegmentedGauge(mHRVStatusGauge, DashboardHrvWidget.getColors(), DashboardHrvWidget.getSegments(), value, false, true);
} }
private List<HRVStatusDayData> 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<CandleEntry> rangeEntries = new ArrayList<>();
final List<Entry> averageEntries = new ArrayList<>();
final List<Entry> lastNightEntries = new ArrayList<>();
final List<BarEntry> baselineEntries = new ArrayList<>();
final List<ILineDataSet> lineDataSets = new ArrayList<>();
final List<ILineDataSet> 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<Entry> 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<ILineDataSet> lineDataSets, final List<Entry> averageEntries) {
if (averageEntries.isEmpty()) {
return;
}
lineDataSets.add(createAverageDataSet(new ArrayList<>(averageEntries)));
averageEntries.clear();
}
private void addLastNightAverageDataSet(final List<ILineDataSet> lineDataSets, final List<Entry> 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<LegendEntry> 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<LegendEntry> 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<LegendEntry> 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<HRVStatusDayData> getPeriodData(DBHandler db, Calendar day, GBDevice device) {
day = DateTimeUtils.dayStart(day); day = DateTimeUtils.dayStart(day);
day.add(Calendar.DATE, -TOTAL_DAYS + 1); day.add(Calendar.DATE, -totalDays + 1);
List<HRVStatusDayData> weeklyData = new ArrayList<>(); List<HRVStatusDayData> 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 startTs = (int) (day.getTimeInMillis() / 1000);
int endTs = startTs + 24 * 60 * 60 - 1; int endTs = startTs + 24 * 60 * 60 - 1;
Optional<? extends HrvSummarySample> latestSummarySample = getSamples(db, device, startTs, endTs) Optional<? extends HrvSummarySample> latestSummarySample = getSamples(db, device, startTs, endTs)
@@ -263,7 +757,16 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
.max(Comparator.comparingLong(HrvSummarySample::getTimestamp)); .max(Comparator.comparingLong(HrvSummarySample::getTimestamp));
List<? extends HrvValueSample> valueSamples = getHrvValueSamples(db, device, startTs, endTs); List<? extends HrvValueSample> 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()) { if (latestSummarySample.isPresent()) {
final HrvSummarySample sample = latestSummarySample.get(); final HrvSummarySample sample = latestSummarySample.get();
Calendar finalDay = (Calendar) day.clone(); Calendar finalDay = (Calendar) day.clone();
@@ -272,6 +775,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
counter, counter,
sample.getTimestamp(), sample.getTimestamp(),
avgHRV, avgHRV,
minHRV,
maxHRV,
sample.getWeeklyAverage() != null ? sample.getWeeklyAverage() : 0, sample.getWeeklyAverage() != null ? sample.getWeeklyAverage() : 0,
sample.getLastNightAverage() != null ? sample.getLastNightAverage() : 0, sample.getLastNightAverage() != null ? sample.getLastNightAverage() : 0,
sample.getLastNight5MinHigh() != null ? sample.getLastNight5MinHigh() : 0, sample.getLastNight5MinHigh() != null ? sample.getLastNight5MinHigh() : 0,
@@ -286,6 +791,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
counter, counter,
0, 0,
avgHRV, avgHRV,
minHRV,
maxHRV,
0, 0,
0, 0,
0, 0,
@@ -322,8 +829,62 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
return sampleProvider.getAllSamples(tsFrom * 1000L, tsTo * 1000L); return sampleProvider.getAllSamples(tsFrom * 1000L, tsTo * 1000L);
} }
private LastNightData getLastNightData(final DBHandler db, final GBDevice device, final Calendar day) {
final Calendar searchStart = getLastNightSearchStart(day);
final Calendar searchEnd = getLastNightSearchEnd(day);
final int startTs = (int) (searchStart.getTimeInMillis() / 1000);
final int endTs = (int) (searchEnd.getTimeInMillis() / 1000);
final SampleProvider<? extends AbstractActivitySample> 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<SleepAnalysis.SleepSession> 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() { private void setupLineChart() {
mWeeklyHRVStatusChart.getDescription().setEnabled(false); mWeeklyHRVStatusChart.getDescription().setEnabled(false);
mWeeklyHRVStatusChart.setDrawOrder(new CombinedChart.DrawOrder[]{
CombinedChart.DrawOrder.BAR,
CombinedChart.DrawOrder.CANDLE,
CombinedChart.DrawOrder.LINE
});
mWeeklyHRVStatusChart.setTouchEnabled(false); mWeeklyHRVStatusChart.setTouchEnabled(false);
mWeeklyHRVStatusChart.setPinchZoom(false); mWeeklyHRVStatusChart.setPinchZoom(false);
mWeeklyHRVStatusChart.setDoubleTapToZoomEnabled(false); mWeeklyHRVStatusChart.setDoubleTapToZoomEnabled(false);
@@ -335,8 +896,10 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
xAxisBottom.setDrawGridLines(false); xAxisBottom.setDrawGridLines(false);
xAxisBottom.setEnabled(true); xAxisBottom.setEnabled(true);
xAxisBottom.setDrawLimitLinesBehindData(true); xAxisBottom.setDrawLimitLinesBehindData(true);
xAxisBottom.setAxisMaximum(6); xAxisBottom.setAxisMinimum(0f);
xAxisBottom.setAxisMinimum(0); xAxisBottom.setAxisMaximum(totalDays - 1f);
xAxisBottom.setGranularity(1f);
xAxisBottom.setGranularityEnabled(true);
xAxisBottom.setTextColor(CHART_TEXT_COLOR); xAxisBottom.setTextColor(CHART_TEXT_COLOR);
final YAxis yAxisLeft = mWeeklyHRVStatusChart.getAxisLeft(); final YAxis yAxisLeft = mWeeklyHRVStatusChart.getAxisLeft();
@@ -358,26 +921,55 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
return new ValueFormatter() { return new ValueFormatter() {
@Override @Override
public String getFormattedValue(float value) { public String getFormattedValue(float value) {
return formatHRVStatusChartValue((long) value, weeklyData); return formatHRVStatusChartValue(value, weeklyData);
} }
}; };
} }
protected String formatHRVStatusChartValue(long value, HRVStatusWeeklyData weeklyData) { protected String formatHRVStatusChartValue(float value, HRVStatusWeeklyData weeklyData) {
HRVStatusDayData day = weeklyData.getDay((int) value); final int dayIndex = Math.round(value);
if (dayIndex < 0 || dayIndex >= 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())); 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 @Override
protected void setupLegend(Chart<?> chart) {} protected void setupLegend(Chart<?> chart) {}
protected static class HRVStatusWeeklyData extends ChartsData { protected static class HRVStatusWeeklyData extends ChartsData {
private final List<HRVStatusDayData> data; private final List<HRVStatusDayData> data;
private final List<? extends HrvValueSample> lastNightSamples;
private final Calendar lastNightStart;
private final Calendar lastNightEnd;
public HRVStatusWeeklyData(final List<HRVStatusDayData> chartsData) { public HRVStatusWeeklyData(final List<HRVStatusDayData> chartsData,
final List<? extends HrvValueSample> lastNightSamples,
final Calendar lastNightStart,
final Calendar lastNightEnd) {
this.data = chartsData; this.data = chartsData;
this.lastNightSamples = lastNightSamples;
this.lastNightStart = lastNightStart;
this.lastNightEnd = lastNightEnd;
} }
public HRVStatusDayData getDay(int i) { public HRVStatusDayData getDay(int i) {
@@ -391,6 +983,40 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
public List<HRVStatusDayData> getDaysData() { public List<HRVStatusDayData> getDaysData() {
return data; 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 { protected static class HRVStatusDayData {
@@ -400,6 +1026,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
public Integer lastNight; public Integer lastNight;
public Integer lastNight5MinHigh; public Integer lastNight5MinHigh;
public Integer dayAvg; public Integer dayAvg;
public Integer dayMin;
public Integer dayMax;
public Integer baseLineBalancedLower; public Integer baseLineBalancedLower;
public Integer baseLineBalancedUpper; public Integer baseLineBalancedUpper;
public Integer baseLineLowUpper; public Integer baseLineLowUpper;
@@ -409,6 +1037,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
public HRVStatusDayData(Calendar day, public HRVStatusDayData(Calendar day,
int i, long timestamp, int i, long timestamp,
Integer dayAvg, Integer dayAvg,
Integer dayMin,
Integer dayMax,
Integer weeklyAvg, Integer weeklyAvg,
Integer lastNight, Integer lastNight,
Integer lastNight5MinHigh, Integer lastNight5MinHigh,
@@ -424,6 +1054,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
this.status = status; this.status = status;
this.day = day; this.day = day;
this.dayAvg = dayAvg; this.dayAvg = dayAvg;
this.dayMin = dayMin;
this.dayMax = dayMax;
this.baseLineLowUpper = baseLineLowUpper; this.baseLineLowUpper = baseLineLowUpper;
this.baseLineBalancedLower = baseLineBalancedLower; this.baseLineBalancedLower = baseLineBalancedLower;
this.baseLineBalancedUpper = baseLineBalancedUpper; this.baseLineBalancedUpper = baseLineBalancedUpper;
@@ -0,0 +1,51 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.adapter.HrvStatusFragmentAdapter;
public class HrvStatusCollectionFragment extends AbstractCollectionFragment {
public HrvStatusCollectionFragment() {
}
public static HrvStatusCollectionFragment newInstance(final boolean allowSwipe) {
final HrvStatusCollectionFragment fragment = new HrvStatusCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
}
@Override
public FragmentStateAdapter getFragmentAdapter() {
return new HrvStatusFragmentAdapter(this);
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
final TabLayout tabLayout = view.findViewById(R.id.tab_layout);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
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();
}
}
@@ -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);
}
}
@@ -1,4 +1,5 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -150,6 +151,7 @@
android:textColor="?attr/textColorPrimary" android:textColor="?attr/textColorPrimary"
android:textSize="20sp" /> android:textSize="20sp" />
<TextView <TextView
android:id="@+id/hrv_status_last_night_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/hrv_status_last_night" android:text="@string/hrv_status_last_night"
@@ -170,6 +172,7 @@
android:textSize="20sp" /> android:textSize="20sp" />
<TextView <TextView
android:id="@+id/hrv_status_last_night_highest_5_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/hrv_status_last_night_highest_5" android:text="@string/hrv_status_last_night_highest_5"
@@ -181,12 +184,21 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="250sp"> android:layout_height="250sp">
<com.github.mikephil.charting.charts.LineChart <com.github.mikephil.charting.charts.CombinedChart
android:id="@+id/hrv_weekly_line_chart" android:id="@+id/hrv_weekly_line_chart"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:layout_weight="2" /> android:layout_weight="2" />
</LinearLayout> </LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/hrv_status_chart_data_type_group"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:layout_marginBottom="10dp"
app:selectionRequired="true" />
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
+4
View File
@@ -57,6 +57,10 @@
<color name="hrv_status_low" type="color">#fc5203</color> <color name="hrv_status_low" type="color">#fc5203</color>
<color name="hrv_status_poor" type="color">#be03fc</color> <color name="hrv_status_poor" type="color">#be03fc</color>
<color name="hrv_status_char_line_color" type="color">#d12a2a</color> <color name="hrv_status_char_line_color" type="color">#d12a2a</color>
<color name="hrv_status_range_color" type="color">#99d12a2a</color>
<color name="hrv_status_last_night_color" type="color">#3f7dff</color>
<color name="hrv_status_baseline_color" type="color">#0c8a03</color>
<color name="hrv_status_baseline_fill_color" type="color">#660c8a03</color>
<color name="training_load_dark" type="color">#FFF</color> <color name="training_load_dark" type="color">#FFF</color>
<color name="training_load" type="color">#1d2436</color> <color name="training_load" type="color">#1d2436</color>
<color name="training_chronic_load" type="color">#8847a1</color> <color name="training_chronic_load" type="color">#8847a1</color>
+5
View File
@@ -1842,11 +1842,16 @@
<string name="hrv_status_low">Low</string> <string name="hrv_status_low">Low</string>
<string name="hrv_status_poor">Poor</string> <string name="hrv_status_poor">Poor</string>
<string name="hrv_status_last_night">Last night</string> <string name="hrv_status_last_night">Last night</string>
<string name="hrv_status_last_night_legend">Last night (ms)</string>
<string name="hrv_status_last_night_highest_5">Last night 5-min max avg</string> <string name="hrv_status_last_night_highest_5">Last night 5-min max avg</string>
<string name="hrv_status_nightly_avg">Nightly average</string>
<string name="hrv_status_nightly_avg_legend">Nightly average (ms)</string>
<string name="hrv_status_highest_nightly_avg">Highest nightly avg</string>
<string name="hrv_status_seven_days_avg_long">7-day average</string> <string name="hrv_status_seven_days_avg_long">7-day average</string>
<string name="hrv_status_unit">%1$d ms</string> <string name="hrv_status_unit">%1$d ms</string>
<string name="hrv_status_baseline">%1$d %2$d ms</string> <string name="hrv_status_baseline">%1$d %2$d ms</string>
<string name="hrv_status_baseline_label">Baseline</string> <string name="hrv_status_baseline_label">Baseline</string>
<string name="hrv_status_daily_range_legend">Daily range</string>
<string name="training_acute_load">Acute Load</string> <string name="training_acute_load">Acute Load</string>
<string name="training_chronic_load">Chronic Load</string> <string name="training_chronic_load">Chronic Load</string>
<string name="this_week_total">This week total</string> <string name="this_week_total">This week total</string>