1
0
mirror of https://github.com/oliexdev/openScale.git synced 2025-08-13 20:24:14 +02:00

Simple moving average on the chart (#968)

* Added simple moving average to the graph

* Fixed comments

* Removed moving average label from the legend

* Revert "Removed moving average label from the legend"

This reverts commit e7b324ff89.

* Refactored trendline computation code and settings

* Removed using the old simple moving average preferences key

* Moved trendline computations methods names to a different class, renamed the interface and reordered menu items

* Removed unnecessary imports

* moved static strings for chart computation method to ChartMeasurementView

* scroll to the new moving average preference

* renaming the internal strings

---------

Co-authored-by: oliexdev <olie.xdev@googlemail.com>
This commit is contained in:
undefiened
2023-07-23 10:25:09 +02:00
committed by GitHub
parent c085a6a097
commit ec4ecf7e07
5 changed files with 191 additions and 44 deletions

View File

@@ -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<ScaleMeasurement> processMeasurements(List<ScaleMeasurement> 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<ILineDataSet> lineDataSets) {
addTrendLine(lineDataSets, this::getExponentiallySmoothedMovingAverageOfScaleMeasurements);
}
private void addSimpleMovingAverage(List<ILineDataSet> lineDataSets) {
addTrendLine(lineDataSets, this::getSimpleMovingAverageOfScaleMeasurements);
}
private void addMeasurementLine(List<ILineDataSet> lineDataSets, List<Entry> 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<ScaleMeasurement> getScaleMeasurementsAsTrendline(List<ScaleMeasurement> measurementList) {
private List<ScaleMeasurement> getExponentiallySmoothedMovingAverageOfScaleMeasurements(List<ScaleMeasurement> measurementList) {
List<ScaleMeasurement> 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<ILineDataSet> lineDataSets) {
private List<ScaleMeasurement> getSimpleMovingAverageOfScaleMeasurements(List<ScaleMeasurement> measurementList) {
final long NUMBER_OF_MS_IN_A_DAY = 1000 * 60 * 60 * 24;
List<ScaleMeasurement> 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<ScaleMeasurement> getNonZeroScaleMeasurementsList(FloatMeasurementView measurementView) {
ArrayList<ScaleMeasurement> 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<scaleMeasurementList.size(); i++) {
ScaleMeasurement measurement = scaleMeasurementList.get(i);
float value = measurementView.getMeasurementValue(measurement);
if (value != 0.0f) {
nonZeroScaleMeasurementList.add(measurement);
}
}
return nonZeroScaleMeasurementList;
}
private void addTrendLine(List<ILineDataSet> lineDataSets, TrendlineComputationInterface trendlineComputation) {
for (MeasurementView view : measurementViews) {
if (view instanceof FloatMeasurementView && view.isVisible()) {
final FloatMeasurementView measurementView = (FloatMeasurementView) view;
final List<Entry> lineEntries = new ArrayList<>();
ArrayList<ScaleMeasurement> 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<scaleMeasurementList.size(); i++) {
ScaleMeasurement measurement = scaleMeasurementList.get(i);
float value = measurementView.getMeasurementValue(measurement);
if (value != 0.0f) {
nonZeroScaleMeasurementList.add(measurement);
}
}
ArrayList<ScaleMeasurement> 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<ScaleMeasurement> scaleMeasurementsAsTrendlineList = getScaleMeasurementsAsTrendline(nonZeroScaleMeasurementList);
List<ScaleMeasurement> scaleMeasurementsAsTrendlineList = trendlineComputation.processMeasurements(nonZeroScaleMeasurementList);
for (int i=0; i<scaleMeasurementsAsTrendlineList.size(); i++) {
ScaleMeasurement measurement = scaleMeasurementsAsTrendlineList.get(i);
float value = measurementView.getConvertedMeasurementValue(measurement);
final List<Entry> 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<Entry> convertMeasurementsToLineEntries(FloatMeasurementView measurementView, List<ScaleMeasurement> measurementsList) {
List<Entry> 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<ILineDataSet> lineDataSets, List<Entry> lineEntries, FloatMeasurementView measurementView) {
if (lineEntries.size() < 2) {
return;
@@ -605,8 +676,8 @@ public class ChartMeasurementView extends LineChart {
}
}
private void addMeasurementLineTrend(List<ILineDataSet> lineDataSets, List<Entry> lineEntries, FloatMeasurementView measurementView) {
LineDataSet measurementLine = new LineDataSet(lineEntries, measurementView.getName().toString() + "-" + getContext().getString(R.string.label_trend_line));
private void addMeasurementLineTrend(List<ILineDataSet> lineDataSets, List<Entry> 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 {
}
}
}
}

View File

@@ -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

View File

@@ -118,4 +118,14 @@
<item>@string/label_time_period_set_reference_day</item>
<item>@string/label_time_period_set_custom_range</item>
</string-array>
<string-array name="trendline_computation_methods_entries">
<item>@string/label_simple_moving_average</item>
<item>@string/label_exponentially_smoothed_moving_average</item>
</string-array>
<string-array name="trendline_computation_methods_values">
<item>SimpleMovingAverage</item>
<item>ExponentiallySmoothedMovingAverage</item>
</string-array>
</resources>

View File

@@ -139,6 +139,10 @@
<string name="Friday">Friday</string>
<string name="Saturday">Saturday</string>
<string name="Sunday">Sunday</string>
<string name="label_simple_moving_average">Simple Moving Average</string>
<string name="label_exponentially_smoothed_moving_average">Exponentially Smoothed Moving Average</string>
<string name="label_trendline_computation_method">Trendline computation method</string>
<string name="label_trendline_future">Future trend</string>
<string name="label_bt_device_no_support">device not supported</string>
<string name="label_exportBackup">Export backup</string>
<string name="label_importBackup">Import backup</string>
@@ -148,6 +152,7 @@
<string name="label_initial_weight">Initial weight</string>
<string name="label_goal_line">Goal line</string>
<string name="label_trend_line">Trendline</string>
<string name="label_simple_moving_average_num_days">Number of days to average over</string>
<string name="label_prediction">Prediction</string>
<string name="label_trend">Trend</string>
<string name="label_help">Help</string>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<CheckBoxPreference
android:defaultValue="true"
android:key="legendEnable"
@@ -24,10 +25,41 @@
android:summaryOff="@string/info_is_not_visible"
android:summaryOn="@string/info_is_visible"
android:title="@string/label_goal_line" />
<CheckBoxPreference
android:defaultValue="false"
android:key="trendLine"
android:summaryOff="@string/info_is_not_enable"
android:summaryOn="@string/info_is_enable"
android:title="@string/label_trend_line" />
<PreferenceCategory
app:key="trendlineCategory"
app:title="@string/label_trend_line">
<CheckBoxPreference
android:defaultValue="false"
android:key="trendLine"
android:summaryOff="@string/info_is_not_enable"
android:summaryOn="@string/info_is_enable"
android:title="@string/label_trend_line" />
<CheckBoxPreference
android:defaultValue="true"
android:key="trendlineFuture"
android:dependency="trendLine"
android:summaryOff="@string/info_is_not_enable"
android:summaryOn="@string/info_is_enable"
android:title="@string/label_trendline_future" />
<DropDownPreference
android:id="@+id/trendlineComputationMethodDropdown"
android:defaultValue="Exponentially Smoothed Moving Average"
android:key="trendlineComputationMethod"
android:dependency="trendLine"
android:entries="@array/trendline_computation_methods_entries"
android:entryValues="@array/trendline_computation_methods_values"
app:useSimpleSummaryProvider="true"
android:title="@string/label_trendline_computation_method"
/>
<SeekBarPreference
android:id="@+id/simpleMovingAverageNumDaysSeekbar"
android:defaultValue="7"
android:key="simpleMovingAverageNumDays"
android:dependency="trendLine"
android:max="30"
app:min="2"
app:showSeekBarValue="true"
android:title="@string/label_simple_moving_average_num_days" />
</PreferenceCategory>
</PreferenceScreen>