Workout details: use combined chart for charts fragment, scatter chart for cadence

This commit is contained in:
a0z
2025-08-28 19:10:07 +02:00
committed by José Rebelo
parent 1bff3eb2e8
commit 9ff56ee0cb
8 changed files with 83 additions and 46 deletions
@@ -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);
@@ -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<ValueFormatter> valueFormatters;
private List<String> 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<ValueFormatter> valueFormatters, List<String> valueUnits) {
public ValueMarker(Context context, CombinedData lineData, List<ValueFormatter> valueFormatters, List<String> 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;
}
@@ -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) }
@@ -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";
@@ -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<LegendEntry> 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<Entry> 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<Entry> 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;
@@ -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<ILineDataSet>()
val combinedData = CombinedData();
val lineData = LineData()
val scatterData = ScatterData()
val lineDataSetsMarkerFormatters = mutableListOf<ValueFormatter?>()
val lineDataSetsMarkerUnits = mutableListOf<String?>()
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<Entry> ?: 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()
}
@@ -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<out IDataSet<out Entry>>,
var chartYLabelFormatter: ValueFormatter? = null,
var unitString: String? = null,
val lineChart: (LineChart) -> Unit = {}
val lineChart: (BarLineChartBase<*>) -> Unit = {}
)
+1 -1
View File
@@ -7,7 +7,7 @@
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.workouts.charts.WorkoutChartsActivity">
<com.github.mikephil.charting.charts.LineChart
<com.github.mikephil.charting.charts.CombinedChart
android:id="@+id/workout_data_chart"
android:layout_width="fill_parent"
android:layout_height="300dp"