diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SpeedYLabelFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SpeedYLabelFormatter.java index 6d3d3cd438..87ad7dae86 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SpeedYLabelFormatter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/SpeedYLabelFormatter.java @@ -1,6 +1,6 @@ package nodomain.freeyourgadget.gadgetbridge.activities.charts; -import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_KM; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM; import com.github.mikephil.charting.formatter.ValueFormatter; @@ -17,7 +17,7 @@ public class SpeedYLabelFormatter extends ValueFormatter { @Override public String getFormattedValue(float value) { - if (unit.equals(UNIT_MINUTES_PER_KM)) { + if (unit.equals(UNIT_SECONDS_PER_KM)) { value = value > 0 ? Math.round((60 / (value * 3.6)) * 60) : 0; } return workoutValueFormatter.formatValue(value, unit, false); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/marker/ValueMarker.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/marker/ValueMarker.java index c6710479aa..5194460bdc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/marker/ValueMarker.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/marker/ValueMarker.java @@ -5,11 +5,11 @@ import android.graphics.Canvas; import android.widget.TextView; import com.github.mikephil.charting.components.MarkerView; +import com.github.mikephil.charting.data.CombinedData; import com.github.mikephil.charting.data.Entry; -import com.github.mikephil.charting.data.LineData; import com.github.mikephil.charting.formatter.ValueFormatter; import com.github.mikephil.charting.highlight.Highlight; -import com.github.mikephil.charting.interfaces.datasets.ILineDataSet; +import com.github.mikephil.charting.interfaces.datasets.IBarLineScatterCandleBubbleDataSet; import com.github.mikephil.charting.utils.MPPointF; import java.util.List; @@ -20,14 +20,14 @@ public class ValueMarker extends MarkerView { private TextView markerContent; private List valueFormatters; private List valueUnits; - private LineData lineData; + private CombinedData lineData; public ValueMarker(Context context) { super(context, R.layout.value_marker); this.markerContent = findViewById(R.id.marker_content); } - public ValueMarker(Context context, LineData lineData, List valueFormatters, List valueUnits) { + public ValueMarker(Context context, CombinedData lineData, List valueFormatters, List valueUnits) { super(context, R.layout.value_marker); this.markerContent = findViewById(R.id.marker_content); this.valueFormatters = valueFormatters; @@ -40,7 +40,7 @@ public class ValueMarker extends MarkerView { float xVal = e.getX(); StringBuilder content = new StringBuilder(); for (int i = 0; i < lineData.getDataSetCount(); i++) { - ILineDataSet dataSet = lineData.getDataSetByIndex(i); + IBarLineScatterCandleBubbleDataSet dataSet = lineData.getDataSetByIndex(i); if (dataSet == null || !dataSet.isVisible()) { continue; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt index edcbc65549..dff72fc804 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutDetailsFragment.kt @@ -45,9 +45,12 @@ import androidx.fragment.app.Fragment import androidx.gridlayout.widget.GridLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import com.github.mikephil.charting.charts.BarLineChartBase import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.charts.ScatterChart import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.ScatterData import com.github.mikephil.charting.listener.ChartTouchListener import com.github.mikephil.charting.listener.OnChartGestureListener import kotlinx.coroutines.Dispatchers @@ -67,6 +70,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.Activity import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummarySimpleEntry import nodomain.freeyourgadget.gadgetbridge.databinding.FragmentWorkoutDetailsBinding import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary +import nodomain.freeyourgadget.gadgetbridge.entities.Device import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData @@ -434,7 +438,10 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider { } val chartTextColor = GBApplication.getSecondaryTextColor(context); - val lineChart = LineChart(requireContext()).apply { + val lineChart: BarLineChartBase<*> = when (chart.chartData) { + is ScatterData -> ScatterChart(requireContext()) + else -> LineChart(requireContext()) + }.apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT @@ -468,7 +475,14 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider { isEnabled = false } chart.lineChart(lineChart); - lineChart.data = chart.chartData as LineData? + when { + lineChart is LineChart && chart.chartData is LineData -> { + lineChart.data = chart.chartData + } + lineChart is ScatterChart && chart.chartData is ScatterData -> { + lineChart.data = chart.chartData + } + } lineChart.description.isEnabled = false; lineChart.onChartGestureListener = object : OnChartGestureListener { override fun onChartLongPressed(me: MotionEvent?) {} @@ -809,7 +823,7 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider { } } - private fun getGBDevice(device: nodomain.freeyourgadget.gadgetbridge.entities.Device?): GBDevice? { + private fun getGBDevice(device: Device?): GBDevice? { return device?.let { findDevice -> GBApplication.app().deviceManager.devices .firstOrNull { it.address.equals(findDevice.identifier, ignoreCase = true) } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutValueFormatter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutValueFormatter.java index 91f1ef2121..c7609f5fab 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutValueFormatter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/WorkoutValueFormatter.java @@ -123,7 +123,6 @@ public class WorkoutValueFormatter { } break; case UNIT_SECONDS_PER_KM: - case UNIT_MINUTES_PER_KM: if (units.equals(UNIT_IMPERIAL)) { value = value / 60D * 1.609344; unit = "minutes_mi"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java index cbe7726978..4ba9aea705 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/DefaultWorkoutCharts.java @@ -1,19 +1,22 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_MINUTES_PER_KM; +import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM; import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SPM; import android.content.Context; -import android.graphics.Color; -import com.github.mikephil.charting.components.LegendEntry; +import com.github.mikephil.charting.charts.ScatterChart; import com.github.mikephil.charting.components.YAxis; 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.data.ScatterData; +import com.github.mikephil.charting.data.ScatterDataSet; import com.github.mikephil.charting.formatter.ValueFormatter; import java.util.ArrayList; @@ -68,7 +71,7 @@ public class DefaultWorkoutCharts { if (!heartRateDataPoints.isEmpty()) { final String label = String.format("%s(%s)", context.getString(R.string.heart_rate), getUnitString(context, UNIT_BPM)); - final LineDataSet dataset = createDataSet(context, heartRateDataPoints, label, context.getResources().getColor(R.color.chart_line_heart_rate)); + final LineDataSet dataset = createLineDataSet(context, heartRateDataPoints, label, context.getResources().getColor(R.color.chart_line_heart_rate)); ValueFormatter integerFormatter = new ValueFormatter() { @Override public String getFormattedValue(float value) { @@ -81,24 +84,18 @@ public class DefaultWorkoutCharts { if (hasSpeedValues && !speedDataPoints.isEmpty()) { if (ActivityKind.isPaceActivity(activityKind)) { final String label = String.format("%s (%s)", context.getString(R.string.Pace), getUnitString(context, UNIT_MINUTES_PER_KM)); - final LineDataSet dataset = createDataSet(context, speedDataPoints, label, context.getResources().getColor(R.color.chart_line_speed)); - charts.add(new WorkoutChart("pace", context.getString(R.string.Pace), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_MINUTES_PER_KM), getUnitString(context, UNIT_MINUTES_PER_KM))); + final LineDataSet dataset = createLineDataSet(context, speedDataPoints, label, context.getResources().getColor(R.color.chart_line_speed)); + charts.add(new WorkoutChart("pace", context.getString(R.string.Pace), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_SECONDS_PER_KM), getUnitString(context, UNIT_MINUTES_PER_KM))); } else { - final String label = String.format("%s (%s)", context.getString(R.string.Speed), getUnitString(context, UNIT_METERS_PER_SECOND)); - final LineDataSet dataset = createDataSet(context, speedDataPoints, label, context.getResources().getColor(R.color.chart_line_speed)); - charts.add(new WorkoutChart("speed", context.getString(R.string.Speed), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND), getUnitString(context, UNIT_METERS_PER_SECOND))); + final String label = String.format("%s (%s)", context.getString(R.string.Speed), getUnitString(context, UNIT_KMPH)); + final LineDataSet dataset = createLineDataSet(context, speedDataPoints, label, context.getResources().getColor(R.color.chart_line_speed)); + charts.add(new WorkoutChart("speed", context.getString(R.string.Speed), ActivitySummaryEntries.GROUP_SPEED, new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND), getUnitString(context, UNIT_KMPH))); } } if (hasCadenceValues && !cadenceDataPoints.isEmpty()) { final String label = String.format("%s (%s)", context.getString(R.string.workout_cadence), getUnitString(context, getCadenceUnit(cycleUnit))); - final LineDataSet dataset = createDataSet(context, cadenceDataPoints, label, context.getResources().getColor(R.color.transparent)); - dataset.setDrawCircles(true); - dataset.setDrawCircleHole(false); - dataset.setLineWidth(-1); - dataset.setCircleRadius(3f); - dataset.setMode(LineDataSet.Mode.CUBIC_BEZIER); - dataset.setCircleColor(context.getResources().getColor(R.color.chart_cadence_circle)); + final ScatterDataSet dataset = createScatterDataSet(context, cadenceDataPoints, label, context.getResources().getColor(R.color.chart_cadence_circle)); final ValueFormatter integerFormatter = new ValueFormatter() { @Override public String getFormattedValue(float value) { @@ -110,7 +107,7 @@ public class DefaultWorkoutCharts { "cadence", context.getString(R.string.workout_cadence), ActivitySummaryEntries.GROUP_CADENCE, - new LineData(dataset), + new ScatterData(dataset), integerFormatter, getUnitString(context, UNIT_SPM), lineChart -> { @@ -120,12 +117,6 @@ public class DefaultWorkoutCharts { YAxis yAxisRight = lineChart.getAxisRight(); yAxisRight.setAxisMinimum(0); yAxisRight.setAxisMaximum(xAxisMaximum); - List legendEntries = new ArrayList<>(1); - LegendEntry hrEntry = new LegendEntry(); - hrEntry.label = label; - hrEntry.formColor = context.getResources().getColor(R.color.chart_cadence_circle); - legendEntries.add(hrEntry); - lineChart.getLegend().setCustom(legendEntries); return kotlin.Unit.INSTANCE; } ) @@ -134,7 +125,7 @@ public class DefaultWorkoutCharts { if (!elevationDataPoints.isEmpty()) { final String label = String.format("%s (%s)", context.getString(R.string.Elevation), getUnitString(context, UNIT_METERS)); - LineDataSet dataset = createDataSet(context, elevationDataPoints, label, context.getResources().getColor(R.color.chart_line_elevation)); + LineDataSet dataset = createLineDataSet(context, elevationDataPoints, label, context.getResources().getColor(R.color.chart_line_elevation)); charts.add(new WorkoutChart("elevation", context.getString(R.string.Elevation), ActivitySummaryEntries.GROUP_ELEVATION, new LineData(dataset), null, getUnitString(context, UNIT_METERS))); } @@ -149,7 +140,7 @@ public class DefaultWorkoutCharts { return ""; } - public static LineDataSet createDataSet(final Context context, + public static LineDataSet createLineDataSet(final Context context, final List entities, final String label, final int color) { @@ -167,6 +158,22 @@ public class DefaultWorkoutCharts { return dataSet; } + public static ScatterDataSet createScatterDataSet(final Context context, + final List entities, + final String label, + final int color) { + final ScatterDataSet dataSet = new ScatterDataSet(entities, label); + dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT); + dataSet.setColor(color); + dataSet.setValueTextColor(GBApplication.getSecondaryTextColor(context)); + dataSet.setHighlightLineWidth(2f); + dataSet.setDrawValues(false); + dataSet.setDrawHorizontalHighlightIndicator(false); + dataSet.setScatterShape(ScatterChart.ScatterShape.CIRCLE); + dataSet.setScatterShapeSize(10f); + return dataSet; + } + public static String getCadenceUnit(final ActivityKind.CycleUnit unit) { return switch (unit) { case STROKES -> ActivitySummaryEntries.UNIT_STROKES_PER_MINUTE; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/WorkoutChartsActivity.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/WorkoutChartsActivity.kt index 1da87d5196..049c7af8f9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/WorkoutChartsActivity.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/workouts/charts/WorkoutChartsActivity.kt @@ -12,11 +12,15 @@ import androidx.core.view.MenuProvider import androidx.core.view.children import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.components.YAxis +import com.github.mikephil.charting.data.CombinedData +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.data.LineScatterCandleRadarDataSet +import com.github.mikephil.charting.data.ScatterData +import com.github.mikephil.charting.data.ScatterDataSet import com.github.mikephil.charting.formatter.DefaultAxisValueFormatter import com.github.mikephil.charting.formatter.ValueFormatter -import com.github.mikephil.charting.interfaces.datasets.ILineDataSet import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -126,17 +130,27 @@ class WorkoutChartsActivity : AbstractGBActivity(), MenuProvider { } fun refreshChart() { - val lineDataSets = mutableListOf() + val combinedData = CombinedData(); + val lineData = LineData() + val scatterData = ScatterData() val lineDataSetsMarkerFormatters = mutableListOf() val lineDataSetsMarkerUnits = mutableListOf() var leftY = true selectedCharts.forEach { selectedChart -> val workoutChart = chartData?.find { it.id == selectedChart } ?: return@forEach - val dataSet = workoutChart.chartData.getDataSetByIndex(0) as? LineDataSet ?: return@forEach + val dataSet = workoutChart.chartData.getDataSetByIndex(0) as? LineScatterCandleRadarDataSet ?: return@forEach dataSet.highLightColor = context.getColor(R.color.chart_highline_dolor); dataSet.highlightLineWidth = 1f; dataSet.axisDependency = if(leftY) YAxis.AxisDependency.LEFT else YAxis.AxisDependency.RIGHT; - lineDataSets.add(dataSet) + when (dataSet) { + is LineDataSet -> { + lineData.addDataSet(dataSet) + } + is ScatterDataSet -> { + scatterData.addDataSet(dataSet) + } + else -> {} + } lineDataSetsMarkerFormatters.add(workoutChart.chartYLabelFormatter); lineDataSetsMarkerUnits.add(workoutChart.unitString); val axis = if (leftY) binding.workoutDataChart.axisLeft else binding.workoutDataChart.axisRight @@ -148,9 +162,10 @@ class WorkoutChartsActivity : AbstractGBActivity(), MenuProvider { val workoutChart = chartData?.find { it.id == selectedChartId } ?: return binding.workoutDataChart.axisRight.valueFormatter = workoutChart?.chartYLabelFormatter ?: DefaultAxisValueFormatter(0) } - val lineData = LineData(lineDataSets) - binding.workoutDataChart.data = lineData; - binding.workoutDataChart.marker = ValueMarker(this, lineData, lineDataSetsMarkerFormatters, lineDataSetsMarkerUnits); + combinedData.setData(lineData) + combinedData.setData(scatterData) + binding.workoutDataChart.data = combinedData; + binding.workoutDataChart.marker = ValueMarker(this, combinedData, lineDataSetsMarkerFormatters, lineDataSetsMarkerUnits); binding.workoutDataChart.highlightValues(null) binding.workoutDataChart.invalidate() } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/workout/WorkoutChart.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/workout/WorkoutChart.kt index ab535808a1..ea0f708599 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/workout/WorkoutChart.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/workout/WorkoutChart.kt @@ -1,15 +1,17 @@ package nodomain.freeyourgadget.gadgetbridge.model.workout -import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.charts.BarLineChartBase import com.github.mikephil.charting.data.ChartData +import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.formatter.ValueFormatter +import com.github.mikephil.charting.interfaces.datasets.IDataSet data class WorkoutChart @JvmOverloads constructor( val id: String, val title: String, val group: String, - val chartData: ChartData<*>, + val chartData: ChartData>, var chartYLabelFormatter: ValueFormatter? = null, var unitString: String? = null, - val lineChart: (LineChart) -> Unit = {} + val lineChart: (BarLineChartBase<*>) -> Unit = {} ) diff --git a/app/src/main/res/layout/workout_charts.xml b/app/src/main/res/layout/workout_charts.xml index 8c49683a9e..9fd18a3d4e 100644 --- a/app/src/main/res/layout/workout_charts.xml +++ b/app/src/main/res/layout/workout_charts.xml @@ -7,7 +7,7 @@ tools:context="nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.WorkoutChartsActivity"> -