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()
"sleep" -> SleepCollectionFragment.newInstance(allowSwipe)
"heartrate" -> HeartRateCollectionFragment.newInstance(allowSwipe)
"hrvstatus" -> HRVStatusFragment()
"hrvstatus" -> HrvStatusCollectionFragment.newInstance(allowSwipe)
"bodyenergy" -> BodyEnergyCollectionFragment.newInstance(allowSwipe)
"vo2max" -> VO2MaxFragment()
"load" -> LoadFragment()
@@ -16,23 +16,35 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
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<HRVStatusFragment.HRVStatusWeeklyData> {
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<HRVStatusFragment.H
protected int CHART_TEXT_COLOR;
protected int LEGEND_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
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);
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);
mHRVStatusSevenDaysAvgStatus = rootView.findViewById(R.id.hrv_status_seven_days_avg_rate);
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);
mHRVStatusBaseline = rootView.findViewById(R.id.hrv_status_baseline);
mDateView = rootView.findViewById(R.id.hrv_status_date_view);
mHRVStatusGauge = rootView.findViewById(R.id.hrv_status_gauge_bar);
mHRVGaugeValue = rootView.findViewById(R.id.hrv_gauge_value);
mHRVGaugeStatus = rootView.findViewById(R.id.hrv_gauge_status);
mHRVStatusDataTypeGroup = rootView.findViewById(R.id.hrv_status_chart_data_type_group);
gaugeDrawer = new GaugeDrawer();
updateNightlySummaryLabels();
setupPeriodDataTypeChips(inflater);
setupLineChart();
refresh();
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
public String getTitle() {
return getString(R.string.pref_header_hrv_status);
@@ -117,6 +299,10 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
TEXT_COLOR = GBApplication.getTextColor(requireContext());
LEGEND_TEXT_COLOR = GBApplication.getTextColor(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
@@ -124,8 +310,11 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
Calendar day = Calendar.getInstance();
day.setTime(getEndDate());
List<HRVStatusDayData> weeklyData = getWeeklyData(db, day, device);
return new HRVStatusWeeklyData(weeklyData);
final List<HRVStatusDayData> 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<HRVStatusFragment.H
mWeeklyHRVStatusChart.invalidate();
}
protected LineDataSet createDataSet(final List<Entry> 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<Entry> 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<HRVStatusFragment.H
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
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<Entry> lineEntries = new ArrayList<>();
final List<ILineDataSet> 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<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));
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<HRVStatusFragment.H
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.add(Calendar.DATE, -TOTAL_DAYS + 1);
day.add(Calendar.DATE, -totalDays + 1);
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 endTs = startTs + 24 * 60 * 60 - 1;
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));
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()) {
final HrvSummarySample sample = latestSummarySample.get();
Calendar finalDay = (Calendar) day.clone();
@@ -272,6 +775,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
counter,
sample.getTimestamp(),
avgHRV,
minHRV,
maxHRV,
sample.getWeeklyAverage() != null ? sample.getWeeklyAverage() : 0,
sample.getLastNightAverage() != null ? sample.getLastNightAverage() : 0,
sample.getLastNight5MinHigh() != null ? sample.getLastNight5MinHigh() : 0,
@@ -286,6 +791,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
counter,
0,
avgHRV,
minHRV,
maxHRV,
0,
0,
0,
@@ -322,8 +829,62 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
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() {
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<HRVStatusFragment.H
xAxisBottom.setDrawGridLines(false);
xAxisBottom.setEnabled(true);
xAxisBottom.setDrawLimitLinesBehindData(true);
xAxisBottom.setAxisMaximum(6);
xAxisBottom.setAxisMinimum(0);
xAxisBottom.setAxisMinimum(0f);
xAxisBottom.setAxisMaximum(totalDays - 1f);
xAxisBottom.setGranularity(1f);
xAxisBottom.setGranularityEnabled(true);
xAxisBottom.setTextColor(CHART_TEXT_COLOR);
final YAxis yAxisLeft = mWeeklyHRVStatusChart.getAxisLeft();
@@ -358,26 +921,55 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
return new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return formatHRVStatusChartValue((long) value, weeklyData);
return formatHRVStatusChartValue(value, weeklyData);
}
};
}
protected String formatHRVStatusChartValue(long value, HRVStatusWeeklyData weeklyData) {
HRVStatusDayData day = weeklyData.getDay((int) value);
protected String formatHRVStatusChartValue(float value, HRVStatusWeeklyData weeklyData) {
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()));
}
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<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.lastNightSamples = lastNightSamples;
this.lastNightStart = lastNightStart;
this.lastNightEnd = lastNightEnd;
}
public HRVStatusDayData getDay(int i) {
@@ -391,6 +983,40 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
public List<HRVStatusDayData> 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<HRVStatusFragment.H
public Integer lastNight;
public Integer lastNight5MinHigh;
public Integer dayAvg;
public Integer dayMin;
public Integer dayMax;
public Integer baseLineBalancedLower;
public Integer baseLineBalancedUpper;
public Integer baseLineLowUpper;
@@ -409,6 +1037,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
public HRVStatusDayData(Calendar day,
int i, long timestamp,
Integer dayAvg,
Integer dayMin,
Integer dayMax,
Integer weeklyAvg,
Integer lastNight,
Integer lastNight5MinHigh,
@@ -424,6 +1054,8 @@ public class HRVStatusFragment extends AbstractChartFragment<HRVStatusFragment.H
this.status = status;
this.day = day;
this.dayAvg = dayAvg;
this.dayMin = dayMin;
this.dayMax = dayMax;
this.baseLineLowUpper = baseLineLowUpper;
this.baseLineBalancedLower = baseLineBalancedLower;
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"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -150,6 +151,7 @@
android:textColor="?attr/textColorPrimary"
android:textSize="20sp" />
<TextView
android:id="@+id/hrv_status_last_night_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hrv_status_last_night"
@@ -170,6 +172,7 @@
android:textSize="20sp" />
<TextView
android:id="@+id/hrv_status_last_night_highest_5_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hrv_status_last_night_highest_5"
@@ -181,12 +184,21 @@
android:layout_width="match_parent"
android:layout_height="250sp">
<com.github.mikephil.charting.charts.LineChart
<com.github.mikephil.charting.charts.CombinedChart
android:id="@+id/hrv_weekly_line_chart"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="2" />
</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>
</ScrollView>
+5 -1
View File
@@ -57,6 +57,10 @@
<color name="hrv_status_low" type="color">#fc5203</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_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" type="color">#1d2436</color>
<color name="training_chronic_load" type="color">#8847a1</color>
@@ -120,4 +124,4 @@
<drawable name="selected">@color/accent</drawable>
</resources>
</resources>
+5
View File
@@ -1842,11 +1842,16 @@
<string name="hrv_status_low">Low</string>
<string name="hrv_status_poor">Poor</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_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_unit">%1$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_daily_range_legend">Daily range</string>
<string name="training_acute_load">Acute Load</string>
<string name="training_chronic_load">Chronic Load</string>
<string name="this_week_total">This week total</string>