diff --git a/android_app/app/src/main/java/com/health/openscale/gui/measurement/ChartMeasurementView.java b/android_app/app/src/main/java/com/health/openscale/gui/measurement/ChartMeasurementView.java index 92c50cc1..a9ae993a 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/measurement/ChartMeasurementView.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/measurement/ChartMeasurementView.java @@ -56,6 +56,9 @@ import java.util.List; import java.util.Stack; public class ChartMeasurementView extends LineChart { + public static final String COMPUTATION_METHOD_SIMPLE_MOVING_AVERAGE = "SimpleMovingAverage"; + public static final String COMPUTATION_METHOD_EXPONENTIALLY_SMOOTHED_MOVING_AVERAGE = "ExponentiallySmoothedMovingAverage"; + public enum ViewMode { DAY_OF_MONTH, WEEK_OF_MONTH, @@ -76,6 +79,10 @@ public class ChartMeasurementView extends LineChart { private boolean isInGraphKey; private ProgressBar progressBar; + private interface TrendlineComputationInterface { + public List processMeasurements(List measurementList); + } + public ChartMeasurementView(Context context) { super(context); initChart(); @@ -400,7 +407,19 @@ public class ChartMeasurementView extends LineChart { } if (prefs.getBoolean("trendLine", false)) { - addTrendLine(lineDataSets); + String selectedTrendLineComputationMethod = prefs.getString("trendlineComputationMethod", COMPUTATION_METHOD_EXPONENTIALLY_SMOOTHED_MOVING_AVERAGE); + switch (selectedTrendLineComputationMethod) { + case COMPUTATION_METHOD_EXPONENTIALLY_SMOOTHED_MOVING_AVERAGE: + addExponentiallySmoothedMovingAverage(lineDataSets); + break; + case COMPUTATION_METHOD_SIMPLE_MOVING_AVERAGE: + addSimpleMovingAverage(lineDataSets); + break; + default: + addExponentiallySmoothedMovingAverage(lineDataSets); + break; // by default fall back to exponentially smoothed moving average + } + } if (!lineDataSets.isEmpty()) { @@ -417,6 +436,14 @@ public class ChartMeasurementView extends LineChart { progressBar.setVisibility(GONE); } + private void addExponentiallySmoothedMovingAverage(List lineDataSets) { + addTrendLine(lineDataSets, this::getExponentiallySmoothedMovingAverageOfScaleMeasurements); + } + + private void addSimpleMovingAverage(List lineDataSets) { + addTrendLine(lineDataSets, this::getSimpleMovingAverageOfScaleMeasurements); + } + private void addMeasurementLine(List lineDataSets, List lineEntries, FloatMeasurementView measurementView) { LineDataSet measurementLine = new LineDataSet(lineEntries, measurementView.getName().toString()); measurementLine.setLineWidth(1.5f); @@ -435,7 +462,7 @@ public class ChartMeasurementView extends LineChart { measurementLine.setDrawValues(prefs.getBoolean("labelsEnable", false)); measurementLine.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); if (prefs.getBoolean("trendLine", false)) { - // show only data point if trend line is enabled + // show only data points if trend line or simple moving average is enabled measurementLine.enableDashedLine(0, 1, 0); } @@ -473,7 +500,7 @@ public class ChartMeasurementView extends LineChart { lineDataSets.add(goalLine); } - private List getScaleMeasurementsAsTrendline(List measurementList) { + private List getExponentiallySmoothedMovingAverageOfScaleMeasurements(List measurementList) { List trendlineList = new ArrayList<>(); // exponentially smoothed moving average with 10% smoothing @@ -493,56 +520,100 @@ public class ChartMeasurementView extends LineChart { return trendlineList; } - private void addTrendLine(List lineDataSets) { + private List getSimpleMovingAverageOfScaleMeasurements(List measurementList) { + final long NUMBER_OF_MS_IN_A_DAY = 1000 * 60 * 60 * 24; + List movingAverageList = new ArrayList<>(); + + int samplingWidth = prefs.getInt("simpleMovingAverageNumDays", 7); + + // simple moving average of the last samplingWidth days + movingAverageList.add(measurementList.get(0)); + + for (int i = 1; i < measurementList.size(); i++) { + ScaleMeasurement entry = measurementList.get(i).clone(); + int numberOfMeasurementsToAverageOut = 0; + + for (int k = i-1; k >= 0; k--){ + ScaleMeasurement previousMeasurement = measurementList.get(i - k - 1); + + if (entry.getDateTime().getTime() - previousMeasurement.getDateTime().getTime() < samplingWidth * NUMBER_OF_MS_IN_A_DAY) { + numberOfMeasurementsToAverageOut += 1; + entry.add(previousMeasurement); + } + } + + entry.multiply(1.0f/(numberOfMeasurementsToAverageOut+1)); + + movingAverageList.add(entry); + } + + return movingAverageList; + } + + private ArrayList getNonZeroScaleMeasurementsList(FloatMeasurementView measurementView) { + ArrayList nonZeroScaleMeasurementList = new ArrayList<>(); + + // filter first all zero measurements out, so that the follow-up trendline calculations are not based on them + for (int i=0; i lineDataSets, TrendlineComputationInterface trendlineComputation) { for (MeasurementView view : measurementViews) { if (view instanceof FloatMeasurementView && view.isVisible()) { final FloatMeasurementView measurementView = (FloatMeasurementView) view; - final List lineEntries = new ArrayList<>(); - - ArrayList nonZeroScaleMeasurementList = new ArrayList<>(); - - // filter first all zero measurements out, so that the follow-up trendline calculations are not based on them - for (int i=0; i nonZeroScaleMeasurementList = getNonZeroScaleMeasurementsList(measurementView); // check if we have some data left otherwise skip the measurement if (nonZeroScaleMeasurementList.isEmpty()) { continue; } // calculate the trendline from the non-zero scale measurement list - List scaleMeasurementsAsTrendlineList = getScaleMeasurementsAsTrendline(nonZeroScaleMeasurementList); + List scaleMeasurementsAsTrendlineList = trendlineComputation.processMeasurements(nonZeroScaleMeasurementList); - for (int i=0; i lineEntries = convertMeasurementsToLineEntries(measurementView, scaleMeasurementsAsTrendlineList); - Entry entry = new Entry(); - entry.setX(convertDateToInt(measurement.getDateTime())); - entry.setY(value); - Object[] extraData = new Object[3]; - extraData[0] = measurement; - extraData[1] = (i == 0) ? null : scaleMeasurementsAsTrendlineList.get(i-1); - extraData[2] = measurementView; - entry.setData(extraData); + addMeasurementLineTrend(lineDataSets, lineEntries, measurementView, getContext().getString(R.string.label_trend_line)); - lineEntries.add(entry); + // add the future entries + if (prefs.getBoolean("trendlineFuture", true)) { + addPredictionLine(lineDataSets, lineEntries, measurementView); } - - addMeasurementLineTrend(lineDataSets, lineEntries, measurementView); - addPredictionLine(lineDataSets, lineEntries, measurementView); } } } + private List convertMeasurementsToLineEntries(FloatMeasurementView measurementView, List measurementsList) { + List lineEntries = new ArrayList<>(); + for (int i = 0; i< measurementsList.size(); i++) { + ScaleMeasurement measurement = measurementsList.get(i); + float value = measurementView.getConvertedMeasurementValue(measurement); + + Entry entry = new Entry(); + entry.setX(convertDateToInt(measurement.getDateTime())); + entry.setY(value); + Object[] extraData = new Object[3]; + extraData[0] = measurement; + extraData[1] = (i == 0) ? null : measurementsList.get(i-1); + extraData[2] = measurementView; + entry.setData(extraData); + + lineEntries.add(entry); + } + + return lineEntries; + } + private void addPredictionLine(List lineDataSets, List lineEntries, FloatMeasurementView measurementView) { if (lineEntries.size() < 2) { return; @@ -605,8 +676,8 @@ public class ChartMeasurementView extends LineChart { } } - private void addMeasurementLineTrend(List lineDataSets, List lineEntries, FloatMeasurementView measurementView) { - LineDataSet measurementLine = new LineDataSet(lineEntries, measurementView.getName().toString() + "-" + getContext().getString(R.string.label_trend_line)); + private void addMeasurementLineTrend(List lineDataSets, List lineEntries, FloatMeasurementView measurementView, String name) { + LineDataSet measurementLine = new LineDataSet(lineEntries, measurementView.getName().toString() + "-" + name); measurementLine.setLineWidth(1.5f); measurementLine.setValueTextSize(10.0f); measurementLine.setColor(measurementView.getColor()); @@ -634,5 +705,4 @@ public class ChartMeasurementView extends LineChart { } } } - } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java index 479de9b3..c21ddac0 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/GraphPreferences.java @@ -19,9 +19,13 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; +import androidx.preference.DropDownPreference; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.SeekBarPreference; import com.health.openscale.R; +import com.health.openscale.gui.measurement.ChartMeasurementView; public class GraphPreferences extends PreferenceFragmentCompat { @Override @@ -29,6 +33,32 @@ public class GraphPreferences extends PreferenceFragmentCompat { setPreferencesFromResource(R.xml.graph_preferences, rootKey); setHasOptionsMenu(true); + + DropDownPreference trendlinePreference = findPreference("trendlineComputationMethod"); + SeekBarPreference simpleMovingAveragePreference = findPreference("simpleMovingAverageNumDays"); + + simpleMovingAveragePreference.setVisible( + trendlinePreference.getValue().equals( + ChartMeasurementView.COMPUTATION_METHOD_SIMPLE_MOVING_AVERAGE + ) + ); + + trendlinePreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object newValue) { + String selectedValue = (String) newValue; + boolean simpleMovingAverageEnabled = selectedValue.equals( + ChartMeasurementView.COMPUTATION_METHOD_SIMPLE_MOVING_AVERAGE + ); + + // hide selector of the number of days when simple moving average is not selected + simpleMovingAveragePreference.setVisible(simpleMovingAverageEnabled); + // scroll to the bottom to show the new preference to the user + getListView().scrollToPosition(getListView().getChildCount()); + + return true; + } + }); } @Override diff --git a/android_app/app/src/main/res/values/arrays.xml b/android_app/app/src/main/res/values/arrays.xml index 3c9d76f9..b74436ce 100644 --- a/android_app/app/src/main/res/values/arrays.xml +++ b/android_app/app/src/main/res/values/arrays.xml @@ -118,4 +118,14 @@ @string/label_time_period_set_reference_day @string/label_time_period_set_custom_range + + + @string/label_simple_moving_average + @string/label_exponentially_smoothed_moving_average + + + + SimpleMovingAverage + ExponentiallySmoothedMovingAverage + diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index ffcbf998..9a8c230b 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -139,6 +139,10 @@ Friday Saturday Sunday + Simple Moving Average + Exponentially Smoothed Moving Average + Trendline computation method + Future trend device not supported Export backup Import backup @@ -148,6 +152,7 @@ Initial weight Goal line Trendline + Number of days to average over Prediction Trend Help diff --git a/android_app/app/src/main/res/xml/graph_preferences.xml b/android_app/app/src/main/res/xml/graph_preferences.xml index 427a8870..d5d4166f 100644 --- a/android_app/app/src/main/res/xml/graph_preferences.xml +++ b/android_app/app/src/main/res/xml/graph_preferences.xml @@ -1,5 +1,6 @@ - + - + + + + + + +