13 Commits

Author SHA1 Message Date
d76f5cf2ce MainActivity: set unit for bottle content 2026-03-10 23:14:58 +01:00
557a9ab69a DateUtils: make duration rounded up to minutes 2026-03-06 12:33:57 +01:00
620e20aa2e MainActivity: inline nested function for dynamic menu 2026-03-06 12:33:57 +01:00
8687d62bac MainActivity: generate dynamic menu from last two weeks 2026-03-06 12:33:57 +01:00
3ae68ffa7b LunaEvent: rework sleep event
Make the UI more flexible and
slightly easier to understand.
2026-03-06 12:33:53 +01:00
dd4be8dcd7 MainActivity: rename datepicker 2026-02-19 16:41:16 +01:00
cb91922e2a layout: replace menu icon with utf8 character
The three dot menu icosn looks odd when stretched
due to the dynamic menu feature. Thus replace it
with the hamburger menu character that looks better
when scaled.
2026-02-19 16:41:16 +01:00
24f48cb533 MainActivity: sort events before saving
Also replace notifyItemInserted since it does not
call the adapter to redraw the row striping when
a new event is added.
2026-02-19 16:41:16 +01:00
08022541f1 StatisticsActivity: rework all statistics
Improve the overall code.
2026-02-19 16:41:12 +01:00
d832aa4330 NumericUtils: remove possible trailing whitespace 2026-02-09 08:23:38 +01:00
672ae37049 MainActivity: do not switch logbook on reload 2026-02-09 08:23:38 +01:00
672e58c028 LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2026-02-09 08:23:38 +01:00
d2729af30f 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.
2026-02-09 08:23:21 +01:00
9 changed files with 348 additions and 359 deletions

View File

@@ -148,19 +148,23 @@ class MainActivity : AppCompatActivity() {
val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>() val eventTypeStats = mutableMapOf<LunaEvent.Type, Int>()
if (dynamicMenu) { if (dynamicMenu) {
val sampleSize = 100 // populate frequency map from all events of the last two weeks
// populate frequency map from first 100 events val lastWeekTime = (System.currentTimeMillis() / 1000) - (14 * 24 * 60 * 60)
allEvents.take(sampleSize.coerceAtMost(allEvents.size)).forEach { allEvents.forEach {
if (it.time > lastWeekTime) {
eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0) eventTypeStats[it.type] = 1 + (eventTypeStats[it.type] ?: 0)
} }
} }
}
// sort all event types by frequency or ordinal // sort all event types by frequency and ordinal
val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith( val eventTypesSorted = LunaEvent.Type.entries.toList().sortedWith(
compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal }) compareBy({ -1 * (eventTypeStats[it] ?: 0) }, { it.ordinal })
).filter { it != LunaEvent.Type.UNKNOWN } ).filter { it != LunaEvent.Type.UNKNOWN }
fun setupMenu(maxButtonCount: Int, sortedEventTypes: List<LunaEvent.Type>): Int { val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val row1 = findViewById<View>(R.id.linear_layout_row1) val row1 = findViewById<View>(R.id.linear_layout_row1)
val row1Button1 = findViewById<TextView>(R.id.button1_row1) val row1Button1 = findViewById<TextView>(R.id.button1_row1)
val row1Button2 = findViewById<TextView>(R.id.button2_row1) val row1Button2 = findViewById<TextView>(R.id.button2_row1)
@@ -186,7 +190,7 @@ class MainActivity : AppCompatActivity() {
fun show(vararg tvs: TextView) { fun show(vararg tvs: TextView) {
for (tv in tvs) { for (tv in tvs) {
val type = sortedEventTypes[showCounter] val type = eventTypesSorted[showCounter]
tv.text = LunaEvent.getHeaderEmoji(applicationContext, type) tv.text = LunaEvent.getHeaderEmoji(applicationContext, type)
tv.setOnClickListener { showCreateDialog(type) } tv.setOnClickListener { showCreateDialog(type) }
tv.visibility = View.VISIBLE tv.visibility = View.VISIBLE
@@ -205,15 +209,8 @@ class MainActivity : AppCompatActivity() {
else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2) else -> show(row1Button1, row1Button2, row2Button1, row2Button2, row2Button3, row3Button1, row3Button2)
} }
return showCounter
}
val usedEventCount = eventTypeStats.count { it.value > 0 }
val maxButtonCount = if (dynamicMenu) { usedEventCount } else { 7 }
val eventsShown = setupMenu(maxButtonCount, eventTypesSorted)
// store left over events for popup menu // store left over events for popup menu
currentPopupItems = eventTypesSorted.subList(eventsShown, eventTypesSorted.size) currentPopupItems = eventTypesSorted.subList(showCounter, eventTypesSorted.size)
} }
override fun onStart() { override fun onStart() {
@@ -275,8 +272,11 @@ class MainActivity : AppCompatActivity() {
numberPicker.wrapSelectorWheel = false numberPicker.wrapSelectorWheel = false
numberPicker.value = event.quantity / 10 numberPicker.value = event.quantity / 10
val numberPickerUnit = dialogView.findViewById<TextView>(R.id.dialog_number_picker_unit)
numberPickerUnit.text = NumericUtils(this).measurement_unit_liquid_base
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
@@ -314,7 +314,7 @@ class MainActivity : AppCompatActivity() {
weightET.setText(event.quantity.toString()) weightET.setText(event.quantity.toString())
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
@@ -365,7 +365,7 @@ class MainActivity : AppCompatActivity() {
} }
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -389,7 +389,8 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar { // Pick a date/time.
fun dateTimePicker(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
dateTextView.text = DateUtils.formatDateTime(time) dateTextView.text = DateUtils.formatDateTime(time)
val dateTime = Calendar.getInstance() val dateTime = Calendar.getInstance()
@@ -420,86 +421,122 @@ class MainActivity : AppCompatActivity() {
return dateTime return dateTime
} }
fun addSleepEvent(event: LunaEvent) { fun addDurationEvent(event: LunaEvent) {
askSleepValue(event, true) { saveEvent(event) } askDurationEvent(event, true) { saveEvent(event) }
} }
fun askSleepValue(event: LunaEvent, hideDurationButtons: Boolean, onPositive: () -> Unit) { fun askDurationEvent(event: LunaEvent, showTime: Boolean, 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))
d.setMessage(event.getDialogMessage(this))
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 datePickerBegin = dialogView.findViewById<TextView>(R.id.dialog_date_picker_begin)
val datePickerEnd = dialogView.findViewById<TextView>(R.id.dialog_date_picker_end)
val dateDelimiter = dialogView.findViewById<TextView>(R.id.dialog_date_range_delimiter)
val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons) 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 durationClearButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_clear)
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)
val currentDurationTextColor = durationTextView.currentTextColor val currentDurationTextColor = durationTextView.currentTextColor
val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger) val invalidDurationTextColor = ContextCompat.getColor(this, R.color.danger)
var duration = event.quantity // in seconds
var durationStart = event.getStartTime()
var durationEnd = event.getEndTime()
fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean { fun isValidTime(timeUnix: Long): Boolean {
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60) return timeUnix in 1..now
} }
val onDateChange = { time: Long -> fun isValidTimeSpan(timeBeginUnix: Long, timeEndUnix: Long): Boolean {
durationTextView.setTextColor(currentDurationTextColor) return (timeBeginUnix <= timeEndUnix) && (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
}
if (duration == 0) { fun updateFields() {
// baby is sleeping datePickerBegin.text = DateUtils.formatDateTime(durationStart)
datePickerEnd.text = DateUtils.formatDateTime(durationEnd)
durationTextView.setTextColor(currentDurationTextColor)
val duration = durationEnd - durationStart
if (duration == 0L) {
// event is ongoing
durationTextView.text = "💤" durationTextView.text = "💤"
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
} else { } else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration.toLong()) durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration)
if (!isValidTime(time, duration)) { if (!isValidTimeSpan(durationStart, durationEnd)) {
durationTextView.setTextColor(invalidDurationTextColor) durationTextView.setTextColor(invalidDurationTextColor)
} }
} dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
} }
val pickedDateTime = datePickerHelper(event.time, datePicker, onDateChange) datePickerBegin.setTextColor(if (isValidTime(durationStart)) { currentDurationTextColor } else { invalidDurationTextColor })
datePickerEnd.setTextColor(if (isValidTime(durationEnd)) { currentDurationTextColor } else { invalidDurationTextColor })
}
onDateChange(pickedDateTime.time.time / 1000) val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { pickedTime: Long ->
durationStart = pickedTime
if (datePickerEnd.visibility == View.GONE) {
durationEnd = pickedTime
}
updateFields()
}
if (hideDurationButtons) { val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { pickedTime: Long ->
durationEnd = pickedTime
updateFields()
}
if (showTime) {
dateDelimiter.visibility = View.GONE
datePickerEnd.visibility = View.GONE
durationTextView.visibility = View.GONE
durationButtons.visibility = View.GONE durationButtons.visibility = View.GONE
d.setMessage(getString(R.string.log_sleep_dialog_description_start)) //d.setMessage("")
} else { } else {
dateDelimiter.visibility = View.VISIBLE
datePickerEnd.visibility = View.VISIBLE
durationTextView.visibility = View.VISIBLE
durationButtons.visibility = View.VISIBLE durationButtons.visibility = View.VISIBLE
d.setMessage(event.getDialogMessage(this)) d.setMessage(event.getDialogMessage(this))
fun adjust(minutes: Int) {
duration += minutes * 60
if (duration < 0) {
duration = 0
}
onDateChange(pickedDateTime.time.time / 1000)
} }
durationMinus5Button.setOnClickListener { adjust(-5) } durationStart = pickedDateTimeBegin.time.time / 1000
durationPlus5Button.setOnClickListener { adjust(5) } durationEnd = pickedDateTimeEnd.time.time / 1000
updateFields()
durationMinus5Button.setOnClickListener {
durationEnd = (durationEnd - 300).coerceAtLeast(durationStart)
updateFields()
}
durationPlus5Button.setOnClickListener {
durationEnd = (durationEnd + 300).coerceAtLeast(durationStart)
updateFields()
}
durationClearButton.setOnClickListener {
durationEnd = durationStart
updateFields()
}
durationNowButton.setOnClickListener { durationNowButton.setOnClickListener {
val now = System.currentTimeMillis() / 1000 durationEnd = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000 updateFields()
if (now > start) {
duration = (now - start).toInt()
duration -= duration % 60 // prevent printing of seconds
onDateChange(pickedDateTime.time.time / 1000)
}
}
} }
d.setPositiveButton(android.R.string.ok) { dialogInterface, i -> d.setPositiveButton(android.R.string.ok) { dialogInterface, i ->
val time = pickedDateTime.time.time / 1000 if (isValidTime(durationStart) && isValidTime(durationEnd) && isValidTimeSpan(durationStart, durationEnd)) {
if (isValidTime(time, duration)) { event.time = durationStart
event.time = time event.quantity = (durationEnd - durationStart).toInt()
event.quantity = duration
onPositive() onPositive()
} else { } else {
Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.toast_date_error, Toast.LENGTH_SHORT).show()
@@ -537,7 +574,7 @@ class MainActivity : AppCompatActivity() {
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)
val pickedTime = datePickerHelper(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -570,7 +607,7 @@ class MainActivity : AppCompatActivity() {
d.setView(dialogView) d.setView(dialogView)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedDateTime = datePickerHelper(event.time, dateTV) val pickedDateTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -605,7 +642,7 @@ class MainActivity : AppCompatActivity() {
val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext) val qtyET = dialogView.findViewById<EditText>(R.id.notes_qty_edittext)
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = datePickerHelper(event.time, dateTV) val pickedTime = dateTimePicker(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
@@ -817,7 +854,7 @@ class MainActivity : AppCompatActivity() {
quantityTextView.text = NumericUtils(this).formatEventQuantity(event) quantityTextView.text = NumericUtils(this).formatEventQuantity(event)
notesTextView.text = event.notes notesTextView.text = event.notes
if (event.type == LunaEvent.Type.SLEEP && event.quantity > 0) { if (event.type == LunaEvent.Type.SLEEP && event.quantity > 0) {
dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity) dateEndTextView.text = DateUtils.formatDateTime(event.getEndTime())
dateEndTextView.visibility = View.VISIBLE dateEndTextView.visibility = View.VISIBLE
} else { } else {
dateEndTextView.visibility = View.GONE dateEndTextView.visibility = View.GONE
@@ -831,7 +868,7 @@ class MainActivity : AppCompatActivity() {
} }
updateValues() updateValues()
datePickerHelper(event.time, dateTextView, { newTime: Long -> dateTimePicker(event.time, dateTextView, { newTime: Long ->
event.time = newTime event.time = newTime
updateValues() updateValues()
}) })
@@ -845,7 +882,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 -> askDurationEvent(event, false, updateValues)
else -> { else -> {
Log.w(TAG, "Unexpected type: ${event.type}") Log.w(TAG, "Unexpected type: ${event.type}")
} }
@@ -875,7 +912,7 @@ class MainActivity : AppCompatActivity() {
val previousEvent = getPreviousSameEvent(event, allEvents) val previousEvent = getPreviousSameEvent(event, allEvents)
if (previousEvent != null) { if (previousEvent != null) {
val emoji = previousEvent.getHeaderEmoji(applicationContext) val emoji = previousEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time) val time = DateUtils.formatTimeDuration(applicationContext, event.getStartTime() - previousEvent.getEndTime())
previousTextView.text = String.format("⬅️ %s %s", emoji, time) previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener { previousTextView.setOnClickListener {
alertDialog.cancel() alertDialog.cancel()
@@ -890,7 +927,7 @@ class MainActivity : AppCompatActivity() {
val nextEvent = getNextSameEvent(event, allEvents) val nextEvent = getNextSameEvent(event, allEvents)
if (nextEvent != null) { if (nextEvent != null) {
val emoji = nextEvent.getHeaderEmoji(applicationContext) val emoji = nextEvent.getHeaderEmoji(applicationContext)
val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time) val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.getStartTime() - event.getEndTime())
nextTextView.text = String.format("%s %s ➡️", time, emoji) nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener { nextTextView.setOnClickListener {
alertDialog.cancel() alertDialog.cancel()
@@ -1272,7 +1309,7 @@ class MainActivity : AppCompatActivity() {
LunaEvent.Type.FOOD -> addNoteEvent(event) LunaEvent.Type.FOOD -> addNoteEvent(event)
LunaEvent.Type.PUKE -> addAmountEvent(event) LunaEvent.Type.PUKE -> addAmountEvent(event)
LunaEvent.Type.BATH -> addPlainEvent(event) LunaEvent.Type.BATH -> addPlainEvent(event)
LunaEvent.Type.SLEEP -> addSleepEvent(event) LunaEvent.Type.SLEEP -> addDurationEvent(event)
LunaEvent.Type.UNKNOWN -> {} // ignore LunaEvent.Type.UNKNOWN -> {} // ignore
} }
} }

View File

@@ -1,5 +1,6 @@
package it.danieleverducci.lunatracker package it.danieleverducci.lunatracker
import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@@ -11,15 +12,19 @@ import android.widget.TextView
import android.widget.Toast 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.animation.ChartAnimator
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
import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.formatter.ValueFormatter
import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider
import com.github.mikephil.charting.listener.OnChartValueSelectedListener 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 it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils import utils.DateUtils
import utils.NumericUtils import utils.NumericUtils
@@ -27,7 +32,6 @@ import java.text.SimpleDateFormat
import java.util.Calendar import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -47,8 +51,7 @@ class StatisticsActivity : AppCompatActivity() {
BOTTLE_SUM, BOTTLE_SUM,
SLEEP_SUM, SLEEP_SUM,
SLEEP_EVENTS, SLEEP_EVENTS,
SLEEP_PATTERN, SLEEP_PATTERN
MEDICINE_EVENTS
} }
enum class TimeRange { enum class TimeRange {
@@ -82,10 +85,12 @@ 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)
@@ -136,84 +141,6 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
fun showMedicineBarGraph(state: GraphState) {
val values = HashMap<String, ArrayList<BarEntry>>()
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) {
ArrayList(List(state.endSpan - state.startSpan + 1) { BarEntry(it.toFloat(), 0F) })
}
array[index].y += 1F
}
/*
Log.d(TAG, "values.size: ${values.size}")
for ((key, value) in values) {
Log.d(TAG, "key: $key, value.size: ${value.size} ,value: ${value.joinToString { it.y.toLong().toString() }}")
}
*/
// make sure legend names are not too long
fun shorten(notes: String): String {
return if (notes.length > 16) {
notes.take(13) + "..."
} else {
notes
}
}
fun chooseColor(notes: String): Int {
return (abs(notes.hashCode()) * 16777215) or (0xFF shl 24)
}
val sets = arrayListOf<IBarDataSet>()
for ((key, value) in values.entries) {
if (key.startsWith("v")) {
val description = shorten(key)
val barDataSet = BarDataSet(value, description)
barDataSet.color = chooseColor(key)
sets.add(barDataSet)
}
}
val data = BarData(sets)
//data.groupBars(0F, 0.2F, 0.1F);
data.setValueTextSize(12f)
data.barWidth = 1F
//data.groupBars(0F, 1F, 1F)
data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String {
return if (value == 0F) {
""
} else {
value.toInt().toString()
}
}
})
barChart.setScaleEnabled(true)
barChart.legend.isEnabled = true
//barChart.xAxis.setLabelCount(min(values.size, 24), false);
//val maxCount = min(maxIndex, 30) // values.size
//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()
}
data class SleepRange(val start: Long, var end: Long) data class SleepRange(val start: Long, var end: Long)
fun toSleepRanges(events: List<LunaEvent>): ArrayList<SleepRange> { fun toSleepRanges(events: List<LunaEvent>): ArrayList<SleepRange> {
@@ -254,15 +181,11 @@ class StatisticsActivity : AppCompatActivity() {
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}")
fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) { fun stackValuePattern(index: Int, spanBegin: Long, spanEnd: Long, begin: Long, end: Long) {
val beginDays = unixToDays(begin) val beginDays = unixToDays(begin)
val endDays = unixToDays(end) val endDays = unixToDays(end)
var mid = begin var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}")
for (i in beginDays..endDays) { for (i in beginDays..endDays) {
// i is the days/weeks/months since unix epoch // i is the days/weeks/months since unix epoch
val dayBegin = daysToUnix(i) val dayBegin = daysToUnix(i)
@@ -275,8 +198,7 @@ class StatisticsActivity : AppCompatActivity() {
assert(sleepBegin <= sleepEnd) assert(sleepBegin <= sleepEnd)
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)}") for (j in iBegin..<iEnd) {
for (j in iBegin..iEnd) {
stack[index][j.toInt()] += 1 stack[index][j.toInt()] += 1
} }
} }
@@ -293,17 +215,14 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix 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) { 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, "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
val duration = sleepEnd - sleepBegin 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) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd) stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -313,13 +232,11 @@ class StatisticsActivity : AppCompatActivity() {
} }
fun mapColor(occurrences: Int, maxOccurrences: Int): Int { fun mapColor(occurrences: Int, maxOccurrences: Int): Int {
//Log.d(TAG, "$occurrences <= $maxOccurrences") // occurrences: number of reported sleeps in a specific time slot
// occurrences: 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)
// map to color // map to color
val q = occurrences.toFloat() / maxOccurrences.toFloat() val q = occurrences.toFloat() / maxOccurrences.toFloat()
val i = q * (SLEEP_PATTERN_COLORS.size - 1).toFloat() val i = q * (SLEEP_PATTERN_COLORS.size - 1).toFloat()
@@ -332,11 +249,10 @@ 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() }}")
val vals = ArrayList<Float>() val vals = ArrayList<Float>()
var prevIndex = -1 // time slice index var prevIndex = -1 // time slot index
var prevValue = -1 // number of entries we have found for time slice var prevValue = -1 // number of entries we have found for time slot
for ((i, v) in dayArray.withIndex()) { for ((i, v) in dayArray.withIndex()) {
if (i == 0) { if (i == 0) {
prevIndex = i prevIndex = i
@@ -354,13 +270,10 @@ class StatisticsActivity : AppCompatActivity() {
allColors.add(mapColor(prevValue, daysWithData)) allColors.add(mapColor(prevValue, daysWithData))
} }
//Log.d(TAG, "Range $index, vals: ${vals.joinToString { it.toInt().toString() }}") //, allColors: ${allColors.joinToString { it.toString() }}") assert(values.size == index)
values.add(BarEntry(values.size.toFloat(), vals.toFloatArray()))
values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
} }
//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) { if (e == null || h == null) {
@@ -384,7 +297,6 @@ class StatisticsActivity : AppCompatActivity() {
val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index) val dayStartUnix = daysToUnix(unixToDays(state.startUnix) + index)
//Log.d(TAG, "startUnix: ${Date(startUnix * 1000)}, x: ${e.x.toInt()}, dayStartUnix: ${Date(dayStartUnix * 1000)}")
val startSeconds = val startSeconds =
SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex) SLEEP_PATTERN_GRANULARITY * value.yVals.sliceArray(0..<h.stackIndex)
.fold(0) { acc, y -> acc + y.toInt() } .fold(0) { acc, y -> acc + y.toInt() }
@@ -397,10 +309,7 @@ class StatisticsActivity : AppCompatActivity() {
format.format((dayStartUnix + startSeconds) * 1000).toString() format.format((dayStartUnix + startSeconds) * 1000).toString()
val endTimeString = val endTimeString =
format.format((dayStartUnix + endSeconds) * 1000).toString() format.format((dayStartUnix + endSeconds) * 1000).toString()
val durationString = NumericUtils(applicationContext).formatEventQuantity( val durationString = DateUtils.formatTimeDuration(applicationContext, durationSeconds.toLong())
LunaEvent.Type.SLEEP,
durationSeconds
)
val daysWithData = val daysWithData =
stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY] stack[e.x.toInt()][startSeconds / SLEEP_PATTERN_GRANULARITY]
@@ -429,24 +338,26 @@ class StatisticsActivity : AppCompatActivity() {
}) })
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.colors = allColors set1.colors = allColors
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.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data)
val data = BarData(set1)
data.setValueTextSize(12f)
val valueCount = min(values.size, 24)
barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() barChart.invalidate()
} }
// Sleep pattern bars that do not use time slots. // Sleep pattern bars that do not use time slots.
// This is useful/nicer for bars that only represent data of a singlur days. // 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)
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)) })
@@ -472,7 +383,8 @@ class StatisticsActivity : AppCompatActivity() {
// update value // update value
val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat()) val newYVals = appendToFloatArray(yVals, awakeDuration.toFloat(), sleepDuration.toFloat())
values[index] = BarEntry(x, newYVals) assert(index == x.toInt())
values[index] = BarEntry(index.toFloat(), newYVals)
} }
for (range in ranges) { for (range in ranges) {
@@ -484,17 +396,14 @@ class StatisticsActivity : AppCompatActivity() {
val endIndex = unixToSpan(endUnix) val endIndex = unixToSpan(endUnix)
var mid = startUnix 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) { 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, "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
val duration = sleepEnd - sleepBegin 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) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd) stackValuePattern(index, spanBegin, spanEnd, sleepBegin, sleepEnd)
@@ -503,16 +412,13 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
val set1 = BarDataSet(values, "")
val data = BarData(set1)
// awake phase color is transparent // awake phase color is transparent
val set1 = BarDataSet(values, "")
set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt()) set1.colors = arrayListOf("#00000000".toColorInt(), "#72d7f5".toColorInt())
set1.setDrawValues(false) // usually too many values set1.setDrawValues(false) // usually too many values
set1.setDrawIcons(false)
set1.isHighlightEnabled = true set1.isHighlightEnabled = true
//barChart.xAxis.setCenterAxisLabels(true)
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) {
@@ -535,7 +441,7 @@ class StatisticsActivity : AppCompatActivity() {
} }
val duration = value.yVals[h.stackIndex].toInt() val duration = value.yVals[h.stackIndex].toInt()
val durationString = NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, duration) val durationString = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
val offsetUnix = spanToUnix(state.startSpan + e.x.toInt()) // start of the time span (day/week/month) 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 startUnix = offsetUnix + value.yVals.sliceArray(0..<h.stackIndex).fold(0) { acc, y -> acc + y.toInt() }
@@ -551,20 +457,36 @@ class StatisticsActivity : AppCompatActivity() {
override fun onNothingSelected() {} override fun onNothingSelected() {}
}) })
set1.setDrawIcons(false) val data = BarData(set1)
barChart.legend.isEnabled = false
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data)
val valueCount = min(values.size, 24)
barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
barChart.invalidate() 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) { 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)}")
for (range in ranges) { for (range in ranges) {
// a sleep event can span to another day // a sleep event can span to another day
@@ -575,18 +497,14 @@ class StatisticsActivity : AppCompatActivity() {
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})")
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)}")
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
val duration = sleepEnd - sleepBegin 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) state.dayCounter.setDaysWithData(sleepBegin, sleepEnd)
@@ -603,7 +521,6 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
if (graphTypeSelection == GraphType.SLEEP_SUM) {
for (index in values.indices) { for (index in values.indices) {
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))
if (daysWithData == 0) { if (daysWithData == 0) {
@@ -612,53 +529,52 @@ class StatisticsActivity : AppCompatActivity() {
values[index].y /= daysWithData values[index].y /= daysWithData
} }
} }
}
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
val data = BarData(set1)
set1.setDrawValues(true) set1.setDrawValues(true)
set1.setDrawIcons(false)
set1.isHighlightEnabled = false set1.isHighlightEnabled = false
val data = BarData(set1)
data.setValueTextSize(12f)
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> value.toInt().toString()
GraphType.SLEEP_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" } val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.SLEEP, value.toInt()) return when (graphTypeSelection) {
GraphType.SLEEP_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.SLEEP_SUM -> {
prefix + DateUtils.formatTimeDuration(applicationContext, value.toLong())
} }
else -> { else -> {
Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection") Log.e(TAG, "unhandled graphTypeSelection $graphTypeSelection")
value.toInt().toString() prefix + value.toInt().toString()
} }
} }
} }
}) })
set1.setDrawIcons(false) val valueCount = min(values.size, 24)
barChart.legend.isEnabled = false barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
barChart.setScaleEnabled(false)
barChart.xAxis.setLabelCount(min(values.size, 24))
data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
barChart.legend.isEnabled = false
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
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)
// 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) {
@@ -669,62 +585,46 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
if (graphTypeSelection == GraphType.BOTTLE_SUM) {
for (index in values.indices) { for (index in values.indices) {
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")
if (daysWithData == 0) { if (daysWithData == 0) {
assert(values[index].y == 0F) assert(values[index].y == 0F)
} else { } else {
values[index].y /= daysWithData values[index].y /= daysWithData
} }
} }
}
val set1 = BarDataSet(values, "") val set1 = BarDataSet(values, "")
set1.setDrawValues(true) set1.setDrawValues(true)
set1.isHighlightEnabled = false set1.isHighlightEnabled = false
//barChart.axisLeft.isSLEEP_PATTERN_GRANULARITYEnabled = true
//barChart.axisLeft.setSLEEP_PATTERN_GRANULARITY(0.8F)
val data = BarData(set1) val data = BarData(set1)
//data.barWidth = 0.3F // 0.85 default // ratio of barWidth to totalWidth. data.setValueTextSize(12f)
//Log.d(TAG, "data.barWidth: ${data.barWidth}")
data.setValueFormatter(object : ValueFormatter() { data.setValueFormatter(object : ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
//Log.d(TAG, "getFormattedValue ${dataTypeSelectionValue} ${eventTypeSelectionValue}")
return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> value.toInt().toString()
GraphType.BOTTLE_SUM -> {
val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" } val prefix = if (timeRangeSelection == TimeRange.DAY) { "" } else { "" }
return prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt()) return when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS -> {
prefix + value.toInt().toString()
}
GraphType.BOTTLE_SUM -> {
prefix + NumericUtils(applicationContext).formatEventQuantity(LunaEvent.Type.BABY_BOTTLE, value.toInt())
} }
else -> { else -> {
Log.e(TAG, "unhandled graphTypeSelection") Log.e(TAG, "unhandled graphTypeSelection")
value.toInt().toString() prefix + value.toInt()
} }
} }
} }
}) })
// hm, does not work yet val valueCount = min(values.size, 24)
barChart.renderer = CustomHorizontalBarChartRenderer(barChart, barChart.animator, barChart.viewPortHandler)
data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.moveViewToX(values.lastOrNull()!!.x) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
val maximumRange = 16F barChart.moveViewTo(data.getEntryCount().toFloat(), 0f, YAxis.AxisDependency.LEFT)
//val count = values.size.coerceIn(5, 20)
barChart.setVisibleXRangeMaximum(maximumRange) // show max 24 entries
barChart.xAxis.setLabelCount(maximumRange.toInt(), true)
//barChart.xAxis.isEnabled = false
barChart.xAxis.setCenterAxisLabels(true)
barChart.setScaleEnabled(false)
barChart.invalidate() barChart.invalidate()
} }
@@ -736,10 +636,8 @@ class StatisticsActivity : AppCompatActivity() {
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
@@ -758,8 +656,22 @@ class StatisticsActivity : AppCompatActivity() {
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 comon graph setup fun showGraph() {
fun prepareGraph(type: LunaEvent.Type, callback: (GraphState) -> Unit) { barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
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 } val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) { if (events.isEmpty()) {
@@ -781,7 +693,7 @@ class StatisticsActivity : AppCompatActivity() {
// 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 week val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span
val dayCounter = DayCounter(startDays, endDays) val dayCounter = DayCounter(startDays, endDays)
// print dates // print dates
@@ -797,45 +709,39 @@ class StatisticsActivity : AppCompatActivity() {
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)
// Dirty hack to get monotone number of weeks // Adjust years if the first week of a year starts in the previous year.
// The first week if the year might start in the previous year. val years = if (month == 12 && week == 1) {
val yearFixed = if (month == 12 && week == 1) {
year + 1 year + 1
} else { } else {
year year
} }
val days = "%02d".format(day)
val weeks = "%02d".format(week)
val months = "%02d".format(month)
return when (timeRangeSelection) { return when (timeRangeSelection) {
TimeRange.DAY -> "$day/$month/$yearFixed" TimeRange.DAY -> "$days/$months/$years"
TimeRange.WEEK -> "$week/$yearFixed" TimeRange.WEEK -> "$weeks/$years"
TimeRange.MONTH -> "$month/$yearFixed" TimeRange.MONTH -> "$months/$years"
} }
} }
} }
//Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}") val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan)
callback(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun showGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
when (graphTypeSelection) { when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS, GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) } GraphType.BOTTLE_SUM -> showBottleBarGraph(state)
GraphType.SLEEP_EVENTS, GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) } GraphType.SLEEP_SUM -> showSleepBarGraph(state)
GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) {
if (timeRangeSelection == TimeRange.DAY) { // specialized pattern bar for daily view (optional)
// specialized pattern bar for daily view
showSleepPatternBarGraphDaily(state) showSleepPatternBarGraphDaily(state)
} else { } else {
showSleepPatternBarGraphSlotted(state) showSleepPatternBarGraphSlotted(state)
} }
} }
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
}
} }
private interface SpinnerItemSelected { private interface SpinnerItemSelected {
@@ -854,8 +760,6 @@ class StatisticsActivity : AppCompatActivity() {
val spinnerAdapter = val spinnerAdapter =
ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item) ArrayAdapter.createFromResource(this, arrayId, R.layout.statistics_spinner_item)
//Log.d(TAG, "spinner ${arrayValues.indexOf(currentValue)} (${arrayValues.joinToString { it }})")
spinner.adapter = spinnerAdapter spinner.adapter = spinnerAdapter
spinner.setSelection(arrayValues.indexOf(currentValue)) spinner.setSelection(arrayValues.indexOf(currentValue))
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {

View File

@@ -58,12 +58,7 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
LunaEvent.Type.NOTE -> item.notes LunaEvent.Type.NOTE -> item.notes
else -> item.getRowItemTitle(context) else -> item.getRowItemTitle(context)
} }
val endTime = if (item.type == LunaEvent.Type.SLEEP) { holder.time.text = DateUtils.formatTimeAgo(context, item.getEndTime())
item.quantity + item.time
} else {
item.time
}
holder.time.text = DateUtils.formatTimeAgo(context, endTime)
var quantityText = numericUtils.formatEventQuantity(item) var quantityText = numericUtils.formatEventQuantity(item)
// if the event is weight, show difference with the last one // if the event is weight, show difference with the last one

View File

@@ -115,6 +115,18 @@ class LunaEvent: Comparable<LunaEvent> {
return getDialogMessage(context, type) return getDialogMessage(context, type)
} }
fun getStartTime(): Long {
return time
}
fun getEndTime(): Long {
return if (type == Type.SLEEP) {
time + quantity
} else {
time
}
}
fun toJson(): JSONObject { fun toJson(): JSONObject {
return jo return jo
} }
@@ -162,7 +174,7 @@ class LunaEvent: Comparable<LunaEvent> {
Type.DIAPERCHANGE_PEE, Type.DIAPERCHANGE_PEE,
Type.PUKE -> R.string.log_amount_dialog_description Type.PUKE -> R.string.log_amount_dialog_description
Type.WEIGHT -> R.string.log_weight_dialog_description Type.WEIGHT -> R.string.log_weight_dialog_description
Type.SLEEP -> R.string.log_sleep_dialog_description Type.SLEEP -> R.string.log_duration_dialog_description
else -> R.string.log_unknown_dialog_description else -> R.string.log_unknown_dialog_description
} }
) )

View File

@@ -9,10 +9,15 @@ import java.util.Date
class DateUtils { class DateUtils {
companion object { companion object {
/** /**
* Format time duration in seconds as e.g. "2 hours, 1 min". * Format time duration in seconds as e.g. "2 hours, 1 min", rounded to minutes.
* Used for the duration to the next/previous event in the event details dialog. * Used for the duration to the next/previous event in the event details dialog.
*/ */
fun formatTimeDuration(context: Context, secondsDiff: Long): String { fun formatTimeDuration(context: Context, secondsDiff: Long): String {
val adjusted = (secondsDiff + 30) - (secondsDiff + 30) % 60
return formatTimeDurationExact(context, adjusted)
}
fun formatTimeDurationExact(context: Context, secondsDiff: Long): String {
var seconds = secondsDiff var seconds = secondsDiff
val years = (seconds / (365 * 24 * 60 * 60F)).toLong() val years = (seconds / (365 * 24 * 60 * 60F)).toLong()
@@ -56,11 +61,11 @@ class DateUtils {
return builder.toString() return builder.toString()
} }
if (years > 0) { if (years != 0L) {
return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago) return format(years, days, R.string.year_ago, R.string.years_ago, R.string.day_ago, R.string.days_ago)
} else if (days > 0) { } else if (days != 0L) {
return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago) return format(days, hours, R.string.day_ago, R.string.days_ago, R.string.hour_ago, R.string.hours_ago)
} else if (hours > 0) { } else if (hours != 0L) {
return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago) return format(hours, minutes, R.string.hour_ago, R.string.hours_ago, R.string.minute_ago, R.string.minutes_ago)
} else { } else {
return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago) return format(minutes, seconds, R.string.minute_ago, R.string.minute_ago, R.string.second_ago, R.string.seconds_ago)

View File

@@ -18,6 +18,7 @@
android:layout_height="wrap_content"/> android:layout_height="wrap_content"/>
<TextView <TextView
android:id="@+id/dialog_number_picker_unit"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"

View File

@@ -11,13 +11,14 @@
android:id="@+id/dialog_date_duration" android:id="@+id/dialog_date_duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textSize="20sp" android:textSize="20sp"
android:text="💤"/> android:text="💤"/>
<LinearLayout <LinearLayout
android:id="@+id/duration_buttons" android:id="@+id/duration_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="center" android:gravity="center"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp" android:layout_marginHorizontal="10dp"
@@ -28,29 +29,61 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="-5"/> android:text="@string/dialog_duration_button_minus_5"/>
<Button <Button
android:id="@+id/dialog_date_duration_now" android:id="@+id/dialog_date_duration_now"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="@string/now"/> android:text="@string/dialog_duration_button_now"/>
<Button
android:id="@+id/dialog_date_duration_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/dialog_duration_button_clear"/>
<Button <Button
android:id="@+id/dialog_date_duration_plus5" android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="+5"/> android:text="@string/dialog_duration_button_plus_5"/>
</LinearLayout> </LinearLayout>
<LinearLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<TextView <TextView
android:id="@+id/dialog_date_picker" android:id="@+id/dialog_date_picker_begin"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:gravity="center"
android:layout_marginTop="20dp"/> android:layout_weight="1"/>
<TextView
android:id="@+id/dialog_date_range_delimiter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"
android:text=""/>
<TextView
android:id="@+id/dialog_date_picker_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -13,7 +13,6 @@
<item>@string/statistics_sleep_sum</item> <item>@string/statistics_sleep_sum</item>
<item>@string/statistics_sleep_events</item> <item>@string/statistics_sleep_events</item>
<item>@string/statistics_sleep_pattern</item> <item>@string/statistics_sleep_pattern</item>
<item>@string/statistics_medicine_events</item>
</string-array> </string-array>
<string-array name="StatisticsTypeValues"> <string-array name="StatisticsTypeValues">
@@ -22,7 +21,6 @@
<item>SLEEP_SUM</item> <item>SLEEP_SUM</item>
<item>SLEEP_EVENTS</item> <item>SLEEP_EVENTS</item>
<item>SLEEP_PATTERN</item> <item>SLEEP_PATTERN</item>
<item>MEDICINE_EVENTS</item>
</string-array> </string-array>
<string-array name="StatisticsTimeLabels"> <string-array name="StatisticsTimeLabels">

View File

@@ -130,8 +130,7 @@
<string name="log_temperature_dialog_description">Select the temperature:</string> <string name="log_temperature_dialog_description">Select the temperature:</string>
<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_duration_dialog_description">Set 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>
@@ -161,6 +160,11 @@
<string name="dialog_event_detail_notes">Notes</string> <string name="dialog_event_detail_notes">Notes</string>
<string name="dialog_event_detail_signature">by %s</string> <string name="dialog_event_detail_signature">by %s</string>
<string name="dialog_duration_button_clear">Clear</string>
<string name="dialog_duration_button_minus_5">-5 min</string>
<string name="dialog_duration_button_now">Now</string>
<string name="dialog_duration_button_plus_5">+5 min</string>
<string name="dialog_add_logbook_title">Add logbook</string> <string name="dialog_add_logbook_title">Add logbook</string>
<string name="dialog_add_logbook_logbookname">👶 Logbook name</string> <string name="dialog_add_logbook_logbookname">👶 Logbook name</string>
<string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string> <string name="dialog_add_logbook_message">Write a name to identify this logbook. This name will appear on top of the screen and, if you use WebDAV, will be in the save file name as well.</string>