mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Add weekly/monthly stress charts
This commit is contained in:
-356
@@ -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; }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -177,7 +177,7 @@ public class ActivityChartsActivity extends AbstractChartsActivity {
|
||||
case "vo2max":
|
||||
return new VO2MaxFragment();
|
||||
case "stress":
|
||||
return new StressChartFragment();
|
||||
return StressCollectionFragment.newInstance(enabledTabsList.size() == 1);
|
||||
case "pai":
|
||||
return new PaiChartFragment();
|
||||
case "stepsweek":
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
// Based on StressChartFragment
|
||||
// Based on StressDailyFragment
|
||||
|
||||
public class Spo2ChartFragment extends AbstractChartFragment<Spo2ChartFragment.Spo2ChartsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(Spo2ChartFragment.class);
|
||||
|
||||
+44
@@ -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());
|
||||
}
|
||||
}
|
||||
+6
-124
@@ -16,7 +16,6 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -29,8 +28,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.github.mikephil.charting.animation.Easing;
|
||||
import com.github.mikephil.charting.charts.Chart;
|
||||
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.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;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class StressChartFragment extends AbstractChartFragment<StressChartFragment.StressChartsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StressChartFragment.class);
|
||||
public class StressDailyFragment extends StressFragment<StressDailyFragment.StressChartsData> {
|
||||
protected static final Logger LOG = LoggerFactory.getLogger(StressDailyFragment.class);
|
||||
|
||||
private LineChart mStressChart;
|
||||
private PieChart mStressLevelsPieChart;
|
||||
@@ -82,27 +77,17 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
private TextView stressChartHighTime;
|
||||
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 final Prefs prefs = GBApplication.getPrefs();
|
||||
|
||||
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 boolean showStressLevelInPercents = false;
|
||||
|
||||
@Override
|
||||
protected void init() {
|
||||
BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext());
|
||||
LEGEND_TEXT_COLOR = DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext());
|
||||
CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext());
|
||||
|
||||
super.init();
|
||||
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);
|
||||
final int tsStart = (int) (day.getTimeInMillis() / 1000);
|
||||
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());
|
||||
|
||||
@@ -196,11 +181,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return getString(R.string.menuitem_stress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater,
|
||||
final ViewGroup container,
|
||||
@@ -277,14 +257,7 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
|
||||
@Override
|
||||
protected void setupLegend(final Chart<?> chart) {
|
||||
final List<LegendEntry> legendEntries = new ArrayList<>(StressType.values().length + 1);
|
||||
|
||||
for (final StressType stressType : StressType.values()) {
|
||||
final LegendEntry entry = new LegendEntry();
|
||||
entry.label = stressType.getLabel(requireContext());
|
||||
entry.formColor = stressType.getColor(requireContext());
|
||||
legendEntries.add(entry);
|
||||
}
|
||||
final List<LegendEntry> legendEntries = createLegendEntries(chart);
|
||||
|
||||
if (!CHARTS_SLEEP_RANGE_24H && SHOW_CHARTS_AVERAGE) {
|
||||
final LegendEntry averageEntry = new LegendEntry();
|
||||
@@ -303,54 +276,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
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 {
|
||||
private static final int UNKNOWN_VAL = 2;
|
||||
|
||||
@@ -407,8 +332,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
}
|
||||
|
||||
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 int ts = tsTranslation.shorten((int) (sample.getTimestamp() / 1000L));
|
||||
|
||||
@@ -442,8 +365,6 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
} else {
|
||||
set(ts, stressType, sample.getStress());
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
if (ts - previousTs > sampleRate * 10) {
|
||||
// More than 15 minutes since last sample
|
||||
@@ -567,43 +488,4 @@ public class StressChartFragment extends AbstractChartFragment<StressChartFragme
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+178
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+513
@@ -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) {
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -21,7 +21,7 @@ import android.os.Bundle;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
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.impl.GBDevice;
|
||||
|
||||
@@ -56,7 +56,7 @@ public class DashboardStressSimpleWidget extends AbstractGaugeWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
final int color = StressChartFragment.StressType.fromStress(
|
||||
final int color = StressDailyFragment.StressType.fromStress(
|
||||
stressData.value,
|
||||
stressData.ranges
|
||||
).getColor(GBApplication.getContext());
|
||||
|
||||
+4
-4
@@ -24,7 +24,7 @@ import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
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.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
@@ -42,7 +42,7 @@ public class DashboardStressData implements Serializable {
|
||||
GBDevice stressDevice = null;
|
||||
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()) {
|
||||
for (GBDevice dev : devices) {
|
||||
@@ -57,8 +57,8 @@ public class DashboardStressData implements Serializable {
|
||||
averageStress = samples.stream()
|
||||
.mapToInt(StressSample::getStress)
|
||||
.peek(stress -> {
|
||||
final StressChartFragment.StressType stressType = StressChartFragment.StressType.fromStress(stress, stressRanges);
|
||||
if (stressType != StressChartFragment.StressType.UNKNOWN) {
|
||||
final StressDailyFragment.StressType stressType = StressDailyFragment.StressType.fromStress(stress, stressRanges);
|
||||
if (stressType != StressDailyFragment.StressType.UNKNOWN) {
|
||||
totalTime[stressType.ordinal() - 1] += 60;
|
||||
}
|
||||
})
|
||||
|
||||
+27
@@ -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>
|
||||
Reference in New Issue
Block a user