15 Commits

Author SHA1 Message Date
6f69b581e4 MainActivity: sort events before saving 2025-12-25 01:54:03 +01:00
ec80f78baf StatisticsActivity: rework all statistics
Improve the overall code.
2025-12-25 01:51:17 +01:00
2f877bdec7 NumericUtils: remove possible trailing whitespace 2025-12-25 01:51:17 +01:00
22e5e3ddec MainActivity: do not switch logbook on reload 2025-12-25 01:51:17 +01:00
2ae5a2deda LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2025-12-25 01:51:17 +01:00
e8480176c3 MainAcitivty: add dynamic header setting
The setting allows to build the menu and
popup list to be populated by the frequency
of events that has been created.
This also makes the 'no breastfeeding' setting irrelevant.
2025-12-25 01:51:13 +01:00
1c90fbd7c4 LunaEvent: use enum class for event types
This helps to have compile errors when some
case it not handled while adding a new type.
The enum class can also be interated over
to create a complete drop down list.
2025-12-25 01:46:38 +01:00
b164a80531 MainActivity: increase bottle volume to 340ml
This is the maximum amount found in sold bottles.
2025-12-25 01:46:38 +01:00
ae965ebe5e gradle: use uniform implementation directive for sardine-android 2025-12-25 01:46:38 +01:00
daac0d063f gradle: avoid inclusion of apk signing blobs
See https://android.izzysoft.de/articles/named/iod-scan-apkchecks?lang=en#blobs
2025-12-25 01:46:38 +01:00
eab55de651 gradle: set compileSDK/targetSdk to 36 2025-12-25 01:46:38 +01:00
7cf396026b StatisticsActivity: add statistics for bottle and sleep events 2025-12-25 01:46:20 +01:00
6759956461 MainActivity: show save button if any values has changed 2025-12-14 19:42:08 +01:00
c521a3373f MainActivity: use unique templates for notes 2025-12-14 19:42:08 +01:00
1ad5f31b32 LunaEvent: add sleep event 2025-12-14 19:42:03 +01:00
7 changed files with 254 additions and 367 deletions

View File

@@ -421,10 +421,10 @@ class MainActivity : AppCompatActivity() {
} }
fun addSleepEvent(event: LunaEvent) { fun addSleepEvent(event: LunaEvent) {
askSleepValue(event, true) { saveEvent(event) } askSleepValue(event) { saveEvent(event) }
} }
fun askSleepValue(event: LunaEvent, hideDurationButtons: Boolean, onPositive: () -> Unit) { fun askSleepValue(event: LunaEvent, onPositive: () -> Unit) {
val d = AlertDialog.Builder(this) val d = AlertDialog.Builder(this)
val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null) val dialogView = layoutInflater.inflate(R.layout.dialog_edit_duration, null)
d.setTitle(event.getDialogTitle(this)) d.setTitle(event.getDialogTitle(this))
@@ -432,9 +432,8 @@ class MainActivity : AppCompatActivity() {
d.setView(dialogView) d.setView(dialogView)
val durationTextView = dialogView.findViewById<TextView>(R.id.dialog_date_duration) val durationTextView = dialogView.findViewById<TextView>(R.id.dialog_date_duration)
val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now) val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5) val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5) val durationPlus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_plus5)
@@ -466,32 +465,24 @@ class MainActivity : AppCompatActivity() {
onDateChange(pickedDateTime.time.time / 1000) onDateChange(pickedDateTime.time.time / 1000)
if (hideDurationButtons) { fun adjust(minutes: Int) {
durationButtons.visibility = View.GONE duration += minutes * 60
d.setMessage(getString(R.string.log_sleep_dialog_description_start)) if (duration < 0) {
} else { duration = 0
durationButtons.visibility = View.VISIBLE
d.setMessage(event.getDialogMessage(this))
fun adjust(minutes: Int) {
duration += minutes * 60
if (duration < 0) {
duration = 0
}
onDateChange(pickedDateTime.time.time / 1000)
} }
onDateChange(pickedDateTime.time.time / 1000)
}
durationMinus5Button.setOnClickListener { adjust(-5) } durationMinus5Button.setOnClickListener { adjust(-5) }
durationPlus5Button.setOnClickListener { adjust(5) } durationPlus5Button.setOnClickListener { adjust(5) }
durationNowButton.setOnClickListener { durationNowButton.setOnClickListener {
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000 val start = pickedDateTime.time.time / 1000
if (now > start) { if (now > start) {
duration = (now - start).toInt() duration = (now - start).toInt()
duration -= duration % 60 // prevent printing of seconds duration -= duration % 60 // prevent printing of seconds
onDateChange(pickedDateTime.time.time / 1000) onDateChange(pickedDateTime.time.time / 1000)
}
} }
} }
@@ -516,7 +507,6 @@ class MainActivity : AppCompatActivity() {
} }
fun addAmountEvent(event: LunaEvent) { fun addAmountEvent(event: LunaEvent) {
setToPreviousQuantity(event)
askAmountValue(event, true) { saveEvent(event) } askAmountValue(event, true) { saveEvent(event) }
} }
@@ -533,7 +523,7 @@ class MainActivity : AppCompatActivity() {
R.array.AmountLabels, R.array.AmountLabels,
android.R.layout.simple_spinner_dropdown_item android.R.layout.simple_spinner_dropdown_item
) )
// set pre-selected item and ensure the quantity to index is in bounds
spinner.setSelection(event.quantity.coerceIn(0, spinner.count - 1)) spinner.setSelection(event.quantity.coerceIn(0, spinner.count - 1))
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
@@ -845,7 +835,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues) LunaEvent.Type.PUKE -> askAmountValue(event, false, updateValues)
LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues) LunaEvent.Type.TEMPERATURE -> askTemperatureValue(event, false, updateValues)
LunaEvent.Type.NOTE -> askNotes(event, false, updateValues) LunaEvent.Type.NOTE -> askNotes(event, false, updateValues)
LunaEvent.Type.SLEEP -> askSleepValue(event, false, updateValues) LunaEvent.Type.SLEEP -> askSleepValue(event, updateValues)
else -> { else -> {
Log.w(TAG, "Unexpected type: ${event.type}") Log.w(TAG, "Unexpected type: ${event.type}")
} }
@@ -1141,7 +1131,7 @@ class MainActivity : AppCompatActivity() {
} }
logbook?.logs?.add(0, event) logbook?.logs?.add(0, event)
logbook?.sort() logbook?.sort()
recyclerView.adapter?.notifyDataSetChanged() recyclerView.adapter?.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0) recyclerView.smoothScrollToPosition(0)
saveLogbook(event) saveLogbook(event)

View File

@@ -12,7 +12,6 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.toColorInt import androidx.core.graphics.toColorInt
import com.github.mikephil.charting.charts.BarChart import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.components.YAxis
import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.data.BarEntry
@@ -32,7 +31,7 @@ import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class StatisticsActivity : AppCompatActivity() { class StatisticsActivity : AppCompatActivity() {
var lastToastShown = 0L var lastToastShown = 0L
lateinit var barChart: BarChart lateinit var barChart: BarChart
@@ -83,12 +82,10 @@ class StatisticsActivity : AppCompatActivity() {
barChart.axisRight.setDrawGridLines(false) barChart.axisRight.setDrawGridLines(false)
barChart.axisRight.setDrawLabels(false) barChart.axisRight.setDrawLabels(false)
barChart.xAxis.setDrawGridLines(true)
barChart.xAxis.setDrawLabels(true) barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false) barChart.xAxis.setDrawAxisLine(false)
barChart.isScaleXEnabled = false
barChart.isScaleYEnabled = true
graphTypeSpinner = findViewById(R.id.graph_type_selection) graphTypeSpinner = findViewById(R.id.graph_type_selection)
timeRangeSpinner = findViewById(R.id.time_range_selection) timeRangeSpinner = findViewById(R.id.time_range_selection)
@@ -101,7 +98,7 @@ class StatisticsActivity : AppCompatActivity() {
//Log.d("event", "new value: $newValue") //Log.d("event", "new value: $newValue")
newValue ?: return newValue ?: return
graphTypeSelection = GraphType.valueOf(newValue) graphTypeSelection = GraphType.valueOf(newValue)
showGraph() updateGraph()
} }
} }
) )
@@ -116,13 +113,13 @@ class StatisticsActivity : AppCompatActivity() {
newValue ?: return newValue ?: return
timeRangeSelection = TimeRange.valueOf(newValue) timeRangeSelection = TimeRange.valueOf(newValue)
setSpans() setSpans()
showGraph() updateGraph()
} }
} }
) )
setSpans() setSpans()
showGraph() updateGraph()
} }
fun setSpans() { fun setSpans() {
@@ -139,54 +136,20 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
fun resetBarChart() {
barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
/*
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.setDrawGridLines(true)
barChart.xAxis.setDrawLabels(true)
barChart.xAxis.setDrawAxisLine(false)
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.isScaleXEnabled = false
//barChart.isScaleYEnabled = true
*/
// for debugging
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}")
}
fun showMedicineBarGraph(state: GraphState) { fun showMedicineBarGraph(state: GraphState) {
val values = HashMap<String, ArrayList<BarEntry>>() val values = HashMap<String, ArrayList<BarEntry>>()
val days = state.endSpan - state.startSpan + 1
for (event in state.events) { for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase() val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) { val array = values.getOrPut(key) {
// create initial array with 0 ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
ArrayList(List(days) { BarEntry(it.toFloat(), 0F) })
} }
array[index].y += 1F array[index].y += 1F
} }
Log.d(TAG, "values.size: ${values.size}, days: $days") Log.d(TAG, "values.size: ${values.size}")
for ((key, value) in values) { for ((key, value) in values) {
Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}") Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}")
} }
@@ -205,18 +168,19 @@ class StatisticsActivity : AppCompatActivity() {
} }
val sets = arrayListOf<IBarDataSet>() val sets = arrayListOf<IBarDataSet>()
for ((key, array) in values.entries) { for ((key, value) in values.entries) {
val description = shorten(key) if (key.startsWith("v")) {
Log.d(TAG, "key: $key") val description = shorten(key)
val barDataSet = BarDataSet(array, description) val barDataSet = BarDataSet(value, description)
barDataSet.color = chooseColor(key) barDataSet.color = chooseColor(key)
sets.add(barDataSet) sets.add(barDataSet)
}
} }
val data = BarData(sets) val data = BarData(sets)
//data.groupBars(0F, 0.2F, 0.1F); //data.groupBars(0F, 0.2F, 0.1F);
//data.setValueTextSize(12f) data.setValueTextSize(12f)
//data.barWidth = 1F data.barWidth = 1F
//data.groupBars(0F, 1F, 1F) //data.groupBars(0F, 1F, 1F)
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
@@ -229,31 +193,22 @@ class StatisticsActivity : AppCompatActivity() {
} }
}) })
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { barChart.setScaleEnabled(true)
override fun onValueSelected(e: Entry?, h: Highlight?) {
Log.d(TAG, "onValueSelected ${e == null} ${h == null}")
if (e == null || h == null) {
return
}
val index = e.x.toInt()
if (index !in 0..values.size) {
return
}
Log.d(TAG, "index: $index")
}
override fun onNothingSelected() {}
})
data.setValueTextSize(12f)
barChart.setData(data)
barChart.legend.isEnabled = true barChart.legend.isEnabled = true
val valueCount = min(days, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) //barChart.xAxis.setLabelCount(min(values.size, 24), false);
barChart.xAxis.setLabelCount(valueCount) //val maxCount = min(maxIndex, 30) // values.size
barChart.xAxis.setCenterAxisLabels(false) //Log.d(TAG, "maxCount: $maxCount")
barChart.setVisibleXRangeMaximum(20F) //maxCount.toFloat()) // show max 24 entries
barChart.xAxis.setLabelCount(30, true)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
barChart.setData(data)
barChart.invalidate() barChart.invalidate()
} }
@@ -292,9 +247,12 @@ class StatisticsActivity : AppCompatActivity() {
return ranges return ranges
} }
fun showSleepPatternBarGraphSlotted(state: GraphState) { fun showSleepPatternBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events) val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
val values = ArrayList<BarEntry>() val values = ArrayList<BarEntry>()
val stack = ArrayList(List(state.endSpan - state.startSpan + 1) { IntArray(24 * 60 * 60 / SLEEP_PATTERN_GRANULARITY) }) 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}") Log.d(TAG, "stack.size: ${stack.size}, array.size: ${stack[0].size}, dayCounter.daysWithData.size: ${state.dayCounter.daysWithData.size}")
@@ -314,11 +272,14 @@ class StatisticsActivity : AppCompatActivity() {
val sleepEnd = min(end, dayEnd) val sleepEnd = min(end, dayEnd)
if (sleepBegin != sleepEnd) { if (sleepBegin != sleepEnd) {
//val index2 = i - spanBegin
//val duration = dayEnd - dayBegin
assert(dayBegin <= dayEnd) assert(dayBegin <= dayEnd)
assert(sleepBegin <= sleepEnd) assert(sleepBegin <= sleepEnd)
//val duration = sleepEnd - sleepBegin
val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY val iBegin = (sleepBegin - dayBegin) / SLEEP_PATTERN_GRANULARITY
val iEnd = iBegin + (sleepEnd - sleepBegin) / 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)}") 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..iEnd) { for (j in iBegin..iEnd) {
stack[index][j.toInt()] += 1 stack[index][j.toInt()] += 1
} }
@@ -356,7 +317,7 @@ class StatisticsActivity : AppCompatActivity() {
} }
fun mapColor(occurrences: Int, maxOccurrences: Int): Int { fun mapColor(occurrences: Int, maxOccurrences: Int): Int {
// occurrences: number of reported sleeps in a specific time slot // occurences: number of reported sleeps in a specific time slice
// maxOccurrences: maximum number of days with data that can contribute to maxOccurrences // maxOccurrences: maximum number of days with data that can contribute to maxOccurrences
assert(maxOccurrences > 0) assert(maxOccurrences > 0)
assert(occurrences <= maxOccurrences) assert(occurrences <= maxOccurrences)
@@ -373,18 +334,18 @@ class StatisticsActivity : AppCompatActivity() {
for ((index, dayArray) in stack.withIndex()) { for ((index, dayArray) in stack.withIndex()) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) 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() }}") //Log.d(TAG, "index: $index: dayArray: ${dayArray.joinToString { it.toString() }}")
val vals = ArrayList<Float>() val vals = ArrayList<Float>()
var prevIndex = -1 // time slot index var prevIndex = -1 // time slice index
var prevValue = -1 // number of entries we have found for time slot var prevValue = -1 // number of entries we have found for time slice
for ((i, v) in dayArray.withIndex()) { for ((i, v) in dayArray.withIndex()) {
if (i == 0) { if (i == 0) {
prevIndex = i prevIndex = i
prevValue = v prevValue = v
} else if (prevValue != v) { } else if (prevValue != v) {
vals.add((i - prevIndex).toFloat()) vals.add((i - prevIndex).toFloat())
allColors.add(mapColor(prevValue.coerceAtMost(daysWithData), daysWithData)) allColors.add(mapColor(prevValue, daysWithData))
prevIndex = i prevIndex = i
prevValue = v prevValue = v
} }
@@ -397,74 +358,47 @@ class StatisticsActivity : AppCompatActivity() {
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}") //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())) values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
} }
//Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}") Log.d(TAG, "daysWithData: ${state.dayCounter.daysWithData.joinToString()}")
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) { override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) {
return if (e != null && h != null && e.x.toInt() != -1 && h.stackIndex != -1) {
if ((lastToastShown + 3500) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val index = e.x.toInt()
val value = values[index]
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + e.x.toInt())
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds = SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> 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 = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, durationSeconds)
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()
} }
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..<h.stackIndex)
.fold(0) { acc, y -> 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 = NumericUtils(applicationContext).formatEventQuantity(
LunaEvent.Type.SLEEP,
durationSeconds
)
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() {} override fun onNothingSelected() {}
@@ -473,53 +407,26 @@ class StatisticsActivity : AppCompatActivity() {
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
val data = BarData(set1) val data = BarData(set1)
set1.colors = allColors set1.colors = allColors
//set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values set1.setDrawValues(false) // usually too many values
set1.isHighlightEnabled = true set1.isHighlightEnabled = true
set1.setDrawIcons(false) set1.setDrawIcons(false)
barChart.legend.isEnabled = false
//barChart.legend.isEnabled = false barChart.setScaleEnabled(false)
/* barChart.xAxis.setLabelCount(min(values.size, 24))
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
*/
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}")
//barChart.minimumWidth
//barChart.isAutoScaleMinMaxEnabled = true
//barChart.setScaleEnabled(true)
//barChart.fitScreen()
//debugBarValues(values)
//barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
Log.d(TAG, "xChartMax: ${barChart.xChartMax}")
barChart.centerViewTo(barChart.xChartMax, 0F, YAxis.AxisDependency.RIGHT)
// does not work quite right yet
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.invalidate() barChart.invalidate()
//barChart.moveViewToX(77F) //values.lastOrNull()!!.x)
} }
// 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) { fun showSleepPatternBarGraphDaily(state: GraphState) {
val ranges = toSleepRanges(state.events) val ranges = toSleepRanges(state.events)
Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) }) val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), FloatArray(0)) })
// stack awake/sleep durations // stack awake/sleep durations
@@ -586,68 +493,46 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(e: Entry?, h: Highlight?) { override fun onValueSelected(e: Entry?, h: Highlight?) {
if (e == null || h == null) { if (e != null && h != null && e.x.toInt() != -1 && h.stackIndex != -1) {
return if ((lastToastShown + 3500) > System.currentTimeMillis()) {
// only show one Toast message after another
return
}
val value = values[e.x.toInt()]
val duration = value.yVals[h.stackIndex].toInt()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month)
val startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> 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()
} }
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 = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration)
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month)
val startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> 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() {} override fun onNothingSelected() {}
}) })
Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
set1.setDrawIcons(false) set1.setDrawIcons(false)
//barChart.legend.isEnabled = false barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
//val valueCount = min(values.size, 24) barChart.xAxis.setLabelCount(min(values.size, 24))
//barChart.setVisibleXRangeMaximum(valueCount.toFloat())
//barChart.xAxis.setLabelCount(valueCount)
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data) 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() barChart.invalidate()
} }
fun showSleepBarGraph(state: GraphState) { fun showSleepBarGraph(state: GraphState) {
val ranges = toSleepRanges(state.events) val ranges = toSleepRanges(state.events)
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) }) 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)}") Log.d(TAG, "startUnix: ${Date(state.startUnix * 1000)}, endUnix: ${Date(state.endUnix * 1000)}")
for (range in ranges) { for (range in ranges) {
// a sleep event can span to another day // a sleep event can span to another day
@@ -658,13 +543,12 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix 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})") //Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, endUnix: ${Date(endUnix * 1000)}, begIndex: $begIndex, endIndex: $endIndex (index diff: ${endIndex - begIndex})")
for (i in begIndex..endIndex) { for (i in begIndex..endIndex) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val spanBegin = spanToUnix(i) val spanBegin = spanToUnix(i)
val spanEnd = spanToUnix(i + 1) 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)}") //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 sleepBegin = max(mid, spanBegin)
val sleepEnd = min(endUnix, spanEnd) val sleepEnd = min(endUnix, spanEnd)
val index = i - state.startSpan val index = i - state.startSpan
@@ -686,12 +570,14 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
} }
} }
for (index in values.indices) { if (graphTypeSelection == GraphType.SLEEP_SUM) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) for (index in values.indices) {
if (daysWithData == 0) { val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
assert(values[index].y == 0F) if (daysWithData == 0) {
} else { assert(values[index].y == 0F)
values[index].y /= daysWithData } else {
values[index].y /= daysWithData
}
} }
} }
@@ -702,44 +588,44 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return when (graphTypeSelection) { return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> { GraphType.SLEEP_EVENTS -> value.toInt().toString()
prefix + value.toInt().toString()
}
GraphType.SLEEP_SUM -> { GraphType.SLEEP_SUM -> {
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt()) val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt())
} }
else -> { else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection") Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
prefix + value.toInt().toString() value.toInt().toString()
} }
} }
} }
}) })
set1.setDrawIcons(false) set1.setDrawIcons(false)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.invalidate() barChart.invalidate()
} }
fun showBottleBarGraph(state: GraphState) { fun showBottleBarGraph(state: GraphState) {
val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) }) val values = ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
// needed?
for (i in values.indices) {
values[i].x = i.toFloat()
}
for (event in state.events) { for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan val index = unixToSpan(event.time) - state.startSpan
state.dayCounter.setDaysWithData(event.time, event.time) state.dayCounter.setDaysWithData(event.time, event.time)
values[index].x += values.size.toFloat() // setDaysWithData(sleepBegin, sleepEnd)
if (graphTypeSelection == GraphType.BOTTLE_EVENTS) { if (graphTypeSelection == GraphType.BOTTLE_EVENTS) {
values[index].y += 1F values[index].y += 1F
} else if (graphTypeSelection == GraphType.BOTTLE_SUM) { } else if (graphTypeSelection == GraphType.BOTTLE_SUM) {
@@ -750,13 +636,15 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
} }
} }
for (index in values.indices) { if (graphTypeSelection == GraphType.BOTTLE_SUM) {
val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1)) for (index in values.indices) {
//Log.d(TAG, "index: $index, daysWithData: $daysWithData") val daysWithData = state.dayCounter.countDaysWithData(spanToUnix(state.startSpan + index), spanToUnix(state.startSpan + index + 1))
if (daysWithData == 0) { //Log.d(TAG, "index: $index, daysWithData: $daysWithData")
assert(values[index].y == 0F) if (daysWithData == 0) {
} else { assert(values[index].y == 0F)
values[index].y /= daysWithData } else {
values[index].y /= daysWithData
}
} }
} }
@@ -765,8 +653,21 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
set1.setDrawValues(true) set1.setDrawValues(true)
set1.isHighlightEnabled = false set1.isHighlightEnabled = false
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true val maximumRange = 20F
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F) if (graphTypeSelection == GraphType.BOTTLE_SUM || graphTypeSelection == GraphType.BOTTLE_EVENTS) {
//val count = values.size.coerceIn(5, 20)
barChart.setVisibleXRangeMaximum(maximumRange) // show max 24 entries
barChart.xAxis.setLabelCount(maximumRange.toInt(), false)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
}
//val dataSets = ArrayList<IBarDataSet?>()
//dataSets.add(set1)
val data = BarData(set1) val data = BarData(set1)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth. //data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth.
@@ -774,34 +675,29 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}") //Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) { return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> { GraphType.BOTTLE_EVENTS -> value.toInt().toString()
prefix + value.toInt().toString()
}
GraphType.BOTTLE_SUM -> { GraphType.BOTTLE_SUM -> {
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt()) val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
} }
//GraphType.BOTTLE_SUM_AVERAGE -> "⌀ " + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.TYPE_BABY_BOTTLE, value.toInt())
else -> { else -> {
Log.e(TAG, "unhandled graphTypeSelection") Log.e(TAG, "unhandled graphTypeSelection")
prefix + value.toInt() value.toInt().toString()
} }
} }
} }
}) })
// hm, does not work yet
Log.d(TAG, "last value: ${values.lastOrNull()!!.x}")
barChart.moveViewToX(100F)
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
//barChart.legend.isEnabled = false
val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.invalidate() barChart.invalidate()
//barChart.moveViewToX(values.lastOrNull()!!.x)
} }
class DayCounter(val startDays: Int, val stopDays: Int) { class DayCounter(val startDays: Int, val stopDays: Int) {
@@ -812,10 +708,8 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
fun countDaysWithData(beginUnix: Long, endUnix: Long): Int { fun countDaysWithData(beginUnix: Long, endUnix: Long): Int {
val beginDays = unixToDays(beginUnix) val beginDays = unixToDays(beginUnix)
val endDays = unixToDays(endUnix) val endDays = unixToDays(endUnix)
//Log.d(TAG, "countDaysWithData: beginDays: $beginDays, endDays: $endDays, ${Date(beginUnix * 1000)} - ${Date(endUnix * 1000)}")
var count = 0 var count = 0
for (i in (beginDays - startDays)..<(endDays - startDays)) { for (i in (beginDays - startDays)..<(endDays - startDays)) {
//Log.d(TAG, "countDaysWithData: i: $i, size: ${daysWithData.size}")
count += if (daysWithData[i]) { 1 } else { 0 } count += if (daysWithData[i]) { 1 } else { 0 }
} }
return count return count
@@ -834,8 +728,8 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int) data class GraphState(val events: List<LunaEvent>, val dayCounter: DayCounter, val startUnix: Long, val endUnix: Long, val startSpan: Int, val endSpan: Int)
// wrapper for common graph setup // wrapper for comon graph setup
fun prepareGraph(type: LunaEvent.Type, callback: (GraphState) -> Unit) { fun prepareGraph(type: LunaEvent.Type, cb: (GraphState) -> Unit) {
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time } val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) { if (events.isEmpty()) {
@@ -857,7 +751,7 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
// days when the a day/week/month starts/ends // days when the a day/week/month starts/ends
val startDays = unixToDays(spanToUnix(startSpan)) val startDays = unixToDays(spanToUnix(startSpan))
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next week
val dayCounter = DayCounter(startDays, endDays) val dayCounter = DayCounter(startDays, endDays)
// print dates // print dates
@@ -872,51 +766,28 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0 val month = dateTime.get(Calendar.MONTH) + 1 // month starts at 0
val week = dateTime.get(Calendar.WEEK_OF_YEAR) val week = dateTime.get(Calendar.WEEK_OF_YEAR)
val day = dateTime.get(Calendar.DAY_OF_MONTH) val day = dateTime.get(Calendar.DAY_OF_MONTH)
// 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) { return when (timeRangeSelection) {
TimeRange.DAY -> "$days/$months/$years" TimeRange.DAY -> "$day/$month/$year"
TimeRange.WEEK -> "$weeks/$years" TimeRange.WEEK -> "$week/$year"
TimeRange.MONTH -> "$months/$years" TimeRange.MONTH -> "$month/$year"
} }
} }
} }
//Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}") Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
callback(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan)) cb(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
} }
fun showGraph() { fun updateGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection") Log.d(TAG, "updateGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
// test
resetBarChart()
when (graphTypeSelection) { when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS, GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) } GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.SLEEP_EVENTS, GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) } GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraphSlotted(state) } GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraph(state) }
/* if (timeRangeSelection == TimeRange.DAY) {
// specialized pattern bar for daily view (optional)
showSleepPatternBarGraphDaily(state)
} else {
showSleepPatternBarGraphSlotted(state)
}
}
*/
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) } GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
} }
} }
@@ -966,10 +837,7 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
const val TAG = "StatisticsActivity" const val TAG = "StatisticsActivity"
// 15 min steps // 15 min steps
const val SLEEP_PATTERN_GRANULARITY = 15 * 60 val SLEEP_PATTERN_GRANULARITY = 15 * 60
// Time between toast messages (to prevent jams)
const val TOAST_FREQUENCY_MS = 3500
// color gradient // color gradient
val SLEEP_PATTERN_COLORS = arrayOf( val SLEEP_PATTERN_COLORS = arrayOf(
@@ -978,22 +846,20 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
"#77B1BF".toColorInt(), "#66A7B7".toColorInt(), "#559DAF".toColorInt(), "#4493A7".toColorInt(), "#77B1BF".toColorInt(), "#66A7B7".toColorInt(), "#559DAF".toColorInt(), "#4493A7".toColorInt(),
"#33899F".toColorInt(), "#228097".toColorInt(), "#11768F".toColorInt(), "#006C87".toColorInt() "#33899F".toColorInt(), "#228097".toColorInt(), "#11768F".toColorInt(), "#006C87".toColorInt()
) )
private val dateTime = Calendar.getInstance() // scratch pad
var graphTypeSelection = GraphType.SLEEP_SUM var graphTypeSelection = GraphType.SLEEP_SUM
var timeRangeSelection = TimeRange.DAY var timeRangeSelection = TimeRange.DAY
private val dateTime = Calendar.getInstance() // scratch pad
// convert month to seconds since epoch
fun unixToMonths(seconds: Long): Int { fun unixToMonths(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000) dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR) val years = dateTime.get(Calendar.YEAR)
val months = dateTime.get(Calendar.MONTH) val months = dateTime.get(Calendar.MONTH)
return 12 * years + months return 12 * years + months
} }
// convert month to seconds since epoch
fun monthsToUnix(months: Int): Long { fun monthsToUnix(months: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0) dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, months / 12) dateTime.set(Calendar.YEAR, months / 12)
dateTime.set(Calendar.MONTH, months % 12) dateTime.set(Calendar.MONTH, months % 12)
@@ -1003,25 +869,18 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
return dateTime.time.time / 1000 return dateTime.time.time / 1000
} }
// convert seconds to weeks since epoch
fun unixToWeeks(seconds: Long): Int { fun unixToWeeks(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000) dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR) - 1970 val years = dateTime.get(Calendar.YEAR)
val weeks = dateTime.get(Calendar.WEEK_OF_YEAR) 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 return 52 * years + weeks
} }
// convert weeks to seconds since epoch
fun weeksToUnix(weeks: Int): Long { fun weeksToUnix(weeks: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0) dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, 1970 + weeks / 52) dateTime.set(Calendar.YEAR, weeks / 52)
dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52) dateTime.set(Calendar.WEEK_OF_YEAR, weeks % 52)
dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY) dateTime.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
dateTime.set(Calendar.HOUR, 0) dateTime.set(Calendar.HOUR, 0)
@@ -1030,16 +889,17 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
return dateTime.time.time / 1000 return dateTime.time.time / 1000
} }
// convert seconds to days since epoch
fun unixToDays(seconds: Long): Int { fun unixToDays(seconds: Long): Int {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(seconds * 1000) dateTime.time = Date(seconds * 1000)
val years = dateTime.get(Calendar.YEAR) val years = dateTime.get(Calendar.YEAR)
val days = dateTime.get(Calendar.DAY_OF_YEAR) val days = dateTime.get(Calendar.DAY_OF_YEAR)
return 365 * years + days return 365 * years + days
} }
// convert days to seconds since epoch // convert from days to Date
fun daysToUnix(days: Int): Long { fun daysToUnix(days: Int): Long {
//val dateTime = Calendar.getInstance()
dateTime.time = Date(0) dateTime.time = Date(0)
dateTime.set(Calendar.YEAR, days / 365) dateTime.set(Calendar.YEAR, days / 365)
dateTime.set(Calendar.DAY_OF_YEAR, days % 365) dateTime.set(Calendar.DAY_OF_YEAR, days % 365)
@@ -1063,6 +923,42 @@ Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barCha
return newArray return newArray
} }
/*
fun colorGradient(fromColor: Int, toColor: Int, percent: Int): Int {
assert(percent in 0..100)
val a1 = fromColor.shr(24).and(0xff)
val r1 = fromColor.shr(16).and(0xff)
val g1 = fromColor.shr(8).and(0xff)
val b1 = fromColor.shr(0).and(0xff)
//Log.d(TAG, "${a1.toHexString()} ${r1.toHexString()} ${g1.toHexString()} ${b1.toHexString()}")
val a2 = toColor.shr(24).and(0xff)
val r2 = toColor.shr(16).and(0xff)
val g2 = toColor.shr(8).and(0xff)
val b2 = toColor.shr(0).and(0xff)
//Log.d(TAG, "${a2.toHexString()} ${r2.toHexString()} ${g2.toHexString()} ${b2.toHexString()}")
val pc = (percent.toFloat() / 100F).coerceIn(0F, 1F)
val a = a1 + (pc * abs(a2 - a1)).toInt()
val r = r1 + (pc * abs(r2 - r1)).toInt()
val g = g1 + (pc * abs(g2 - g1)).toInt()
val b = a1 + (pc * abs(b2 - b1)).toInt()
//Log.d(TAG, "${a.toHexString()} ${r.toHexString()} ${g.toHexString()} ${b.toHexString()}")
val Red = r.shl(16).and(0x00FF0000)
val Green = g.shl(8).and(0x0000FF00)
val Blue = b.and(0x000000FF)
val aa = a.shl(24).and(0xFF000000.toInt())
val color = aa.or(Red).or(Green).or(Blue)
return color
//Log.d(TAG, "c: ${c.toHexString()} ${color.toInt().toHexString()}")
//return Color.argb(a, r, g, b)
}
*/
// for debugging // for debugging
fun debugBarValues(values: ArrayList<BarEntry>) { fun debugBarValues(values: ArrayList<BarEntry>) {
for (value in values) { for (value in values) {

View File

@@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
</vector>

View File

@@ -177,16 +177,15 @@
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp"/> android:textSize="30sp"/>
<TextView <ImageView
android:id="@+id/button_more" android:id="@+id/button_more"
android:layout_width="0dp" android:layout_width="60dp"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:layout_margin="5dp" android:layout_margin="5dp"
android:layout_weight="1" android:layout_weight="0"
android:background="@drawable/button_background" android:background="@drawable/button_background"
android:gravity="center_horizontal" android:gravity="center_horizontal"
android:textSize="30sp" android:src="@drawable/ic_more"
android:text="☰"
app:tint="@android:color/darker_gray"/> app:tint="@android:color/darker_gray"/>
</LinearLayout> </LinearLayout>

View File

@@ -21,7 +21,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" android:visibility="gone"
android:gravity="center" android:gravity="center"
android:text="@string/statistics_no_data"/> android:text="No Data"/>
</FrameLayout> </FrameLayout>

View File

@@ -15,7 +15,6 @@
android:text="💤"/> android:text="💤"/>
<LinearLayout <LinearLayout
android:id="@+id/duration_buttons"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"

View File

@@ -89,7 +89,6 @@
<string name="no_connection_retry">Retry</string> <string name="no_connection_retry">Retry</string>
<string name="statistics_title">Statistics</string> <string name="statistics_title">Statistics</string>
<string name="statistics_no_data">No Data</string>
<string name="settings_dynamic_menu">Dynamic Menu</string> <string name="settings_dynamic_menu">Dynamic Menu</string>
<string name="settings_dynamic_menu_desc">Populate the header menu with the most used events.</string> <string name="settings_dynamic_menu_desc">Populate the header menu with the most used events.</string>
@@ -131,7 +130,6 @@
<string name="log_unknown_dialog_description"></string> <string name="log_unknown_dialog_description"></string>
<string name="log_weight_dialog_description">Insert the weight:</string> <string name="log_weight_dialog_description">Insert the weight:</string>
<string name="log_sleep_dialog_description">Set sleep duration:</string> <string name="log_sleep_dialog_description">Set sleep duration:</string>
<string name="log_sleep_dialog_description_start">Start sleep cycle:</string>
<string name="measurement_unit_liquid_base_metric" translatable="false">ml</string> <string name="measurement_unit_liquid_base_metric" translatable="false">ml</string>
<string name="measurement_unit_weight_base_metric" translatable="false">g</string> <string name="measurement_unit_weight_base_metric" translatable="false">g</string>