Workout details: add additional charts

This commit is contained in:
a0z
2025-08-18 00:01:00 +01:00
committed by José Rebelo
parent 98e7aa2c39
commit 2d56002b12
6 changed files with 199 additions and 18 deletions
@@ -0,0 +1,31 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import android.annotation.SuppressLint;
import com.github.mikephil.charting.formatter.ValueFormatter;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
public class DurationXLabelFormatter extends ValueFormatter {
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat annotationDateFormat;
private final Calendar cal = GregorianCalendar.getInstance();
public DurationXLabelFormatter(String simpleDateFormatPattern) {
this.annotationDateFormat = new SimpleDateFormat(simpleDateFormatPattern);
}
// TODO: this does not work. Cannot use precomputed labels
@Override
public String getFormattedValue(final float value) {
cal.clear();
final int ts = (int) value;
cal.setTimeInMillis(ts);
final Date date = cal.getTime();
return annotationDateFormat.format(date);
}
}
@@ -0,0 +1,25 @@
package nodomain.freeyourgadget.gadgetbridge.activities.charts;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
import com.github.mikephil.charting.formatter.ValueFormatter;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter;
public class SpeedYLabelFormatter extends ValueFormatter {
String unit;
private final WorkoutValueFormatter workoutValueFormatter = new WorkoutValueFormatter();
public SpeedYLabelFormatter(String unit) {
this.unit = unit;
}
@Override
public String getFormattedValue(float value) {
if (unit.equals(UNIT_SECONDS_PER_KM)) {
value = value > 0 ? Math.round((60 / (value * 3.6)) * 60) : 0;
}
return workoutValueFormatter.formatValue(value, unit, false);
}
}
@@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities.workouts
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Typeface
import android.os.Bundle
import android.util.TypedValue
@@ -44,12 +45,17 @@ import androidx.fragment.app.Fragment
import androidx.gridlayout.widget.GridLayout
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import com.github.mikephil.charting.charts.LineChart
import com.github.mikephil.charting.components.XAxis
import com.github.mikephil.charting.data.LineData
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nodomain.freeyourgadget.gadgetbridge.GBApplication
import nodomain.freeyourgadget.gadgetbridge.R
import nodomain.freeyourgadget.gadgetbridge.activities.ActivitySummariesChartFragment
import nodomain.freeyourgadget.gadgetbridge.activities.charts.DurationXLabelFormatter
import nodomain.freeyourgadget.gadgetbridge.activities.fit.FitViewerActivity
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryEntry
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryGroup
@@ -393,7 +399,44 @@ class WorkoutDetailsFragment : Fragment(), MenuProvider {
)
}
// TODO add chartData
val chartTextColor = GBApplication.getSecondaryTextColor(context);
val lineChart = LineChart(requireContext()).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
legend.textColor = GBApplication.getTextColor(context)
isScaleXEnabled = false
isScaleYEnabled = false
isHighlightPerDragEnabled = false
isHighlightPerTapEnabled = false
isDragEnabled = false
}
lineChart.xAxis.apply {
setDrawLabels(true)
setDrawGridLines(false)
setDrawLimitLinesBehindData(true)
isEnabled = true
textColor = chartTextColor
position = XAxis.XAxisPosition.BOTTOM
valueFormatter = DurationXLabelFormatter("mm:ss")
}
lineChart.axisLeft.apply {
setDrawGridLines(false)
setDrawTopYLabelEntry(true)
textColor = chartTextColor
isEnabled = true
if (chart.chartYLabelFormatter != null) {
valueFormatter = chart.chartYLabelFormatter;
}
}
lineChart.axisRight.apply {
isEnabled = false
}
lineChart.data = chart.chartData as LineData?
lineChart.invalidate()
chartsFragmentHolder.addView(lineChart)
binding.dynamicCharts.addView(chartsFragmentHolder)
}
@@ -46,7 +46,7 @@ public class WorkoutValueFormatter {
this.show_raw_data = !show_raw_data;
}
public String formatValue(final Object rawValue, String unit) {
public String formatValue(final Object rawValue, String unit, boolean showUnit) {
if (rawValue == null) {
return GBApplication.getContext().getString(R.string.stats_empty_value);
}
@@ -172,21 +172,27 @@ public class WorkoutValueFormatter {
}
}
if (unit.equals("seconds") && !show_raw_data) { //rather then plain seconds, show formatted duration
if (unit.equals("seconds") && !show_raw_data && showUnit) { //rather then plain seconds, show formatted duration
return DateTimeUtils.formatDurationHoursMinutes((long) value, TimeUnit.SECONDS);
} else if (unit.equals("minutes_km") || unit.equals("minutes_mi") || unit.equals("minutes_100m") || unit.equals("minutes_100yd")) {
// Format pace
String format = showUnit ? "%d:%02d %s" : "%d:%02d";
return String.format(
Locale.getDefault(),
"%d:%02d %s",
format,
(int) Math.floor(value), (int) Math.round(60 * (value - (int) Math.floor(value))),
getStringResourceByName(unit)
);
} else {
return String.format("%s %s", df.format(value), getStringResourceByName(unit));
String format = showUnit ? "%s %s" : "%s";
return String.format(format, df.format(value), getStringResourceByName(unit));
}
}
public String formatValue(final Object rawValue, String unit) {
return formatValue(rawValue, unit, true);
}
public String getStringResourceByName(String aString) {
String packageName = BuildConfig.APPLICATION_ID;
int resId = GBApplication.getContext().getResources().getIdentifier(aString, "string", packageName);
@@ -86,6 +86,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KMPH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_LB;
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_MILLISECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_ML_KG_MIN;
@@ -94,10 +95,17 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_PERCENTAGE;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_RPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS_PER_KM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STROKES_PER_LENGTH;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_WATT;
import android.content.Context;
import android.graphics.Color;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -111,7 +119,10 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.SpeedYLabelFormatter;
import nodomain.freeyourgadget.gadgetbridge.activities.charts.TimestampTranslation;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableBuilder;
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
@@ -157,6 +168,10 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
private final List<FitSet> sets = new ArrayList<>();
private final List<FitLap> laps = new ArrayList<>();
TimestampTranslation tsTranslation = new TimestampTranslation();
ActivityKind activityKind;
public GarminWorkoutParser(final Context context) {
this.context = context;
}
@@ -201,11 +216,56 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
handleRecord(record);
}
if (sport != null) {
if (StringUtils.isNullOrEmpty(summary.getName())) {
summary.setName(sport.getName());
}
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
} else {
activityKind = getActivityKind(session.getSport(), session.getSubSport());
}
final ActivitySummaryData activitySummaryData = updateSummary(summary);
final List<WorkoutChart> charts = new LinkedList<>();
// TODO charts
if (!this.activityPoints.isEmpty()) {
final List<Entry> heartRateDataPoints = new ArrayList<>();
final List<Entry> speedDataPoints = new ArrayList<>();
final List<Entry> elevationDataPoints = new ArrayList<>();
boolean hasSpeedValues = false;
for (int i=0; i<=activityPoints.size()-1; i++) {
ActivityPoint point = activityPoints.get(i);
long time = tsTranslation.shorten((int) point.getTime().getTime());
heartRateDataPoints.add(new Entry(time, point.getHeartRate()));
speedDataPoints.add(new Entry(time, point.getSpeed()));
if (!hasSpeedValues && point.getSpeed() > 0) {
hasSpeedValues = true;
}
if (point.getLocation() != null) {
elevationDataPoints.add(new Entry(time, (float) point.getLocation().getAltitude()));
}
}
if (!heartRateDataPoints.isEmpty()) {
String label = String.format("%s(%s)", context.getString(R.string.heart_rate), getUnitString(UNIT_BPM));
LineDataSet dataset = createDataSet(heartRateDataPoints, label, Color.RED);
charts.add(new WorkoutChart(context.getString(R.string.heart_rate), new LineData(dataset)));
}
if (hasSpeedValues && !speedDataPoints.isEmpty()) {
if (ActivityKind.isPaceActivity(activityKind)) {
String label = String.format("%s(%s)", context.getString(R.string.Pace), getUnitString(UNIT_SECONDS_PER_KM));
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Pace), new LineData(dataset), new SpeedYLabelFormatter(UNIT_SECONDS_PER_KM)));
} else {
String label = String.format("%s(%s)", context.getString(R.string.Speed), getUnitString(UNIT_METERS_PER_SECOND));
LineDataSet dataset = createDataSet(speedDataPoints, label, Color.BLUE);
charts.add(new WorkoutChart(context.getString(R.string.Speed), new LineData(dataset), new SpeedYLabelFormatter(UNIT_METERS_PER_SECOND)));
}
}
if (!elevationDataPoints.isEmpty()) {
String label = String.format("%s(%s)", context.getString(R.string.Elevation), getUnitString(UNIT_METERS));
LineDataSet dataset = createDataSet(elevationDataPoints, label, Color.GREEN);
charts.add(new WorkoutChart(context.getString(R.string.Elevation), new LineData(dataset)));
}
}
final long nanoEnd = System.nanoTime();
final long executionTime = (nanoEnd - nanoStart) / 1000000;
@@ -218,6 +278,29 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
);
}
public String getUnitString(String unit) {
int resId = context.getResources().getIdentifier(unit, "string", context.getPackageName());
if (resId != 0) {
return context.getString(resId);
}
return "";
}
public LineDataSet createDataSet(List<Entry> entities, String label, int color) {
LineDataSet dataSet = new LineDataSet(entities, label);
dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER);
dataSet.setCubicIntensity(0.05f);
dataSet.setDrawCircles(false);
dataSet.setAxisDependency(YAxis.AxisDependency.RIGHT);
dataSet.setColor(color);
dataSet.setValueTextColor(GBApplication.getSecondaryTextColor(context));
dataSet.setLineWidth(1.5f);
dataSet.setHighlightLineWidth(2f);
dataSet.setDrawValues(false);
dataSet.setDrawHorizontalHighlightIndicator(false);
return dataSet;
}
public void reset() {
timesInZone.clear();
activityPoints.clear();
@@ -294,15 +377,6 @@ public class GarminWorkoutParser implements ActivitySummaryParser {
return summaryData;
}
final ActivityKind activityKind;
if (sport != null) {
if (StringUtils.isNullOrEmpty(summary.getName())) {
summary.setName(sport.getName());
}
activityKind = getActivityKind(sport.getSport(), sport.getSubSport());
} else {
activityKind = getActivityKind(session.getSport(), session.getSubSport());
}
final ActivityKind.CycleUnit cycleUnit = ActivityKind.getCycleUnit(activityKind);
final String weightUnit;
@@ -1,8 +1,10 @@
package nodomain.freeyourgadget.gadgetbridge.model.workout
import com.github.mikephil.charting.data.ChartData
import com.github.mikephil.charting.formatter.ValueFormatter
data class WorkoutChart(
data class WorkoutChart @JvmOverloads constructor(
val title: String,
val chartData: ChartData<*>
val chartData: ChartData<*>,
var chartYLabelFormatter: ValueFormatter? = null,
)