/* Copyright (C) 2023 Alicia Hormann This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.activities.charts; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.github.mikephil.charting.animation.Easing; import com.github.mikephil.charting.charts.Chart; import com.github.mikephil.charting.charts.LineChart; import com.github.mikephil.charting.components.XAxis; 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.formatter.ValueFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; public class TemperatureChartFragment extends AbstractChartFragment { protected static final Logger LOG = LoggerFactory.getLogger(TemperatureChartFragment.class); private LineChart mTemperatureChart; private int BACKGROUND_COLOR; private int DESCRIPTION_COLOR; private int CHART_TEXT_COLOR; protected final int TOTAL_DAYS = getRangeDays(); @Override protected void init() { BACKGROUND_COLOR = GBApplication.getBackgroundColor(requireContext()); DESCRIPTION_COLOR = GBApplication.getTextColor(requireContext()); CHART_TEXT_COLOR = GBApplication.getSecondaryTextColor(requireContext()); } private int getRangeDays() { if (GBApplication.getPrefs().getBoolean("charts_range", true)) { return 30; } else { return 7; } } @Override protected TemperatureChartsData refreshInBackground(final ChartsHost chartsHost, final DBHandler db, final GBDevice device) { final List samples = getSamples(db, device); LOG.info("Got {} temperature samples", samples.size()); return new TemperatureChartsDataBuilder(samples).build(); } @Override protected void updateChartsnUIThread(final TemperatureChartsData temperatureData) { mTemperatureChart.setData(null); // workaround for https://github.com/PhilJay/MPAndroidChart/issues/2317 mTemperatureChart.getXAxis().setValueFormatter(temperatureData.getXValueFormatter()); mTemperatureChart.getXAxis().setAvoidFirstLastClipping(true); // Using approximately the range of survivable body-temperatures (in celsius), rounded to multiples of 5 mTemperatureChart.getAxisLeft().setAxisMinimum(30f); mTemperatureChart.getAxisLeft().setAxisMaximum(45f); mTemperatureChart.setData(temperatureData.getData()); } @Override public String getTitle() { return getString(R.string.menuitem_temperature); } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { final View rootView = inflater.inflate(R.layout.fragment_temperaturechart, container, false); mTemperatureChart = rootView.findViewById(R.id.temperature_line_chart); setupLineChart(); // refresh immediately instead of use refreshIfVisible(), for perceived performance refresh(); return rootView; } private void setupLineChart() { mTemperatureChart.setBackgroundColor(BACKGROUND_COLOR); mTemperatureChart.getDescription().setTextColor(DESCRIPTION_COLOR); configureBarLineChartDefaults(mTemperatureChart); final XAxis x = mTemperatureChart.getXAxis(); x.setDrawLabels(true); x.setDrawGridLines(false); x.setEnabled(true); x.setTextColor(CHART_TEXT_COLOR); x.setDrawLimitLinesBehindData(true); final YAxis yAxisLeft = mTemperatureChart.getAxisLeft(); yAxisLeft.setDrawGridLines(true); yAxisLeft.setDrawTopYLabelEntry(false); yAxisLeft.setTextColor(CHART_TEXT_COLOR); yAxisLeft.setEnabled(true); final YAxis yAxisRight = mTemperatureChart.getAxisRight(); yAxisRight.setDrawGridLines(false); yAxisRight.setDrawLabels(false); } @Override protected void setupLegend(final Chart chart) { } @Override protected void renderCharts() { mTemperatureChart.animateX(ANIM_TIME, Easing.EaseInOutQuart); } @Override protected int getTSStart() { return getTSEnd() - TOTAL_DAYS*24*60*60; } private List getSamples(final DBHandler db, final GBDevice device) { final int tsStart = getTSStart(); final int tsEnd = getTSEnd(); final DeviceCoordinator coordinator = device.getDeviceCoordinator(); final TimeSampleProvider sampleProvider = coordinator.getTemperatureSampleProvider(device, db.getDaoSession()); return sampleProvider.getAllSamples(tsStart * 1000L, tsEnd * 1000L); } protected class TemperatureChartsDataBuilder { private final List samples; public TemperatureChartsDataBuilder(final List samples) { this.samples = samples; } public TemperatureChartsData build() { TimestampTranslation tsTranslation = new TimestampTranslation(); List entries = new ArrayList(); long firstTs = 0; for (TemperatureSample sample : samples) { int timestamp_in_seconds = (int) (sample.getTimestamp() / 1000L); entries.add(new Entry(tsTranslation.shorten(timestamp_in_seconds), sample.getTemperature())); if (firstTs == 0) { firstTs = sample.getTimestamp(); } } LineDataSet dataSet = new LineDataSet(entries, getString(R.string.menuitem_temperature)); dataSet.setLineWidth(2.2f); dataSet.setMode(LineDataSet.Mode.HORIZONTAL_BEZIER); dataSet.setCubicIntensity(0.1f); dataSet.setDrawCircles(true); dataSet.setCircleRadius(5f); dataSet.setDrawCircleHole(false); dataSet.setDrawValues(true); dataSet.setValueTextSize(10f); dataSet.setValueTextColor(CHART_TEXT_COLOR); dataSet.setHighlightEnabled(true); dataSet.setValueFormatter(new MyValueFormatter()); LineData lineData = new LineData(dataSet); return new TemperatureChartsData(lineData, tsTranslation); } } protected static class TemperatureChartsData extends DefaultChartsData { public TemperatureChartsData(LineData lineData, TimestampTranslation tsTranslation) { super(lineData, new dateFormatter(tsTranslation)); } } protected static class dateFormatter extends ValueFormatter { private final TimestampTranslation tsTranslation; SimpleDateFormat annotationDateFormat = new SimpleDateFormat("dd.MM."); Calendar cal = GregorianCalendar.getInstance(); public dateFormatter(TimestampTranslation tsTranslation) { this.tsTranslation = tsTranslation; } @Override public String getFormattedValue(float value) { cal.clear(); int ts = (int) value; cal.setTimeInMillis(tsTranslation.toOriginalValue(ts) * 1000L); Date date = cal.getTime(); return annotationDateFormat.format(date); } } protected static class MyValueFormatter extends ValueFormatter { private final DecimalFormat formatter = new DecimalFormat("0.00"); @Override public String getPointLabel(Entry entry) { return formatter.format(entry.getY()); } } }