Add weekly/monthly stress charts

This commit is contained in:
trentsuzuki
2025-05-26 21:49:48 +02:00
committed by José Rebelo
parent 7021804db5
commit d251062b85
11 changed files with 903 additions and 488 deletions
@@ -1,356 +0,0 @@
/* Copyright (C) 2017-2024 Alberto, Andreas Shimokawa, Carsten Pfeiffer,
Daniele Gobbetti, José Rebelo, Pavel Elagin, Petr Vaněk, a0z
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.app.Activity;
import android.graphics.Color;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.components.XAxis;
import com.github.mikephil.charting.components.YAxis;
import com.github.mikephil.charting.data.BarData;
import com.github.mikephil.charting.data.BarDataSet;
import com.github.mikephil.charting.data.BarEntry;
import com.github.mikephil.charting.data.ChartData;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityAmounts;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue;
public abstract class AbstractWeekChartFragment extends AbstractActivityChartFragment<AbstractWeekChartFragment.MyChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(AbstractWeekChartFragment.class);
protected int TOTAL_DAYS = getRangeDays();
protected int TOTAL_DAYS_FOR_AVERAGE = 0;
protected Locale mLocale;
protected int mTargetValue = 0;
protected BarChart mWeekChart;
private final int mOffsetHours = getOffsetHours();
protected String getWeeksChartsLabel(Calendar day){
if (TOTAL_DAYS > 7) {
//month, show day date
return String.valueOf(day.get(Calendar.DAY_OF_MONTH));
}
else{
//week, show short day name
return day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale);
}
}
protected WeekChartsData<BarData> refreshWeekBeforeData(DBHandler db, BarChart barChart, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS + 1);
List<BarEntry> entries = new ArrayList<>();
ArrayList<String> labels = new ArrayList<String>();
long balance = 0;
long daily_balance = 0;
TOTAL_DAYS_FOR_AVERAGE=0;
List<Entry> sleepScoreEntities = new ArrayList<>();
final List<ILineDataSet> sleepScoreDataSets = new ArrayList<>();
for (int counter = 0; counter < TOTAL_DAYS; counter++) {
// Sleep stages
ActivityAmounts amounts = getActivityAmountsForDay(db, day, device);
daily_balance=calculateBalance(amounts);
if (daily_balance > 0) {
TOTAL_DAYS_FOR_AVERAGE++;
}
balance += daily_balance;
entries.add(new BarEntry(counter, getTotalsForActivityAmounts(amounts)));
labels.add(getWeeksChartsLabel(day));
// Sleep score
if (supportsSleepScore()) {
List<? extends SleepScoreSample> sleepScoreSamples = getSleepScoreSamples(db, device, day);
if (!sleepScoreSamples.isEmpty() && sleepScoreSamples.get(0).getSleepScore() > 0) {
sleepScoreEntities.add(new Entry(counter, sleepScoreSamples.get(0).getSleepScore()));
} else {
if (!sleepScoreEntities.isEmpty()) {
List<Entry> clone = new ArrayList<>(sleepScoreEntities.size());
clone.addAll(sleepScoreEntities);
sleepScoreDataSets.add(createSleepScoreDataSet(clone));
sleepScoreEntities.clear();
}
}
}
day.add(Calendar.DATE, 1);
}
if (!sleepScoreEntities.isEmpty()) {
sleepScoreDataSets.add(createSleepScoreDataSet(sleepScoreEntities));
}
final LineData sleepScoreLineData = new LineData(sleepScoreDataSets);
sleepScoreLineData.setHighlightEnabled(false);
BarDataSet set = new BarDataSet(entries, "");
set.setColors(getColors());
set.setValueFormatter(getBarValueFormatter());
BarData barData = new BarData(set);
barData.setValueTextColor(Color.GRAY); //prevent tearing other graph elements with the black text. Another approach would be to hide the values cmpletely with data.setDrawValues(false);
barData.setValueTextSize(10f);
barChart.getAxisLeft().setAxisMaximum(Math.max(set.getYMax(), mTargetValue) + 60);
LimitLine target = new LimitLine(mTargetValue);
target.setLineWidth(1.5f);
target.enableDashedLine(15f, 10f, 0f);
target.setLineColor(getResources().getColor(R.color.chart_deep_sleep_dark));
barChart.getAxisLeft().removeAllLimitLines();
barChart.getAxisLeft().addLimitLine(target);
float average = 0;
if (TOTAL_DAYS_FOR_AVERAGE > 0) {
average = Math.abs(balance / TOTAL_DAYS_FOR_AVERAGE);
}
LimitLine average_line = new LimitLine(average);
average_line.setLineWidth(1.5f);
average_line.enableDashedLine(15f, 10f, 0f);
average_line.setLabel(getString(R.string.average, getAverage(average)));
if (average > (mTargetValue)) {
average_line.setLineColor(Color.GREEN);
average_line.setTextColor(Color.GREEN);
}
else {
average_line.setLineColor(Color.RED);
average_line.setTextColor(Color.RED);
}
if (average > 0) {
if (GBApplication.getPrefs().getBoolean("charts_show_average", true)) {
barChart.getAxisLeft().addLimitLine(average_line);
}
}
if (supportsSleepScore()) {
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue), sleepScoreLineData);
}
return new WeekChartsData(barData, new PreformattedXIndexLabelFormatter(labels), getBalanceMessage(balance, mTargetValue));
}
protected List<SleepScoreSample> getSleepScoreSamples(DBHandler db, GBDevice device, Calendar day) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
day.add(Calendar.HOUR, 0);
startTs = (int) (day.getTimeInMillis() / 1000);
endTs = startTs + 24 * 60 * 60 - 1;
TimeSampleProvider<? extends SleepScoreSample> provider = device.getDeviceCoordinator().getSleepScoreProvider(device, db.getDaoSession());
return (List<SleepScoreSample>) provider.getAllSamples(startTs * 1000L, endTs * 1000L);
}
protected LineDataSet createSleepScoreDataSet(final List<Entry> values) {
final LineDataSet lineDataSet = new LineDataSet(values, getString(R.string.sleep_score));
lineDataSet.setColor(getResources().getColor(R.color.chart_light_sleep_light));
lineDataSet.setDrawCircles(false);
lineDataSet.setLineWidth(2f);
lineDataSet.setFillAlpha(255);
lineDataSet.setCircleRadius(5f);
lineDataSet.setDrawCircles(true);
lineDataSet.setDrawCircleHole(true);
lineDataSet.setCircleColor(getResources().getColor(R.color.chart_light_sleep_light));
lineDataSet.setAxisDependency(YAxis.AxisDependency.LEFT);
lineDataSet.setDrawValues(true);
lineDataSet.setValueTextSize(10f);
lineDataSet.setValueTextColor(CHART_TEXT_COLOR);
lineDataSet.setValueFormatter(new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return String.format(Locale.ROOT, "%d", (int) value);
}
});
return lineDataSet;
};
protected void setupWeekChart() {
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekChart.getDescription().setText("");
mWeekChart.setFitBars(true);
configureBarLineChartDefaults(mWeekChart);
XAxis x = mWeekChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
x.setPosition(XAxis.XAxisPosition.BOTTOM);
YAxis y = mWeekChart.getAxisLeft();
y.setDrawGridLines(false);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setDrawZeroLine(true);
y.setSpaceBottom(0);
y.setAxisMinimum(0);
y.setValueFormatter(getYAxisFormatter());
y.setEnabled(true);
YAxis yAxisRight = mWeekChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
}
private List<? extends ActivitySample> getSamplesOfDay(DBHandler db, Calendar day, int offsetHours, GBDevice device) {
int startTs;
int endTs;
day = (Calendar) day.clone(); // do not modify the caller's argument
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
day.add(Calendar.HOUR, offsetHours);
startTs = (int) (day.getTimeInMillis() / 1000);
endTs = startTs + 24 * 60 * 60 - 1;
return getSamples(db, device, startTs, endTs);
}
@Override
protected List<? extends ActivitySample> getSamples(DBHandler db, GBDevice device, int tsFrom, int tsTo) {
return super.getAllSamples(db, device, tsFrom, tsTo);
}
protected static class MyChartsData extends ChartsData {
private final WeekChartsData<BarData> weekBeforeData;
MyChartsData(WeekChartsData<BarData> weekBeforeData) {
this.weekBeforeData = weekBeforeData;
}
WeekChartsData<BarData> getWeekBeforeData() {
return weekBeforeData;
}
}
protected ActivityAmounts getActivityAmountsForDay(DBHandler db, Calendar day, GBDevice device) {
LimitedQueue<Integer, ActivityAmounts> activityAmountCache = null;
ActivityAmounts amounts = null;
Activity activity = getActivity();
int key = (int) (day.getTimeInMillis() / 1000) + (mOffsetHours * 3600);
if (activity != null) {
activityAmountCache = ((ActivityChartsActivity) activity).mActivityAmountCache;
amounts = activityAmountCache.lookup(key);
}
if (amounts == null) {
ActivityAnalysis analysis = new ActivityAnalysis();
amounts = analysis.calculateActivityAmounts(getSamplesOfDay(db, day, mOffsetHours, device));
if (activityAmountCache != null) {
activityAmountCache.add(key, amounts);
}
}
return amounts;
}
private int getRangeDays(){
if (GBApplication.getPrefs().getBoolean("charts_range", true)) {
return 30;}
else{
return 7;
}
}
public boolean supportsSleepScore() {
final GBDevice device = getChartsHost().getDevice();
return device.getDeviceCoordinator().supportsSleepScore(device);
}
abstract String getAverage(float value);
abstract int getGoal();
abstract int getOffsetHours();
abstract float[] getTotalsForActivityAmounts(ActivityAmounts activityAmounts);
abstract String formatPieValue(long value);
abstract String[] getPieLabels();
abstract ValueFormatter getPieValueFormatter();
abstract ValueFormatter getBarValueFormatter();
abstract ValueFormatter getYAxisFormatter();
abstract int[] getColors();
abstract String getPieDescription(int targetValue);
protected abstract long calculateBalance(ActivityAmounts amounts);
protected abstract String getBalanceMessage(long balance, int targetValue);
protected class WeekChartsData<T extends ChartData<?>> extends DefaultChartsData<T> {
private final String balanceMessage;
private LineData sleepScoresLineData;
public WeekChartsData(T data, PreformattedXIndexLabelFormatter xIndexLabelFormatter, String balanceMessage) {
super(data, xIndexLabelFormatter);
this.balanceMessage = balanceMessage;
}
public WeekChartsData(T data, PreformattedXIndexLabelFormatter xIndexLabelFormatter, String balanceMessage, LineData sleepScores) {
super(data, xIndexLabelFormatter);
this.balanceMessage = balanceMessage;
this.sleepScoresLineData = sleepScores;
}
public String getBalanceMessage() {
return balanceMessage;
}
public LineData getSleepScoreData() { return sleepScoresLineData; }
}
}
@@ -177,7 +177,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
case "vo2max": case "vo2max":
return new VO2MaxFragment(); return new VO2MaxFragment();
case "stress": case "stress":
return new StressChartFragment(); return StressCollectionFragment.newInstance(enabledTabsList.size() == 1);
case "pai": case "pai":
return new PaiChartFragment(); return new PaiChartFragment();
case "stepsweek": case "stepsweek":
@@ -53,7 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
// Based on StressChartFragment // Based on StressDailyFragment
public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> { public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class); protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class);
@@ -0,0 +1,44 @@
/* Copyright (C) 2024 a0z, José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Bundle;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.adapter.NestedFragmentAdapter;
import nodomain.freeyourgadget.gadgetbridge.adapter.StressFragmentAdapter;
public class StressCollectionFragment extends AbstractCollectionFragment {
public StressCollectionFragment() {
}
public static StressCollectionFragment newInstance(final boolean allowSwipe) {
final StressCollectionFragment fragment = new StressCollectionFragment();
final Bundle args = new Bundle();
args.putBoolean(ARG_ALLOW_SWIPE, allowSwipe);
fragment.setArguments(args);
return fragment;
}
@Override
public NestedFragmentAdapter getNestedFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
return new StressFragmentAdapter(this, getChildFragmentManager());
}
}
@@ -16,7 +16,6 @@
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.content.Context;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
@@ -29,8 +28,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.animation.Easing;
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.LineChart;
@@ -64,15 +61,13 @@ import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class StressChartFragment extends AbstractChartFragment<StressChartFragment.StressChartsData> { public class StressDailyFragment extends StressFragment<StressDailyFragment.StressChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class); protected static final Logger LOG = LoggerFactory.getLogger(StressDailyFragment.class);
private LineChart mStressChart; private LineChart mStressChart;
private PieChart mStressLevelsPieChart; private PieChart mStressLevelsPieChart;
@@ -82,27 +77,17 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
private TextView stressChartHighTime; private TextView stressChartHighTime;
private TextView stressDate; private TextView stressDate;
private int BACKGROUND_COLOR;
private int DESCRIPTION_COLOR;
private int CHART_TEXT_COLOR;
private int LEGEND_TEXT_COLOR;
private String STRESS_AVERAGE_LABEL; private String STRESS_AVERAGE_LABEL;
private final Prefs prefs = GBApplication.getPrefs(); private final Prefs prefs = GBApplication.getPrefs();
private final boolean CHARTS_SLEEP_RANGE_24H = prefs.getBoolean("chart_sleep_range_24h", false); private final boolean CHARTS_SLEEP_RANGE_24H = prefs.getBoolean("chart_sleep_range_24h", false);
private final boolean SHOW_CHARTS_AVERAGE = prefs.getBoolean("charts_show_average", true); private final boolean SHOW_CHARTS_AVERAGE = prefs.getBoolean("charts_show_average", true);
private boolean showStressLevelInPercents = false; private boolean showStressLevelInPercents = false;
@Override @Override
protected void init() { protected void init() {
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext()); super.init();
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
STRESS_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_stress_average); STRESS_AVERAGE_LABEL = requireContext().getString(R.string.charts_legend_stress_average);
} }
@@ -116,7 +101,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
day.set(Calendar.SECOND, 0); day.set(Calendar.SECOND, 0);
final int tsStart = (int) (day.getTimeInMillis() / 1000); final int tsStart = (int) (day.getTimeInMillis() / 1000);
tsEnd = tsStart + 24 * 60 * 60 - 1; tsEnd = tsStart + 24 * 60 * 60 - 1;
final List<? extends StressSample> samples = getSamples(db, device, tsStart, tsEnd); final List<? extends StressSample> samples = getStressSamples(db, device, tsStart, tsEnd);
LOG.info("Got {} stress samples", samples.size()); LOG.info("Got {} stress samples", samples.size());
@@ -196,11 +181,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
} }
} }
@Override
public String getTitle() {
return getString(R.string.menuitem_stress);
}
@Override @Override
public View onCreateView(final LayoutInflater inflater, public View onCreateView(final LayoutInflater inflater,
final ViewGroup container, final ViewGroup container,
@@ -277,14 +257,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
@Override @Override
protected void setupLegend(final Chart<?> chart) { protected void setupLegend(final Chart<?> chart) {
final List<LegendEntry> legendEntries = new ArrayList<>(StressType.values().length + 1); final List<LegendEntry> legendEntries = createLegendEntries(chart);
for (final StressType stressType : StressType.values()) {
final LegendEntry entry = new LegendEntry();
entry.label = stressType.getLabel(requireContext());
entry.formColor = stressType.getColor(requireContext());
legendEntries.add(entry);
}
if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) { if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) {
final LegendEntry averageEntry = new LegendEntry(); final LegendEntry averageEntry = new LegendEntry();
@@ -303,54 +276,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
mStressLevelsPieChart.invalidate(); mStressLevelsPieChart.invalidate();
} }
private List<? extends StressSample> getSamples(final DBHandler db, final GBDevice device, int tsStart, int tsEnd) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
final TimeSampleProvider<? extends StressSample> sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession());
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
}
protected void ensureStartAndEndSamples(final List<StressSample> samples, int tsStart, int tsEnd) {
if (samples == null || samples.isEmpty()) {
return;
}
final long tsEndMillis = tsEnd * 1000L;
final long tsStartMillis = tsStart * 1000L;
final StressSample lastSample = samples.get(samples.size() - 1);
if (lastSample.getTimestamp() < tsEndMillis) {
samples.add(new EmptyStressSample(tsEndMillis));
}
final StressSample firstSample = samples.get(0);
if (firstSample.getTimestamp() > tsStartMillis) {
samples.add(0, new EmptyStressSample(tsStartMillis));
}
}
protected static final class EmptyStressSample implements StressSample {
private final long ts;
public EmptyStressSample(final long ts) {
this.ts = ts;
}
@Override
public Type getType() {
return Type.AUTOMATIC;
}
@Override
public int getStress() {
return -1;
}
@Override
public long getTimestamp() {
return ts;
}
}
protected class StressChartsDataBuilder { protected class StressChartsDataBuilder {
private static final int UNKNOWN_VAL = 2; private static final int UNKNOWN_VAL = 2;
@@ -407,8 +332,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
} }
private void processSample(final StressSample sample) { private void processSample(final StressSample sample) {
//LOG.debug("Processing sample {} {}", sdf.format(new Date(sample.getTimestamp())), sample.getStress());
final StressType stressType = StressType.fromStress(sample.getStress(), stressRanges); final StressType stressType = StressType.fromStress(sample.getStress(), stressRanges);
final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L)); final int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L));
@@ -442,8 +365,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
} else { } else {
set(ts, stressType, sample.getStress()); set(ts, stressType, sample.getStress());
} }
} else { } else {
if (ts - previousTs > sampleRate * 10) { if (ts - previousTs > sampleRate * 10) {
// More than 15 minutes since last sample // More than 15 minutes since last sample
@@ -567,43 +488,4 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
return totalStressTime; return totalStressTime;
} }
} }
public enum StressType {
UNKNOWN(R.string.unknown, R.color.chart_stress_unknown),
RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed),
MILD(R.string.stress_mild, R.color.chart_stress_mild),
MODERATE(R.string.stress_moderate, R.color.chart_stress_moderate),
HIGH(R.string.stress_high, R.color.chart_stress_high),
;
private final int labelId;
private final int colorId;
StressType(final int labelId, final int colorId) {
this.labelId = labelId;
this.colorId = colorId;
}
public String getLabel(final Context context) {
return context.getString(labelId);
}
public int getColor(final Context context) {
return ContextCompat.getColor(context, colorId);
}
public static StressType fromStress(final int stress, final int[] stressRanges) {
if (stress < stressRanges[0]) {
return StressType.UNKNOWN;
} else if (stress < stressRanges[1]) {
return StressType.RELAXED;
} else if (stress < stressRanges[2]) {
return StressType.MILD;
} else if (stress < stressRanges[3]) {
return StressType.MODERATE;
} else {
return StressType.HIGH;
}
}
}
} }
@@ -0,0 +1,178 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.content.Context;
import androidx.core.content.ContextCompat;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.components.LegendEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
abstract class StressFragment<D extends ChartsData> extends AbstractChartFragment<D> {
protected int BACKGROUND_COLOR;
protected int DESCRIPTION_COLOR;
protected int CHART_TEXT_COLOR;
protected int LEGEND_TEXT_COLOR;
@Override
public String getTitle() {
return getString(R.string.menuitem_stress);
}
@Override
protected void init() {
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
}
protected List<? extends StressSample> getStressSamples(DBHandler db, GBDevice device, int tsStart, int tsEnd) {
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
final TimeSampleProvider<? extends StressSample> sampleProvider = coordinator.getStressSampleProvider(device, db.getDaoSession());
return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L);
}
protected void ensureStartAndEndSamples(final List<StressSample> samples, int tsStart, int tsEnd) {
if (samples == null || samples.isEmpty()) {
return;
}
final long tsEndMillis = tsEnd * 1000L;
final long tsStartMillis = tsStart * 1000L;
final StressSample lastSample = samples.get(samples.size() - 1);
if (lastSample.getTimestamp() < tsEndMillis) {
samples.add(new EmptyStressSample(tsEndMillis));
}
final StressSample firstSample = samples.get(0);
if (firstSample.getTimestamp() > tsStartMillis) {
samples.add(0, new EmptyStressSample(tsStartMillis));
}
}
protected List<LegendEntry> createLegendEntries(Chart<?> chart) {
List<LegendEntry> legendEntries = new ArrayList<>(StressType.values().length);
for (final StressType stressType : StressType.values()) {
final LegendEntry entry = new LegendEntry();
entry.label = stressType.getLabel(requireContext());
entry.formColor = stressType.getColor(requireContext());
legendEntries.add(entry);
}
return legendEntries;
}
protected Map<StressType, Integer> calculateStressTotals(List<? extends StressSample> samples, int[] stressRanges, int sampleRate) {
Map<StressType, Integer> result = new HashMap<>();
for (StressType type : StressType.values()) {
result.put(type, 0);
}
StressSample prevSample = null;
for (StressSample sample : samples) {
if (prevSample != null && sample.getStress() >= 0) {
long durationSinceLastSample = sample.getTimestamp() - prevSample.getTimestamp();
durationSinceLastSample = Math.min(durationSinceLastSample, sampleRate * 10 * 1000); // Cap at 10x sample rate
StressType type = StressType.fromStress(prevSample.getStress(), stressRanges);
if (type != StressType.UNKNOWN) {
int seconds = (int) (durationSinceLastSample / 1000);
result.put(type, result.get(type) + seconds);
}
}
prevSample = sample;
}
return result;
}
protected int calculateAverageStress(List<? extends StressSample> samples) {
long sum = 0;
int count = 0;
for (StressSample sample : samples) {
if (sample.getStress() > 0) {
sum += sample.getStress();
count++;
}
}
return count > 0 ? Math.round((float) sum / count) : 0;
}
protected static final class EmptyStressSample implements StressSample {
private final long ts;
public EmptyStressSample(final long ts) {
this.ts = ts;
}
@Override
public Type getType() {
return Type.AUTOMATIC;
}
@Override
public int getStress() {
return -1;
}
@Override
public long getTimestamp() {
return ts;
}
}
public enum StressType {
UNKNOWN(R.string.unknown, R.color.chart_stress_unknown),
RELAXED(R.string.stress_relaxed, R.color.chart_stress_relaxed),
MILD(R.string.stress_mild, R.color.chart_stress_mild),
MODERATE(R.string.stress_moderate, R.color.chart_stress_moderate),
HIGH(R.string.stress_high, R.color.chart_stress_high);
private final int labelId;
private final int colorId;
StressType(final int labelId, final int colorId) {
this.labelId = labelId;
this.colorId = colorId;
}
public String getLabel(final Context context) {
return context.getString(labelId);
}
public int getColor(final Context context) {
return ContextCompat.getColor(context, colorId);
}
public static StressType fromStress(final int stress, final int[] stressRanges) {
if (stress < stressRanges[0]) {
return StressType.UNKNOWN;
} else if (stress < stressRanges[1]) {
return StressType.RELAXED;
} else if (stress < stressRanges[2]) {
return StressType.MILD;
} else if (stress < stressRanges[3]) {
return StressType.MODERATE;
} else {
return StressType.HIGH;
}
}
}
}
@@ -0,0 +1,513 @@
/* Copyright (C) 2023-2024
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.RelativeSizeSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.Chart;
import com.github.mikephil.charting.charts.PieChart;
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.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.ValueFormatter;
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public class StressPeriodFragment extends StressFragment<StressPeriodFragment.MyChartsData> {
protected static final Logger LOG = LoggerFactory.getLogger(StressPeriodFragment.class);
protected int TOTAL_DAYS = getRangeDays();
protected int TOTAL_DAYS_FOR_AVERAGE = 0;
private TextView relaxedStressTimeText;
private TextView mildStressTimeText;
private TextView moderateStressTimeText;
private TextView highStressTimeText;
private TextView stressDatesText;
private PieChart mStressLevelsPieChart;
private BarChart mWeekChart;
private MyStressWeeklyData myStressWeeklyData;
private boolean showStressLevelInPercents = false;
protected Locale mLocale;
public static StressPeriodFragment newInstance(int totalDays) {
StressPeriodFragment fragment = new StressPeriodFragment();
Bundle args = new Bundle();
args.putInt("totalDays", totalDays);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TOTAL_DAYS = getArguments() != null ? getArguments().getInt("totalDays") : 0;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mLocale = getResources().getConfiguration().locale;
View rootView = inflater.inflate(R.layout.fragment_weekstress_chart, container, false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
rootView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
getChartsHost().enableSwipeRefresh(scrollY == 0);
});
}
mWeekChart = rootView.findViewById(R.id.weekstresschart);
mStressLevelsPieChart = rootView.findViewById(R.id.stress_pie_chart);
relaxedStressTimeText = rootView.findViewById(R.id.stress_chart_relaxed_time);
mildStressTimeText = rootView.findViewById(R.id.stress_chart_mild_time);
moderateStressTimeText = rootView.findViewById(R.id.stress_chart_moderate_time);
highStressTimeText = rootView.findViewById(R.id.stress_chart_high_time);
stressDatesText = rootView.findViewById(R.id.stress_dates);
setupPieChart();
setupWeekChart();
refresh();
return rootView;
}
@Override
protected MyChartsData refreshInBackground(ChartsHost chartsHost, DBHandler db, GBDevice device) {
Calendar day = Calendar.getInstance();
day.setTime(chartsHost.getEndDate());
DefaultChartsData<BarData> weekBeforeData = refreshWeekBeforeStressData(db, mWeekChart, day, device);
myStressWeeklyData = getMyStressWeeklyData(db, day, device);
return new MyChartsData(weekBeforeData);
}
@Override
protected void updateChartsnUIThread(MyChartsData mcd) {
setupLegend(mWeekChart);
mWeekChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317
mWeekChart.setData(mcd.getWeekBeforeData().getData());
mWeekChart.getXAxis().setValueFormatter(mcd.getWeekBeforeData().getXValueFormatter());
mWeekChart.getBarData().setValueTextSize(10f);
updatePieChart();
updateStressTimeTexts();
stressDatesText.setText(DateTimeUtils.formatDaysUntil(TOTAL_DAYS, getTSEnd()));
}
private void updatePieChart() {
List<PieEntry> pieEntries = new ArrayList<>();
List<Integer> pieColors = new ArrayList<>();
if (TOTAL_DAYS_FOR_AVERAGE > 0 && myStressWeeklyData != null) {
long totalTime = myStressWeeklyData.totalStressTime();
if (totalTime > 0) {
if (myStressWeeklyData.totalRelaxed() > 0) {
pieEntries.add(new PieEntry(myStressWeeklyData.totalRelaxed(),
StressType.RELAXED.getLabel(getContext())));
pieColors.add(StressType.RELAXED.getColor(getContext()));
}
if (myStressWeeklyData.totalMild() > 0) {
pieEntries.add(new PieEntry(myStressWeeklyData.totalMild(),
StressType.MILD.getLabel(getContext())));
pieColors.add(StressType.MILD.getColor(getContext()));
}
if (myStressWeeklyData.totalModerate() > 0) {
pieEntries.add(new PieEntry(myStressWeeklyData.totalModerate(),
StressType.MODERATE.getLabel(getContext())));
pieColors.add(StressType.MODERATE.getColor(getContext()));
}
if (myStressWeeklyData.totalHigh() > 0) {
pieEntries.add(new PieEntry(myStressWeeklyData.totalHigh(),
StressType.HIGH.getLabel(getContext())));
pieColors.add(StressType.HIGH.getColor(getContext()));
}
}
}
if (pieEntries.isEmpty()) {
pieEntries.add(new PieEntry(1));
pieColors.add(getResources().getColor(R.color.gauge_line_color));
}
PieDataSet pieDataSet = new PieDataSet(pieEntries, "");
pieDataSet.setColors(pieColors);
pieDataSet.setValueTextColor(DESCRIPTION_COLOR);
pieDataSet.setValueTextSize(13f);
pieDataSet.setXValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
pieDataSet.setYValuePosition(PieDataSet.ValuePosition.OUTSIDE_SLICE);
pieDataSet.setDrawValues(false);
pieDataSet.setSliceSpace(2f);
PieData pieData = new PieData(pieDataSet);
mStressLevelsPieChart.setData(pieData);
// Set center text with average stress level
if (myStressWeeklyData != null && myStressWeeklyData.averageStress() > 0) {
int avgStress = myStressWeeklyData.averageStress();
int noc = String.valueOf(avgStress).length();
SpannableString centerText = new SpannableString(avgStress + "\n" +
getContext().getString(R.string.stress_average));
centerText.setSpan(new RelativeSizeSpan(1.75f), 0, noc, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
centerText.setSpan(new RelativeSizeSpan(0.72f), noc, centerText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mStressLevelsPieChart.setCenterText(centerText);
} else {
SpannableString centerText = new SpannableString("-\n" +
getContext().getString(R.string.stress_average));
centerText.setSpan(new RelativeSizeSpan(1.25f), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
centerText.setSpan(new RelativeSizeSpan(0.72f), 2, centerText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
mStressLevelsPieChart.setCenterText(centerText);
}
}
private void updateStressTimeTexts() {
if (TOTAL_DAYS_FOR_AVERAGE > 0 && myStressWeeklyData != null) {
int relaxedAvg = (int) (myStressWeeklyData.totalRelaxed() / TOTAL_DAYS_FOR_AVERAGE);
int mildAvg = (int) (myStressWeeklyData.totalMild() / TOTAL_DAYS_FOR_AVERAGE);
int moderateAvg = (int) (myStressWeeklyData.totalModerate() / TOTAL_DAYS_FOR_AVERAGE);
int highAvg = (int) (myStressWeeklyData.totalHigh() / TOTAL_DAYS_FOR_AVERAGE);
if (showStressLevelInPercents) {
long totalDailyAvg = relaxedAvg + mildAvg + moderateAvg + highAvg;
relaxedStressTimeText.setText(String.format(Locale.ROOT, "%d%%",
totalDailyAvg > 0 ? Math.round(100f * relaxedAvg / totalDailyAvg) : 0));
mildStressTimeText.setText(String.format(Locale.ROOT, "%d%%",
totalDailyAvg > 0 ? Math.round(100f * mildAvg / totalDailyAvg) : 0));
moderateStressTimeText.setText(String.format(Locale.ROOT, "%d%%",
totalDailyAvg > 0 ? Math.round(100f * moderateAvg / totalDailyAvg) : 0));
highStressTimeText.setText(String.format(Locale.ROOT, "%d%%",
totalDailyAvg > 0 ? Math.round(100f * highAvg / totalDailyAvg) : 0));
} else {
relaxedStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(relaxedAvg, TimeUnit.SECONDS));
mildStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(mildAvg, TimeUnit.SECONDS));
moderateStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(moderateAvg, TimeUnit.SECONDS));
highStressTimeText.setText(DateTimeUtils.formatDurationHoursMinutes(highAvg, TimeUnit.SECONDS));
}
} else {
relaxedStressTimeText.setText("-");
mildStressTimeText.setText("-");
moderateStressTimeText.setText("-");
highStressTimeText.setText("-");
}
}
private MyStressWeeklyData getMyStressWeeklyData(DBHandler db, Calendar day, GBDevice device) {
day = (Calendar) day.clone(); // do not modify the caller's argument
day.add(Calendar.DATE, -TOTAL_DAYS + 1);
TOTAL_DAYS_FOR_AVERAGE = 0;
long relaxedWeeklyTotal = 0;
long mildWeeklyTotal = 0;
long moderateWeeklyTotal = 0;
long highWeeklyTotal = 0;
long totalStressTime = 0;
long avgStressSum = 0;
long avgStressSamples = 0;
int[] stressRanges = device.getDeviceCoordinator().getStressRanges();
showStressLevelInPercents = device.getDeviceCoordinator().showStressLevelInPercents();
DeviceCoordinator coordinator = device.getDeviceCoordinator();
int sampleRate = 60; // Default sample rate
int[] params = coordinator.getStressChartParameters();
if (params != null && params.length > 0) {
sampleRate = params[0];
}
for (int counter = 0; counter < TOTAL_DAYS; counter++) {
Calendar dayStart = (Calendar) day.clone();
Calendar dayEnd = (Calendar) day.clone();
dayEnd.add(Calendar.DAY_OF_MONTH, 1);
List<? extends StressSample> samples = getStressSamples(db, device,
(int) (dayStart.getTimeInMillis() / 1000),
(int) (dayEnd.getTimeInMillis() / 1000));
if (!samples.isEmpty()) {
TOTAL_DAYS_FOR_AVERAGE++;
Map<StressType, Integer> dailyTotals = calculateStressTotals(samples, stressRanges, sampleRate);
relaxedWeeklyTotal += dailyTotals.getOrDefault(StressType.RELAXED, 0);
mildWeeklyTotal += dailyTotals.getOrDefault(StressType.MILD, 0);
moderateWeeklyTotal += dailyTotals.getOrDefault(StressType.MODERATE, 0);
highWeeklyTotal += dailyTotals.getOrDefault(StressType.HIGH, 0);
// Calculate average stress for the day
int dailyAverage = calculateAverageStress(samples);
if (dailyAverage > 0) {
avgStressSum += dailyAverage;
avgStressSamples++;
}
}
day.add(Calendar.DATE, 1);
}
totalStressTime = relaxedWeeklyTotal + mildWeeklyTotal + moderateWeeklyTotal + highWeeklyTotal;
int averageStress = avgStressSamples > 0 ? Math.round((float) avgStressSum / avgStressSamples) : 0;
return new MyStressWeeklyData(relaxedWeeklyTotal, mildWeeklyTotal, moderateWeeklyTotal,
highWeeklyTotal, totalStressTime, averageStress);
}
private DefaultChartsData<BarData> refreshWeekBeforeStressData(DBHandler db, BarChart chart, Calendar day, GBDevice device) {
day = (Calendar) day.clone();
day.set(Calendar.HOUR_OF_DAY, 0);
day.set(Calendar.MINUTE, 0);
day.set(Calendar.SECOND, 0);
day.set(Calendar.MILLISECOND, 0);
day.add(Calendar.DATE, -TOTAL_DAYS + 1);
List<BarEntry> entries = new ArrayList<>();
ArrayList<String> labels = new ArrayList<>();
int[] colors = new int[TOTAL_DAYS * 5]; // 4 stress types + unknown type
int colorIndex = 0;
DeviceCoordinator coordinator = device.getDeviceCoordinator();
int sampleRate = 60;
int[] params = coordinator.getStressChartParameters();
if (params != null && params.length > 0) {
sampleRate = params[0];
}
Calendar now = Calendar.getInstance();
for (int counter = 0; counter < TOTAL_DAYS; counter++) {
Calendar dayStart = (Calendar) day.clone();
Calendar dayEnd = (Calendar) day.clone();
dayEnd.add(Calendar.DAY_OF_MONTH, 1);
List<? extends StressSample> samples = getStressSamples(db, device,
(int) (dayStart.getTimeInMillis() / 1000),
(int) (dayEnd.getTimeInMillis() / 1000));
Map<StressType, Integer> dailyTotals = calculateStressTotals(samples,
device.getDeviceCoordinator().getStressRanges(), sampleRate);
float[] yValues = new float[5]; // For stacked bar chart
int idx = 0;
float totalMinutesTracked = dailyTotals.values().stream().reduce(0, Integer::sum) / 60f;
// Calculate the total possible minutes for this day, excluding future time
float totalPossibleMinutes;
if (dayEnd.before(now) || dayEnd.equals(now)) {
// Full day in the past
totalPossibleMinutes = 24 * 60;
} else if (dayStart.after(now)) {
// Full day in the future
totalPossibleMinutes = 0;
} else {
// Partial day (current day) - only count minutes up to now
long minutesFromStartToNow = (now.getTimeInMillis() - dayStart.getTimeInMillis()) / (1000 * 60);
totalPossibleMinutes = Math.max(0, minutesFromStartToNow);
}
float untrackedMins = Math.max(totalPossibleMinutes - totalMinutesTracked, 0);
yValues[idx++] = dailyTotals.get(StressType.HIGH) / 60f;
yValues[idx++] = dailyTotals.get(StressType.MODERATE) / 60f;
yValues[idx++] = dailyTotals.get(StressType.MILD) / 60f;
yValues[idx++] = dailyTotals.get(StressType.RELAXED) / 60f;
yValues[idx++] = untrackedMins;
colors[colorIndex++] = StressType.HIGH.getColor(getContext());
colors[colorIndex++] = StressType.MODERATE.getColor(getContext());
colors[colorIndex++] = StressType.MILD.getColor(getContext());
colors[colorIndex++] = StressType.RELAXED.getColor(getContext());
colors[colorIndex++] = StressType.UNKNOWN.getColor(getContext());
entries.add(new BarEntry(counter, yValues));
labels.add(TOTAL_DAYS > 7
? String.valueOf(day.get(Calendar.DAY_OF_MONTH))
: day.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.SHORT, mLocale));
day.add(Calendar.DATE, 1);
}
BarDataSet set = new BarDataSet(entries, "");
set.setDrawValues(false);
set.setAxisDependency(YAxis.AxisDependency.LEFT);
set.setColors(colors);
set.setStackLabels(new String[]{
StressType.HIGH.getLabel(getContext()),
StressType.MODERATE.getLabel(getContext()),
StressType.MILD.getLabel(getContext()),
StressType.RELAXED.getLabel(getContext()),
StressType.UNKNOWN.getLabel(getContext())
});
ArrayList<IBarDataSet> dataSets = new ArrayList<>();
dataSets.add(set);
BarData barData = new BarData(dataSets);
barData.setBarWidth(0.9f);
return new DefaultChartsData<>(barData, new PreformattedXIndexLabelFormatter(labels));
}
private void setupPieChart() {
mStressLevelsPieChart.setBackgroundColor(BACKGROUND_COLOR);
mStressLevelsPieChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mStressLevelsPieChart.setEntryLabelColor(DESCRIPTION_COLOR);
mStressLevelsPieChart.getDescription().setText("");
mStressLevelsPieChart.setNoDataText("");
mStressLevelsPieChart.setTouchEnabled(false);
mStressLevelsPieChart.setCenterTextColor(GBApplication.getTextColor(getContext()));
mStressLevelsPieChart.setCenterTextSize(18f);
mStressLevelsPieChart.setHoleColor(requireContext().getResources().getColor(R.color.transparent));
mStressLevelsPieChart.setHoleRadius(85);
mStressLevelsPieChart.setDrawEntryLabels(false);
mStressLevelsPieChart.getLegend().setEnabled(false);
}
protected void setupWeekChart() {
mWeekChart.setBackgroundColor(BACKGROUND_COLOR);
mWeekChart.getDescription().setTextColor(DESCRIPTION_COLOR);
mWeekChart.getDescription().setText("");
mWeekChart.setFitBars(true);
configureBarLineChartDefaults(mWeekChart);
XAxis x = mWeekChart.getXAxis();
x.setDrawLabels(true);
x.setDrawGridLines(false);
x.setEnabled(true);
x.setTextColor(CHART_TEXT_COLOR);
x.setDrawLimitLinesBehindData(true);
x.setPosition(XAxis.XAxisPosition.BOTTOM);
if (TOTAL_DAYS > 7) {
x.setSpaceMin(0);
}
YAxis y = mWeekChart.getAxisLeft();
y.setDrawGridLines(false);
y.setEnabled(true);
y.setDrawTopYLabelEntry(false);
y.setTextColor(CHART_TEXT_COLOR);
y.setDrawZeroLine(true);
y.setSpaceBottom(0);
y.setAxisMinimum(0);
y.setAxisMaximum(24 * 60);
y.setValueFormatter(getYAxisFormatter());
YAxis yAxisRight = mWeekChart.getAxisRight();
yAxisRight.setDrawGridLines(false);
yAxisRight.setEnabled(false);
yAxisRight.setDrawLabels(false);
yAxisRight.setDrawTopYLabelEntry(false);
yAxisRight.setTextColor(CHART_TEXT_COLOR);
if (TOTAL_DAYS > 7) {
mWeekChart.setRenderer(new AngledLabelsChartRenderer(mWeekChart, mWeekChart.getAnimator(),
mWeekChart.getViewPortHandler()));
} else {
mWeekChart.setScaleEnabled(false);
mWeekChart.setTouchEnabled(false);
}
}
@Override
protected void setupLegend(Chart<?> chart) {
List<LegendEntry> legendEntries = createLegendEntries(chart);
chart.getLegend().setCustom(legendEntries);
chart.getLegend().setTextColor(LEGEND_TEXT_COLOR);
chart.getLegend().setWordWrapEnabled(true);
chart.getLegend().setHorizontalAlignment(Legend.LegendHorizontalAlignment.CENTER);
}
@Override
protected void renderCharts() {
mWeekChart.invalidate();
mStressLevelsPieChart.invalidate();
}
private int getRangeDays() {
return GBApplication.getPrefs().getBoolean("charts_range", true) ? 30 : 7;
}
ValueFormatter getYAxisFormatter() {
return new ValueFormatter() {
@Override
public String getFormattedValue(float value) {
return DateTimeUtils.minutesToHHMM((int) value);
}
};
}
@Override
protected boolean isSingleDay() {
return false;
}
protected static class MyChartsData extends ChartsData {
private final DefaultChartsData<BarData> weekBeforeData;
public MyChartsData(DefaultChartsData<BarData> weekBeforeData) {
this.weekBeforeData = weekBeforeData;
}
public DefaultChartsData<BarData> getWeekBeforeData() {
return weekBeforeData;
}
}
private record MyStressWeeklyData(long totalRelaxed,
long totalMild,
long totalModerate,
long totalHigh,
long totalStressTime,
int averageStress) {
}
}
@@ -21,7 +21,7 @@ import android.os.Bundle;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment; import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData; import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.data.DashboardStressData;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@@ -56,7 +56,7 @@ public class DashboardStressSimpleWidget extends AbstractGaugeWidget {
return; return;
} }
final int color = StressChartFragment.StressType.fromStress( final int color = StressDailyFragment.StressType.fromStress(
stressData.value, stressData.value,
stressData.ranges stressData.ranges
).getColor(GBApplication.getContext()); ).getColor(GBApplication.getContext());
@@ -24,7 +24,7 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment; import nodomain.freeyourgadget.gadgetbridge.activities.DashboardFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressChartFragment; import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample; import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
@@ -42,7 +42,7 @@ public class DashboardStressData implements Serializable {
GBDevice stressDevice = null; GBDevice stressDevice = null;
double averageStress = -1; double averageStress = -1;
final int[] totalTime = new int[StressChartFragment.StressType.values().length]; final int[] totalTime = new int[StressDailyFragment.StressType.values().length];
try (DBHandler dbHandler = GBApplication.acquireDB()) { try (DBHandler dbHandler = GBApplication.acquireDB()) {
for (GBDevice dev : devices) { for (GBDevice dev : devices) {
@@ -57,8 +57,8 @@ public class DashboardStressData implements Serializable {
averageStress = samples.stream() averageStress = samples.stream()
.mapToInt(StressSample::getStress) .mapToInt(StressSample::getStress)
.peek(stress -> { .peek(stress -> {
final StressChartFragment.StressType stressType = StressChartFragment.StressType.fromStress(stress, stressRanges); final StressDailyFragment.StressType stressType = StressDailyFragment.StressType.fromStress(stress, stressRanges);
if (stressType != StressChartFragment.StressType.UNKNOWN) { if (stressType != StressDailyFragment.StressType.UNKNOWN) {
totalTime[stressType.ordinal() - 1] += 60; totalTime[stressType.ordinal() - 1] += 60;
} }
}) })
@@ -0,0 +1,27 @@
package nodomain.freeyourgadget.gadgetbridge.adapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressDailyFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.StressPeriodFragment;
public class StressFragmentAdapter extends NestedFragmentAdapter {
public StressFragmentAdapter(AbstractGBFragment fragment, FragmentManager childFragmentManager) {
super(fragment, childFragmentManager);
}
@Override
public Fragment createFragment(int position) {
switch (position) {
case 0:
return new StressDailyFragment();
case 1:
return StressPeriodFragment.newInstance(7);
case 2:
return StressPeriodFragment.newInstance(30);
}
return new StressDailyFragment();
}
}
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.charts.ActivityChartsActivity$PlaceholderFragment">
<TextView
android:id="@+id/stress_dates"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="20sp"
android:layout_marginTop="15dp"
android:layout_marginBottom="10dp" />
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/stress_pie_chart"
android:layout_width="fill_parent"
android:layout_height="200dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp" />
<GridLayout
android:background="@color/gauge_line_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2">
<LinearLayout
style="@style/GridTile"
android:layout_marginBottom="0dp">
<View
android:layout_width="fill_parent"
android:layout_height="5dp"
android:background="@color/chart_stress_relaxed"
android:layout_marginBottom="10dp" />
<TextView
android:id="@+id/stress_chart_relaxed_time"
android:text="@string/stats_empty_value"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stress_relaxed"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginBottom="0dp">
<View
android:layout_width="fill_parent"
android:layout_height="5dp"
android:background="@color/chart_stress_mild"
android:layout_marginBottom="10dp" />
<TextView
android:id="@+id/stress_chart_mild_time"
android:text="@string/stats_empty_value"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stress_mild"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginBottom="0dp">
<View
android:layout_width="fill_parent"
android:layout_height="5dp"
android:background="@color/chart_stress_moderate"
android:layout_marginBottom="10dp" />
<TextView
android:id="@+id/stress_chart_moderate_time"
android:text="@string/stats_empty_value"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stress_moderate"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
style="@style/GridTile"
android:layout_marginBottom="0dp">
<View
android:layout_width="fill_parent"
android:layout_height="5dp"
android:background="@color/chart_stress_high"
android:layout_marginBottom="10dp" />
<TextView
android:id="@+id/stress_chart_high_time"
android:text="@string/stats_empty_value"
android:textSize="20sp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/stress_high"
android:textSize="12sp" />
</LinearLayout>
</GridLayout>
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/weekstresschart"
android:layout_width="fill_parent"
android:layout_height="350dp"
android:layout_marginTop="15dp" />
</LinearLayout>
</ScrollView>