From a8305b8fc994b7f65e67525fa1530640e59a77a8 Mon Sep 17 00:00:00 2001 From: Moritz Warning Date: Thu, 12 Feb 2026 23:06:23 +0100 Subject: [PATCH] statistics: complete removal --- app/build.gradle.kts | 1 - app/src/main/AndroidManifest.xml | 4 - .../lunatracker/MainActivity.kt | 12 - .../lunatracker/StatisticsActivity.kt | 947 ------------------ .../main/res/layout/activity_statistics.xml | 50 - app/src/main/res/layout/more_events_popup.xml | 12 +- app/src/main/res/values/arrays.xml | 28 - app/src/main/res/values/strings.xml | 10 - gradle/libs.versions.toml | 4 - 9 files changed, 1 insertion(+), 1067 deletions(-) delete mode 100644 app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt delete mode 100644 app/src/main/res/layout/activity_statistics.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 222fc51..b55793c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,5 +65,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) - implementation(libs.mpandroidchart.vv310) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b335ff9..97f621b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -30,10 +30,6 @@ android:name=".SettingsActivity" android:label="@string/settings_title" android:theme="@style/Theme.LunaTracker"/> - \ No newline at end of file diff --git a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt index 461778b..4a0ba30 100644 --- a/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt +++ b/app/src/main/java/it/danieleverducci/lunatracker/MainActivity.kt @@ -1284,18 +1284,6 @@ class MainActivity : AppCompatActivity() { val inflater = LayoutInflater.from(anchor.context) contentView = inflater.inflate(R.layout.more_events_popup, null) - // Add statistics (hard coded) - contentView.findViewById(R.id.button_statistics).setOnClickListener { - if (logbook != null && !pauseLogbookUpdate) { - val i = Intent(applicationContext, StatisticsActivity::class.java) - i.putExtra("LOOGBOOK_NAME", logbook!!.name) - startActivity(i) - } else { - Toast.makeText(applicationContext, "No logbook selected!", Toast.LENGTH_SHORT).show() - } - dismiss() - } - val linearLayout = contentView.findViewById(R.id.layout_list) // Add buttons to create other events diff --git a/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt b/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt deleted file mode 100644 index 9452662..0000000 --- a/app/src/main/java/it/danieleverducci/lunatracker/StatisticsActivity.kt +++ /dev/null @@ -1,947 +0,0 @@ -package it.danieleverducci.lunatracker - -import android.graphics.Canvas -import android.graphics.Color -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.Spinner -import android.widget.TextView -import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.toColorInt -import com.github.mikephil.charting.animation.ChartAnimator -import com.github.mikephil.charting.charts.BarChart -import com.github.mikephil.charting.data.BarData -import com.github.mikephil.charting.data.BarDataSet -import com.github.mikephil.charting.data.BarEntry -import com.github.mikephil.charting.data.Entry -import com.github.mikephil.charting.formatter.ValueFormatter -import com.github.mikephil.charting.highlight.Highlight -import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider -import com.github.mikephil.charting.listener.OnChartValueSelectedListener -import com.github.mikephil.charting.renderer.HorizontalBarChartRenderer -import com.github.mikephil.charting.utils.ViewPortHandler -import it.danieleverducci.lunatracker.entities.LunaEvent -import utils.DateUtils -import utils.NumericUtils -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale -import kotlin.math.max -import kotlin.math.min - -class StatisticsActivity : AppCompatActivity() { - var lastToastShown = 0L - - lateinit var barChart: BarChart - lateinit var noDataTextView: TextView - lateinit var graphTypeSpinner: Spinner - lateinit var timeRangeSpinner: Spinner - - lateinit var unixToSpan: (Long) -> Int - lateinit var spanToUnix: (Int) -> Long - - enum class GraphType { - BOTTLE_EVENTS, - BOTTLE_SUM, - SLEEP_SUM, - SLEEP_EVENTS, - SLEEP_PATTERN - } - - enum class TimeRange { - DAY, - WEEK, - MONTH - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.activity_statistics) - - val logbookName = intent.getStringExtra("LOOGBOOK_NAME") - if (logbookName == null) { - finish() - return - } - - noDataTextView = findViewById(R.id.no_data) - - barChart = findViewById(R.id.bar_chart) - barChart.setBackgroundColor(Color.WHITE) - barChart.description.text = logbookName - barChart.setDrawValueAboveBar(false) - - barChart.axisLeft.setAxisMinimum(0F) - barChart.axisLeft.setDrawGridLines(false) - barChart.axisLeft.setDrawLabels(false) - - barChart.axisRight.setDrawGridLines(false) - barChart.axisRight.setDrawLabels(false) - - barChart.xAxis.setDrawLabels(true) - barChart.xAxis.setDrawAxisLine(false) - - barChart.isScaleXEnabled = false - barChart.isScaleYEnabled = true - - graphTypeSpinner = findViewById(R.id.graph_type_selection) - timeRangeSpinner = findViewById(R.id.time_range_selection) - - setupSpinner(graphTypeSelection.name, - R.id.graph_type_selection, - R.array.StatisticsTypeLabels, - R.array.StatisticsTypeValues, - object : SpinnerItemSelected { - override fun call(newValue: String?) { - //Log.d("event", "new value: $newValue") - newValue ?: return - graphTypeSelection = GraphType.valueOf(newValue) - showGraph() - } - } - ) - - setupSpinner(timeRangeSelection.name, - R.id.time_range_selection, - R.array.StatisticsTimeLabels, - R.array.StatisticsTimeValues, - object : SpinnerItemSelected { - override fun call(newValue: String?) { - //Log.d("event", "new value: $newValue") - newValue ?: return - timeRangeSelection = TimeRange.valueOf(newValue) - setSpans() - showGraph() - } - } - ) - - setSpans() - showGraph() - } - - fun setSpans() { - unixToSpan = when (timeRangeSelection) { - TimeRange.DAY -> { unix: Long -> unixToDays(unix) } - TimeRange.WEEK -> { unix: Long -> unixToWeeks(unix) } - TimeRange.MONTH -> { unix: Long -> unixToMonths(unix) } - } - - spanToUnix = when (timeRangeSelection) { - TimeRange.DAY -> { span: Int -> daysToUnix(span) } - TimeRange.WEEK -> { span: Int -> weeksToUnix(span) } - TimeRange.MONTH -> { span: Int -> monthsToUnix(span) } - } - } - - data class SleepRange(val start: Long, var end: Long) - - fun toSleepRanges(events: List): ArrayList { - val ranges = arrayListOf() - val now = System.currentTimeMillis() / 1000 - - // Transform events into time ranges. - // Merge overlapping times and extend - // ongoing sleep events until now. - var warningShown = false - for (event in events) { - val startTime = event.time - val endTime = if (event.quantity == 0) { - now - } else { - event.time + event.quantity - } - - // handle overlap - val previousRange = ranges.lastOrNull() - if (previousRange != null && previousRange.end > startTime) { - // cap previous range to avoid overlap - previousRange.end = startTime - if (!warningShown) { - Toast.makeText(applicationContext, "Overlapping sleep event at ${DateUtils.formatDateTime(startTime)}", Toast.LENGTH_SHORT).show() - warningShown = true - } - } - - ranges.add(SleepRange(startTime, endTime)) - } - - return ranges - } - - fun showSleepPatternBarGraphSlotted(state: GraphState) { - val ranges = toSleepRanges(state.events) - val values = ArrayList() - val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) }) - - Log.d(TAG, "stack.size: ${stack.size}, array.size: ${stack[0].size}, dayCounter.daysWithData.size: ${state.dayCounter.daysWithData.size}") - - fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) { - val beginDays = unixToDays(begin) - val endDays = unixToDays(end) - var mid = begin - - //Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}") - for (i in beginDays..endDays) { - // i is the days/weeks/months since unix epoch - val dayBegin = daysToUnix(i) - val dayEnd = daysToUnix(i + 1) - val sleepBegin = max(mid, dayBegin) - val sleepEnd = min(end, dayEnd) - - if (sleepBegin != sleepEnd) { - assert(dayBegin <= dayEnd) - assert(sleepBegin <= sleepEnd) - val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY - val iEnd = iBegin + (sleepEnd - sleepBegin) / SLEEP_PATTERN_GRANULARITY - //Log.d(TAG, "index: $index, iBegin: $iBegin, iEnd: $iEnd, dayBegin: ${Date(dayBegin * 1000)}, dayEnd: ${Date(dayEnd * 1000)}, sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}") - for (j in iBegin.. 0) - assert(occurrences <= maxOccurrences) - - // map to color - val q = occurrences.toFloat() / maxOccurrences.toFloat() - val i = q * (SLEEP_PATTERN_COLORS.size - 1).toFloat() - return SLEEP_PATTERN_COLORS[i.toInt()] - } - - val allColors = ArrayList() - - // convert array of time slots that represent a day to value and color arrays used by chart library - for ((index, dayArray) in stack.withIndex()) { - val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) - - //Log.d(TAG, "index: $index: daysWithData: $daysWithData, dayArray: ${dayArray.joinToString { it.toString() }}") - val vals = ArrayList() - - var prevIndex = -1 // time slot index - var prevValue = -1 // number of entries we have found for time slot - for ((i, v) in dayArray.withIndex()) { - if (i == 0) { - prevIndex = i - prevValue = v - } else if (prevValue != v) { - vals.add((i - prevIndex).toFloat()) - allColors.add(mapColor(prevValue.coerceAtMost(daysWithData), daysWithData)) - prevIndex = i - prevValue = v - } - } - - if (prevIndex != -1) { - vals.add((dayArray.size - prevIndex).toFloat()) - allColors.add(mapColor(prevValue, daysWithData)) - } - - //Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}") - - //Log.d(TAG, "index: ${index.toFloat()}") - values.add(BarEntry(index.toFloat(), vals.toFloatArray())) - } - - //Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}") - - barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onValueSelected(e: Entry?, h: Highlight?) { - if (e == null || h == null) { - return - } - - val index = e.x.toInt() - if (index !in 0..values.size) { - return - } - - val value = values[index] - if (value.yVals == null || h.stackIndex !in 0..value.yVals.size) { - return - } - - if ((lastToastShown + TOAST_FREQUENCY_MS) > System.currentTimeMillis()) { - // only show one Toast message after another - return - } - - val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index) - - //Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}") - val startSeconds = - SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0.. acc + y.toInt() } - val durationSeconds = - SLEEP_PATTERN_GRANULARITY * value.yVals[h.stackIndex].toInt() - val endSeconds = startSeconds + durationSeconds - - val format = SimpleDateFormat("HH:mm", Locale.getDefault()) - val startTimeString = - format.format((dayStartUnix + startSeconds) * 1000).toString() - val endTimeString = - format.format((dayStartUnix + endSeconds) * 1000).toString() - val durationString = DateUtils.formatTimeDuration(applicationContext, durationSeconds.toLong()) - - val daysWithData = - stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY] - val daysWithDataMax = state.dayCounter.countDaysWithData( - spanToUnix(state.startSpan + index), - spanToUnix(state.startSpan + index + 1) - ) - - // percentage of days in this span where baby is asleep in this time slot - val pc = if (daysWithDataMax > 0) { - (100F * daysWithData.toFloat() / daysWithDataMax.toFloat()).toInt() - } else { - // no data for this day - 0 - } - - Toast.makeText( - applicationContext, - "$startTimeString - $endTimeString ($durationString) - ${pc}%", - Toast.LENGTH_LONG - ).show() - lastToastShown = System.currentTimeMillis() - } - - override fun onNothingSelected() {} - }) - - val set1 = BarDataSet(values, "") - val data = BarData(set1) - set1.colors = allColors - - set1.setDrawValues(false) // usually too many values - set1.isHighlightEnabled = true - - set1.setDrawIcons(false) - - //Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}") - - data.setValueTextSize(12f) - barChart.setData(data) - - // does not work quite right yet - //Log.d(TAG, "xChartMax: ${barChart.xChartMax}") - //barChart.centerViewTo(barChart.xChartMax, 0F, YAxis.AxisDependency.LEFT) - - barChart.legend.isEnabled = false - val valueCount = min(values.size, 24) - barChart.setVisibleXRangeMaximum(valueCount.toFloat()) - barChart.xAxis.setLabelCount(valueCount) - barChart.xAxis.setCenterAxisLabels(false) - barChart.invalidate() - } - - // Sleep pattern bars that do not use time slots. - // This is useful/nicer for bars that only represent data of a singular days. - fun showSleepPatternBarGraphDaily(state: GraphState) { - val ranges = toSleepRanges(state.events) - val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) }) - - // stack awake/sleep durations - fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) { - assert(begin in spanBegin..spanEnd) - assert(end in spanBegin..spanEnd) - assert(begin <= end) - - val x = values[index].x - val yVals = values[index].yVals // alternating sleep/awake durations - val y = yVals.fold(0F) { acc, next -> acc + next } - - // y value is seconds when last awake - val awakeDuration = max(begin - spanBegin - y.toLong(), 0L) - val sleepDuration = end - begin - - if ((awakeDuration + sleepDuration) > (spanEnd - spanBegin)) { - Log.e(TAG, "Invalid sleep duration, exceeds day/week or month bounds => ignore value") - return - } - - // update value - val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat()) - values[index] = BarEntry(x, newYVals) - } - - for (range in ranges) { - // a sleep event can span to another day - // distribute sleep time over the days - val startUnix = range.start - val endUnix = range.end - val begIndex = unixToSpan(startUnix) - val endIndex = unixToSpan(endUnix) - var mid = startUnix - - //Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})") - for (i in begIndex..endIndex) { - // i is the days/weeks/months since unix epoch - val spanBegin = spanToUnix(i) - val spanEnd = spanToUnix(i + 1) - //Log.d(TAG, "mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}") - val sleepBegin = max(mid, spanBegin) - val sleepEnd = min(endUnix, spanEnd) - val index = i - state.startSpan - val duration = sleepEnd - sleepBegin - //Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}") - - state.dayCounter.setDaysWithData(sleepBegin, sleepEnd) - stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd) - - mid = sleepEnd - } - } - - val set1 = BarDataSet(values, "") - val data = BarData(set1) - - // awake phase color is transparent - set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt()) - set1.setDrawValues(false) // usually too many values - set1.isHighlightEnabled = true - - barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onValueSelected(e: Entry?, h: Highlight?) { - if (e == null || h == null) { - return - } - - val index = e.x.toInt() - if (index !in 0..values.size) { - return - } - - val value = values[index] - if (value.yVals == null || h.stackIndex !in 0..value.yVals.size) { - return - } - - if ((lastToastShown + TOAST_FREQUENCY_MS) > System.currentTimeMillis()) { - // only show one Toast message after another - return - } - - val duration = value.yVals[h.stackIndex].toInt() - val durationString = DateUtils.formatTimeDuration(applicationContext, duration.toLong()) - - val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month) - val startUnix = offsetUnix + value.yVals.sliceArray(0.. acc + y.toInt() } - val endUnix = startUnix + duration - - val format = SimpleDateFormat("HH:mm", Locale.getDefault()) - val startTimeString = format.format(startUnix * 1000).toString() - val endTimeString = format.format(endUnix * 1000).toString() - Toast.makeText(applicationContext, "$startTimeString - $endTimeString ($durationString)", Toast.LENGTH_LONG).show() - lastToastShown = System.currentTimeMillis() - } - - override fun onNothingSelected() {} - }) - //Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}") - set1.setDrawIcons(false) - - data.setValueTextSize(12f) - barChart.setData(data) - - //Log.d(TAG, "showSleepPatternBarGraphDaily: new barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}") - - barChart.legend.isEnabled = false - val valueCount = min(values.size, 24) - barChart.setVisibleXRangeMaximum(valueCount.toFloat()) - barChart.xAxis.setLabelCount(valueCount) - barChart.xAxis.setCenterAxisLabels(false) - barChart.invalidate() - } - - // Make sure the value on the bar is not out of screen. - class CustomHorizontalBarChartRenderer(chart: BarDataProvider, animator: ChartAnimator, viewPortHandler: ViewPortHandler): HorizontalBarChartRenderer(chart, animator, viewPortHandler) { - override fun drawValue( - c: Canvas, - valueText: String, - x: Float, - y: Float, - color: Int - ) { - mValuePaint.setColor(color) - c.drawText(valueText, x.coerceAtLeast(60F), y, mValuePaint) - } - - } - - fun showSleepBarGraph(state: GraphState) { - val ranges = toSleepRanges(state.events) - val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) }) - //Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}") - - for (range in ranges) { - // a sleep event can span to another day - // distribute sleep time over the days - val startUnix = range.start - val endUnix = range.end - val begIndex = unixToSpan(startUnix) - val endIndex = unixToSpan(endUnix) - var mid = startUnix - - //Log.d(TAG, "beginIndex: $begIndex, endIndex: $endIndex, startUnix: ${Date(startUnix * 1000)} ($startUnix), endUnix: ${Date(endUnix * 1000)} ($endUnix)") - //Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})") - for (i in begIndex..endIndex) { - // i is the days/weeks/months since unix epoch - val spanBegin = spanToUnix(i) - val spanEnd = spanToUnix(i + 1) - //Log.d(TAG, "i: $i, mid: ${Date(mid * 1000)}, spanBegin: ${Date(spanBegin * 1000)}, spanEnd: ${Date(spanEnd * 1000)}, beginUnix: ${Date(startUnix * 1000)} endUnix: ${Date(endUnix * 1000)}") - val sleepBegin = max(mid, spanBegin) - val sleepEnd = min(endUnix, spanEnd) - val index = i - state.startSpan - val duration = sleepEnd - sleepBegin - //Log.d(TAG, "[$index] sleepBegin: ${Date(sleepBegin * 1000)}, sleepEnd: ${Date(sleepEnd * 1000)}, ${DateUtils.formatTimeDuration(this, duration)}") - - state.dayCounter.setDaysWithData(sleepBegin, sleepEnd) - - if (graphTypeSelection == GraphType.SLEEP_SUM) { - values[index].y += duration.toFloat() - } else if (graphTypeSelection == GraphType.SLEEP_EVENTS) { - values[index].y += 1F - } else { - Log.e(TAG, "Unexpected graph type.") - return - } - - mid = sleepEnd - } - } - - for (index in values.indices) { - val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) - if (daysWithData == 0) { - assert(values[index].y == 0F) - } else { - values[index].y /= daysWithData - } - } - - val set1 = BarDataSet(values, "") - val data = BarData(set1) - set1.setDrawValues(true) - set1.isHighlightEnabled = false - - data.setValueFormatter(object : ValueFormatter() { - override fun getFormattedValue(value: Float): String { - val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "⌀ " } - return when (graphTypeSelection) { - GraphType.SLEEP_EVENTS -> { - prefix + value.toInt().toString() - } - GraphType.SLEEP_SUM -> { - prefix + DateUtils.formatTimeDuration(applicationContext, value.toLong()) - } - else -> { - Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection") - prefix + value.toInt().toString() - } - } - } - }) - - set1.setDrawIcons(false) - - data.setValueTextSize(12f) - barChart.setData(data) - - barChart.setRenderer(CustomHorizontalBarChartRenderer(barChart, barChart.getAnimator(), barChart.getViewPortHandler())) - - barChart.legend.isEnabled = false - val valueCount = min(values.size, 24) - barChart.setVisibleXRangeMaximum(valueCount.toFloat()) - barChart.xAxis.setLabelCount(valueCount) - barChart.xAxis.setCenterAxisLabels(false) - barChart.invalidate() - } - - fun showBottleBarGraph(state: GraphState) { - val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) }) - - for (event in state.events) { - val index = unixToSpan(event.time) - state.startSpan - - state.dayCounter.setDaysWithData(event.time, event.time) - - if (graphTypeSelection == GraphType.BOTTLE_EVENTS) { - values[index].y += 1F - } else if (graphTypeSelection == GraphType.BOTTLE_SUM) { - values[index].y += event.quantity.toFloat() - } else { - Log.e(TAG, "unhandled graphTypeSelection: $graphTypeSelection") - return - } - } - - for (index in values.indices) { - val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) - //Log.d(TAG, "values[$index].y: ${values[index].y}, daysWithData: $daysWithData") - if (daysWithData == 0) { - assert(values[index].y == 0F) - } else { - values[index].y /= daysWithData - } - } - - val set1 = BarDataSet(values, "") - - set1.setDrawValues(true) - set1.isHighlightEnabled = false - val data = BarData(set1) - - data.setValueFormatter(object : ValueFormatter() { - override fun getFormattedValue(value: Float): String { - val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "⌀ " } - //Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}") - return when (graphTypeSelection) { - GraphType.BOTTLE_EVENTS -> { - prefix + value.toInt().toString() - } - GraphType.BOTTLE_SUM -> { - prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt()) - } - else -> { - Log.e(TAG, "unhandled graphTypeSelection") - prefix + value.toInt() - } - } - } - }) - - data.setValueTextSize(12f) - barChart.setData(data) - - barChart.setRenderer(CustomHorizontalBarChartRenderer(barChart, barChart.getAnimator(), barChart.getViewPortHandler())) - - val valueCount = min(values.size, 24) - barChart.setVisibleXRangeMaximum(valueCount.toFloat()) - barChart.xAxis.setLabelCount(valueCount) - barChart.xAxis.setCenterAxisLabels(false) - barChart.invalidate() - } - - class DayCounter(val startDays: Int, val stopDays: Int) { - val daysWithData = BooleanArray(stopDays - startDays + 1) - - // count days in a span that have data - // e.g. return 7 (days) for applied span of a week where there is data for every day - fun countDaysWithData(beginUnix: Long, endUnix: Long): Int { - val beginDays = unixToDays(beginUnix) - val endDays = unixToDays(endUnix) - //Log.d(TAG, "countDaysWithData: beginDays: $beginDays, endDays: $endDays, ${Date(beginUnix * 1000)} - ${Date(endUnix * 1000)}") - var count = 0 - for (i in (beginDays - startDays)..<(endDays - startDays)) { - //Log.d(TAG, "countDaysWithData: i: $i, size: ${daysWithData.size}") - count += if (daysWithData[i]) { 1 } else { 0 } - } - return count - } - - fun setDaysWithData(beginUnix: Long, endUnix: Long) { - val beginDays = unixToDays(beginUnix) - val endDays = unixToDays(endUnix) - assert(beginDays <= endDays) - assert(startDays <= beginDays) - for (i in (beginDays - startDays)..(endDays - startDays)) { - daysWithData[i] = true - } - } - } - - data class GraphState(val events: List, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int) - - fun showGraph() { - //Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection") - barChart.fitScreen() - barChart.data?.clearValues() - barChart.xAxis.valueFormatter = null - barChart.notifyDataSetChanged() - barChart.clear() - barChart.invalidate() - //Log.d(TAG, "resetBarChart; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}") - - val type = when (graphTypeSelection) { - GraphType.BOTTLE_EVENTS, - GraphType.BOTTLE_SUM -> LunaEvent.Type.BABY_BOTTLE - GraphType.SLEEP_EVENTS, - GraphType.SLEEP_SUM, - GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP - } - - - val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time } - - if (events.isEmpty()) { - barChart.visibility = View.GONE - noDataTextView.visibility = View.VISIBLE - return - } else { - barChart.visibility = View.VISIBLE - noDataTextView.visibility = View.GONE - } - - // unix time span of all events - val startUnix = events.minOf { it.time } - val endUnix = System.currentTimeMillis() / 1000 - - // convert to days, weeks or months - val startSpan = unixToSpan(startUnix) - val endSpan = unixToSpan(endUnix) - - // days when the a day/week/month starts/ends - val startDays = unixToDays(spanToUnix(startSpan)) - val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span - val dayCounter = DayCounter(startDays, endDays) - - //Log.d(TAG, "startUnix: ${Date(1000 * startUnix)}, endUnix: ${Date(1000 * endUnix)}, startSpan: ${Date(1000 * spanToUnix(startSpan))}, endSpan: ${Date(1000 * spanToUnix(endSpan))}, startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}") - - // print dates - barChart.xAxis.valueFormatter = object: ValueFormatter() { - override fun getFormattedValue(value: Float): String { - val index = value.toInt() - val unixSeconds = spanToUnix(startSpan + index) - - val dateTime = Calendar.getInstance() - dateTime.time = Date(1000L * unixSeconds) - val year = dateTime.get(Calendar.YEAR) - val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0 - val week = dateTime.get(Calendar.WEEK_OF_YEAR) - val day = dateTime.get(Calendar.DAY_OF_MONTH) - - //Log.d(TAG, "index: $index, unixSeconds: ${Date(1000 * unixSeconds)}, day: $day, week: $week, month: $month, year: $year") - - // Adjust years if the first week of a year starts in the previous year. - val years = if (month == 12 && week == 1) { - year + 1 - } else { - year - } - - val days = "%02d".format(day) - val weeks = "%02d".format(week) - val months = "%02d".format(month) - - return when (timeRangeSelection) { - TimeRange.DAY -> "$days/$months/$years" - TimeRange.WEEK -> "$weeks/$years" - TimeRange.MONTH -> "$months/$years" - } - } - } - - val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan) - - when (graphTypeSelection) { - GraphType.BOTTLE_EVENTS, - GraphType.BOTTLE_SUM -> showBottleBarGraph(state) - GraphType.SLEEP_EVENTS, - GraphType.SLEEP_SUM -> showSleepBarGraph(state) - GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) { - // specialized pattern bar for daily view (optional) - showSleepPatternBarGraphDaily(state) - } else { - showSleepPatternBarGraphSlotted(state) - } - } - } - - private interface SpinnerItemSelected { - fun call(newValue: String?) - } - - private fun setupSpinner( - currentValue: String, - spinnerId: Int, - arrayId: Int, - arrayValuesId: Int, - callback: SpinnerItemSelected - ) { - val arrayValues = resources.getStringArray(arrayValuesId) - val spinner = findViewById(spinnerId) - val spinnerAdapter = - ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item) - - //Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})") - - spinner.adapter = spinnerAdapter - spinner.setSelection(arrayValues.indexOf(currentValue)) - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - var check = 0 - override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) { - if (pos >= arrayValues.size) { - Toast.makeText( - this@StatisticsActivity, - "pos out of bounds: $arrayValues", Toast.LENGTH_SHORT - ).show() - return - } - if (check++ > 0) { - callback.call(arrayValues[pos]) - } - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - // ignore - } - } - } - - companion object { - const val TAG = "StatisticsActivity" - - // 15 min steps - const val SLEEP_PATTERN_GRANULARITY = 15 * 60 - - // Time between toast messages (to prevent jams) - const val TOAST_FREQUENCY_MS = 3500 - - // color gradient - val SLEEP_PATTERN_COLORS = arrayOf( - "#FFFFFF".toColorInt(), "#EEF5F7".toColorInt(), "#DDEBEF".toColorInt(), "#CCE2E7".toColorInt(), - "#BBD8DF".toColorInt(), "#AACED7".toColorInt(), "#99C4CF".toColorInt(), "#88BAC7".toColorInt(), - "#77B1BF".toColorInt(), "#66A7B7".toColorInt(), "#559DAF".toColorInt(), "#4493A7".toColorInt(), - "#33899F".toColorInt(), "#228097".toColorInt(), "#11768F".toColorInt(), "#006C87".toColorInt() - ) - - var graphTypeSelection = GraphType.SLEEP_SUM - var timeRangeSelection = TimeRange.DAY - - private val dateTime = Calendar.getInstance() // scratch pad - - // convert month to seconds since epoch - fun unixToMonths(seconds: Long): Int { - dateTime.time = Date(seconds * 1000) - val years = dateTime.get(Calendar.YEAR) - val months = dateTime.get(Calendar.MONTH) - return 12 * years + months - } - - // convert month to seconds since epoch - fun monthsToUnix(months: Int): Long { - dateTime.time = Date(0) - dateTime.set(Calendar.YEAR, months / 12) - dateTime.set(Calendar.MONTH, months % 12) - dateTime.set(Calendar.HOUR, 0) - dateTime.set(Calendar.MINUTE, 0) - dateTime.set(Calendar.SECOND, 0) - return dateTime.time.time / 1000 - } - - // convert seconds to weeks since epoch - fun unixToWeeks(seconds: Long): Int { - dateTime.time = Date(seconds * 1000) - val years = dateTime.get(Calendar.YEAR) - 1970 - val weeks = dateTime.get(Calendar.WEEK_OF_YEAR) - val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0 - - if (month == 12 && weeks == 1) { - // The first week if the year might start in the previous year. - return 52 * (years + 1) + weeks - } - - return 52 * years + weeks - } - - // convert weeks to seconds since epoch - fun weeksToUnix(weeks: Int): Long { - dateTime.time = Date(0) - dateTime.set(Calendar.YEAR, 1970 + weeks / 52) - dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52) - dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) - dateTime.set(Calendar.HOUR, 0) - dateTime.set(Calendar.MINUTE, 0) - dateTime.set(Calendar.SECOND, 0) - return dateTime.time.time / 1000 - } - - // convert seconds to days since epoch - fun unixToDays(seconds: Long): Int { - dateTime.time = Date(seconds * 1000) - val years = dateTime.get(Calendar.YEAR) - val days = dateTime.get(Calendar.DAY_OF_YEAR) - return 365 * years + days - } - - // convert days to seconds since epoch - fun daysToUnix(days: Int): Long { - dateTime.time = Date(0) - dateTime.set(Calendar.YEAR, days / 365) - dateTime.set(Calendar.DAY_OF_YEAR, days % 365) - dateTime.set(Calendar.HOUR, 0) - dateTime.set(Calendar.MINUTE, 0) - dateTime.set(Calendar.SECOND, 0) - return dateTime.time.time / 1000 - } - - fun appendToFloatArray(array: FloatArray, vararg values: Float): FloatArray { - // create new array - val newArray = FloatArray(array.size + values.size) - // copy old values - for (i in array.indices) { - newArray[i] = array[i] - } - // add new values - for (i in values.indices) { - newArray[array.size + i] = values[i] - } - return newArray - } - - // for debugging - fun debugBarValues(values: ArrayList) { - for (value in values) { - val yVals = value.yVals - if (yVals != null) { - val y = yVals.fold(0F) { acc, next -> acc + next } - val yVals = yVals.joinToString { it.toString() } - Log.d(TAG, "value: ${value.x} $y ($yVals)") - } else { - Log.d(TAG, "value: ${value.x} ${value.y}") - } - } - } - } -} diff --git a/app/src/main/res/layout/activity_statistics.xml b/app/src/main/res/layout/activity_statistics.xml deleted file mode 100644 index 94290d2..0000000 --- a/app/src/main/res/layout/activity_statistics.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/more_events_popup.xml b/app/src/main/res/layout/more_events_popup.xml index eb99f21..b0c35be 100644 --- a/app/src/main/res/layout/more_events_popup.xml +++ b/app/src/main/res/layout/more_events_popup.xml @@ -10,17 +10,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - - - - + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 51c918f..87a93d3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -6,32 +6,4 @@ @string/amount_normal @string/amount_plenty - - - @string/statistics_bottle_sum - @string/statistics_bottle_events - @string/statistics_sleep_sum - @string/statistics_sleep_events - @string/statistics_sleep_pattern - - - - BOTTLE_SUM - BOTTLE_EVENTS - SLEEP_SUM - SLEEP_EVENTS - SLEEP_PATTERN - - - - Day - Week - Month - - - - DAY - WEEK - MONTH - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c4a4857..b9b94cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -88,9 +88,6 @@ Settings Retry - Statistics - No Data - Dynamic Menu Populate the header menu with the most used events. Settings @@ -141,13 +138,6 @@ °F °C - Bottle Events - Bottle Per Day - Medicine Events - Sleep Per Day - Sleep Events - Sleep Pattern - Description Qty Time diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99a0396..440fa8e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,8 +9,6 @@ lifecycleRuntimeKtx = "2.6.1" activityCompose = "1.8.0" composeBom = "2024.04.01" appcompat = "1.7.0" -mpandroidchart = "v4.2.2" -mpandroidchartVersion = "v3.1.0" recyclerview = "1.3.2" material = "1.12.0" sardineAndroid = "v0.9" @@ -33,8 +31,6 @@ androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } -mpandroidchart = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchart" } -mpandroidchart-vv310 = { module = "com.github.PhilJay:MPAndroidChart", version.ref = "mpandroidchartVersion" } sardine-android = { module = "com.github.thegrizzlylabs:sardine-android", version.ref = "sardineAndroid" } [plugins]