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:
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user