7 Commits

Author SHA1 Message Date
a4b5ae7cd0 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-01 23:00:52 +01:00
bed3350113 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-01 23:00:52 +01:00
758f37b510 StatisticsActivity: rework all statistics
Improve the overall code.
2026-02-01 23:00:48 +01:00
c8c678b294 NumericUtils: remove possible trailing whitespace 2026-01-23 06:46:11 +01:00
45e798ac3b MainActivity: do not switch logbook on reload 2026-01-23 06:46:11 +01:00
a06264091b LunaEvent: reorganize event text getters
Use method names that better reflect
the use of the returned text.
2026-01-23 06:46:11 +01:00
1e82c94d83 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-01-23 06:46:11 +01:00
8 changed files with 298 additions and 122 deletions

View File

@@ -155,7 +155,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
// sort all event types by frequency and ordinal // sort all event types by frequency or 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 }
@@ -276,7 +276,7 @@ class MainActivity : AppCompatActivity() {
numberPicker.value = event.quantity / 10 numberPicker.value = event.quantity / 10
val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker) val dateTV = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val pickedTime = dateTimePicker(event.time, dateTV) val pickedTime = datePickerHelper(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 = dateTimePicker(event.time, dateTV) val pickedTime = datePickerHelper(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 = dateTimePicker(event.time, dateTV) val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -389,8 +389,7 @@ class MainActivity : AppCompatActivity() {
alertDialog.show() alertDialog.show()
} }
// Pick a date/time. fun datePickerHelper(time: Long, dateTextView: TextView, onChange: (Long) -> Unit = {}): Calendar {
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()
@@ -425,79 +424,82 @@ class MainActivity : AppCompatActivity() {
askSleepValue(event, true) { saveEvent(event) } askSleepValue(event, true) { saveEvent(event) }
} }
fun askSleepValue(event: LunaEvent, showTime: Boolean, onPositive: () -> Unit) { fun askSleepValue(event: LunaEvent, hideDurationButtons: 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 datePickerBegin = dialogView.findViewById<TextView>(R.id.dialog_date_picker_begin) val datePicker = dialogView.findViewById<TextView>(R.id.dialog_date_picker)
val datePickerEnd = dialogView.findViewById<TextView>(R.id.dialog_date_picker_end) val durationButtons = dialogView.findViewById<LinearLayout>(R.id.duration_buttons)
val durationNowButton = dialogView.findViewById<Button>(R.id.dialog_date_duration_now)
val durationMinus5Button = dialogView.findViewById<Button>(R.id.dialog_date_duration_minus5)
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)
// in seconds var duration = event.quantity
var sleepStart = event.time
var sleepEnd = event.time + event.quantity
fun isValidTimeSpan(timeBeginUnix: Long, timeEndUnix: Long): Boolean { fun isValidTime(timeSeconds: Long, durationSeconds: Int): Boolean {
val now = System.currentTimeMillis() / 1000 val now = System.currentTimeMillis() / 1000
return (timeBeginUnix > 0) return (timeSeconds + durationSeconds) <= now && durationSeconds < (24 * 60 * 60)
&& (timeEndUnix > 0)
&& (timeBeginUnix <= timeEndUnix)
&& (timeBeginUnix <= now)
&& (timeEndUnix <= now)
&& (timeEndUnix - timeBeginUnix) < (24 * 60 * 60)
} }
// prevent printing of seconds val onDateChange = { time: Long ->
fun adjustToMinute(unixTime: Long): Long {
return unixTime - (unixTime % 60)
}
fun updateDuration() {
durationTextView.setTextColor(currentDurationTextColor) durationTextView.setTextColor(currentDurationTextColor)
val duration = sleepEnd - sleepStart
if (duration == 0L) { if (duration == 0) {
// baby is sleeping // baby is sleeping
durationTextView.text = "💤" durationTextView.text = "💤"
} else { } else {
durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration) durationTextView.text = DateUtils.formatTimeDuration(applicationContext, duration.toLong())
if (!isValidTimeSpan(sleepStart, sleepEnd)) { if (!isValidTime(time, duration)) {
durationTextView.setTextColor(invalidDurationTextColor) durationTextView.setTextColor(invalidDurationTextColor)
} }
} }
} }
val pickedDateTimeBegin = dateTimePicker(event.time, datePickerBegin) { time: Long -> val pickedDateTime = datePickerHelper(event.time, datePicker, onDateChange)
sleepStart = adjustToMinute(time)
updateDuration()
}
val pickedDateTimeEnd = dateTimePicker(event.time + event.quantity, datePickerEnd) { time: Long -> onDateChange(pickedDateTime.time.time / 1000)
sleepEnd = adjustToMinute(time)
updateDuration()
}
sleepStart = adjustToMinute(pickedDateTimeBegin.time.time / 1000) if (hideDurationButtons) {
sleepEnd = adjustToMinute(pickedDateTimeEnd.time.time / 1000) durationButtons.visibility = View.GONE
updateDuration() d.setMessage(getString(R.string.log_sleep_dialog_description_start))
if (showTime) {
datePickerEnd.visibility = View.GONE
durationTextView.visibility = View.GONE
//d.setMessage("")
} else { } else {
durationTextView.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) }
durationPlus5Button.setOnClickListener { adjust(5) }
durationNowButton.setOnClickListener {
val now = System.currentTimeMillis() / 1000
val start = pickedDateTime.time.time / 1000
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 ->
if (isValidTimeSpan(sleepStart, sleepEnd)) { val time = pickedDateTime.time.time / 1000
event.time = sleepStart if (isValidTime(time, duration)) {
event.quantity = (sleepEnd - sleepStart).toInt() event.time = time
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()
@@ -535,7 +537,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 = dateTimePicker(event.time, dateTV) val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -568,7 +570,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 = dateTimePicker(event.time, dateTV) val pickedDateTime = datePickerHelper(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
} }
@@ -603,7 +605,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 = dateTimePicker(event.time, dateTV) val pickedTime = datePickerHelper(event.time, dateTV)
if (!showTime) { if (!showTime) {
dateTV.visibility = View.GONE dateTV.visibility = View.GONE
@@ -815,7 +817,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.getEndTime()) dateEndTextView.text = DateUtils.formatDateTime(event.time + event.quantity)
dateEndTextView.visibility = View.VISIBLE dateEndTextView.visibility = View.VISIBLE
} else { } else {
dateEndTextView.visibility = View.GONE dateEndTextView.visibility = View.GONE
@@ -829,7 +831,7 @@ class MainActivity : AppCompatActivity() {
} }
updateValues() updateValues()
dateTimePicker(event.time, dateTextView, { newTime: Long -> datePickerHelper(event.time, dateTextView, { newTime: Long ->
event.time = newTime event.time = newTime
updateValues() updateValues()
}) })
@@ -873,7 +875,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.getStartTime() - previousEvent.getEndTime()) val time = DateUtils.formatTimeDuration(applicationContext, event.time - previousEvent.time)
previousTextView.text = String.format("⬅️ %s %s", emoji, time) previousTextView.text = String.format("⬅️ %s %s", emoji, time)
previousTextView.setOnClickListener { previousTextView.setOnClickListener {
alertDialog.cancel() alertDialog.cancel()
@@ -888,7 +890,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.getStartTime() - event.getEndTime()) val time = DateUtils.formatTimeDuration(applicationContext, nextEvent.time - event.time)
nextTextView.text = String.format("%s %s ➡️", time, emoji) nextTextView.text = String.format("%s %s ➡️", time, emoji)
nextTextView.setOnClickListener { nextTextView.setOnClickListener {
alertDialog.cancel() alertDialog.cancel()

View File

@@ -12,12 +12,14 @@ 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
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.listener.OnChartValueSelectedListener import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import it.danieleverducci.lunatracker.entities.LunaEvent import it.danieleverducci.lunatracker.entities.LunaEvent
import utils.DateUtils import utils.DateUtils
@@ -26,6 +28,7 @@ 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
@@ -45,7 +48,8 @@ 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 {
@@ -135,6 +139,124 @@ 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) {
val values = HashMap<String, ArrayList<BarEntry>>()
val days = state.endSpan - state.startSpan + 1
for (event in state.events) {
val index = unixToSpan(event.time) - state.startSpan
val key = event.notes.trim().lowercase()
val array = values.getOrPut(key) {
// create initial array with 0
ArrayList(List(days) { BarEntry(it.toFloat(), 0F) })
}
array[index].y += 1F
}
Log.d(TAG, "values.size: ${values.size}, days: $days")
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, array) in values.entries) {
val description = shorten(key)
Log.d(TAG, "key: $key")
val barDataSet = BarDataSet(array, 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.setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
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
val valueCount = min(days, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false)
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> {
@@ -183,6 +305,7 @@ class StatisticsActivity : AppCompatActivity() {
var mid = begin var mid = begin
//Log.d(TAG, "stackValuePattern: ${beginDays}..${endDays}") //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)
@@ -196,7 +319,7 @@ class StatisticsActivity : AppCompatActivity() {
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
} }
} }
@@ -274,7 +397,7 @@ 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()}") Log.d(TAG, "index: ${index.toFloat()}")
values.add(BarEntry(index.toFloat(), vals.toFloatArray())) values.add(BarEntry(index.toFloat(), vals.toFloatArray()))
} }
@@ -356,14 +479,31 @@ class StatisticsActivity : AppCompatActivity() {
set1.setDrawIcons(false) set1.setDrawIcons(false)
//Log.d(TAG, "showSleepPatternBarGraphSlotted; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}") //barChart.legend.isEnabled = false
/*
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 // does not work quite right yet
//Log.d(TAG, "xChartMax: ${barChart.xChartMax}")
//barChart.centerViewTo(barChart.xChartMax, 0F, YAxis.AxisDependency.LEFT)
barChart.legend.isEnabled = false barChart.legend.isEnabled = false
val valueCount = min(values.size, 24) val valueCount = min(values.size, 24)
@@ -371,6 +511,9 @@ class StatisticsActivity : AppCompatActivity() {
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) barChart.xAxis.setCenterAxisLabels(false)
barChart.invalidate() barChart.invalidate()
//barChart.moveViewToX(77F) //values.lastOrNull()!!.x)
} }
// Sleep pattern bars that do not use time slots. // Sleep pattern bars that do not use time slots.
@@ -439,6 +582,8 @@ class StatisticsActivity : AppCompatActivity() {
set1.setDrawValues(false) // usually too many values set1.setDrawValues(false) // usually too many values
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) {
@@ -476,13 +621,20 @@ class StatisticsActivity : AppCompatActivity() {
override fun onNothingSelected() {} override fun onNothingSelected() {}
}) })
//Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
set1.setDrawIcons(false)
Log.d(TAG, "showSleepPatternBarGraphDaily: values.size: ${values.size}, barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
set1.setDrawIcons(false)
//barChart.legend.isEnabled = false
//val valueCount = 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}") Log.d(TAG, "showSleepPatternBarGraphDaily: new barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}")
barChart.legend.isEnabled = false barChart.legend.isEnabled = false
val valueCount = min(values.size, 24) val valueCount = min(values.size, 24)
@@ -587,6 +739,7 @@ class StatisticsActivity : AppCompatActivity() {
state.dayCounter.setDaysWithData(event.time, event.time) state.dayCounter.setDaysWithData(event.time, event.time)
values[index].x += values.size.toFloat()
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) {
@@ -599,7 +752,7 @@ class StatisticsActivity : AppCompatActivity() {
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, "values[$index].y: ${values[index].y}, daysWithData: $daysWithData") //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 {
@@ -611,7 +764,13 @@ class StatisticsActivity : AppCompatActivity() {
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.
//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 {
@@ -635,11 +794,14 @@ class StatisticsActivity : AppCompatActivity() {
data.setValueTextSize(12f) data.setValueTextSize(12f)
barChart.setData(data) barChart.setData(data)
//barChart.legend.isEnabled = false
val valueCount = min(values.size, 24) val valueCount = min(values.size, 24)
barChart.setVisibleXRangeMaximum(valueCount.toFloat()) barChart.setVisibleXRangeMaximum(valueCount.toFloat())
barChart.xAxis.setLabelCount(valueCount) barChart.xAxis.setLabelCount(valueCount)
barChart.xAxis.setCenterAxisLabels(false) 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) {
@@ -672,25 +834,8 @@ 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)
fun showGraph() { // wrapper for common graph setup
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection") fun prepareGraph(type: LunaEvent.Type, callback: (GraphState) -> Unit) {
barChart.fitScreen()
barChart.data?.clearValues()
barChart.xAxis.valueFormatter = null
barChart.notifyDataSetChanged()
barChart.clear()
barChart.invalidate()
//Log.d(TAG, "resetBarChart; barChart.xAxis.labelCount: ${barChart.xAxis.labelCount}, barChart.visibleXRange: ${barChart.visibleXRange}, barChart.xAxis.isCenterAxisLabelsEnabled: ${barChart.xAxis.isCenterAxisLabelsEnabled}, barChart.isAutoScaleMinMaxEnabled: ${barChart.isAutoScaleMinMaxEnabled}, barChart.isScaleXEnabled: ${barChart.isScaleXEnabled}, barChart.isScaleYEnabled: ${barChart.isScaleYEnabled}")
val type = when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> LunaEvent.Type.BABY_BOTTLE
GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM,
GraphType.SLEEP_PATTERN -> LunaEvent.Type.SLEEP
}
val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time } val events = MainActivity.allEvents.filter { it.type == type }.sortedBy { it.time }
if (events.isEmpty()) { if (events.isEmpty()) {
@@ -715,8 +860,6 @@ class StatisticsActivity : AppCompatActivity() {
val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span val endDays = unixToDays(spanToUnix(endSpan + 1)) // until end of next span
val dayCounter = DayCounter(startDays, endDays) val dayCounter = DayCounter(startDays, endDays)
//Log.d(TAG, "startUnix: ${Date(1000 * startUnix)}, endUnix: ${Date(1000 * endUnix)}, startSpan: ${Date(1000 * spanToUnix(startSpan))}, endSpan: ${Date(1000 * spanToUnix(endSpan))}, startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
// print dates // print dates
barChart.xAxis.valueFormatter = object: ValueFormatter() { barChart.xAxis.valueFormatter = object: ValueFormatter() {
override fun getFormattedValue(value: Float): String { override fun getFormattedValue(value: Float): String {
@@ -730,8 +873,6 @@ 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)
//Log.d(TAG, "index: $index, unixSeconds: ${Date(1000 * unixSeconds)}, day: $day, week: $week, month: $month, year: $year")
// Adjust years if the first week of a year starts in the previous year. // Adjust years if the first week of a year starts in the previous year.
val years = if (month == 12 && week == 1) { val years = if (month == 12 && week == 1) {
year + 1 year + 1
@@ -751,19 +892,32 @@ class StatisticsActivity : AppCompatActivity() {
} }
} }
val state = GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan) //Log.d(TAG, "startDaysUnix: ${Date(daysToUnix(startDays) * 1000)}. endDaysUnix: ${Date(daysToUnix(endDays) * 1000)}")
callback(GraphState(events, dayCounter, startUnix, endUnix, startSpan, endSpan))
}
fun showGraph() {
//Log.d(TAG, "showGraph: graphTypeSelection: $graphTypeSelection, timeRangeSelection: $timeRangeSelection")
// test
resetBarChart()
when (graphTypeSelection) { when (graphTypeSelection) {
GraphType.BOTTLE_EVENTS, GraphType.BOTTLE_EVENTS,
GraphType.BOTTLE_SUM -> showBottleBarGraph(state) GraphType.BOTTLE_SUM -> prepareGraph(LunaEvent.Type.BABY_BOTTLE) { state -> showBottleBarGraph(state) }
GraphType.SLEEP_EVENTS, GraphType.SLEEP_EVENTS,
GraphType.SLEEP_SUM -> showSleepBarGraph(state) GraphType.SLEEP_SUM -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepBarGraph(state) }
GraphType.SLEEP_PATTERN -> if (timeRangeSelection == TimeRange.DAY) { GraphType.SLEEP_PATTERN -> prepareGraph(LunaEvent.Type.SLEEP) { state -> showSleepPatternBarGraphSlotted(state) }
// specialized pattern bar for daily view (optional) /* if (timeRangeSelection == TimeRange.DAY) {
showSleepPatternBarGraphDaily(state) // specialized pattern bar for daily view (optional)
} else { showSleepPatternBarGraphDaily(state)
showSleepPatternBarGraphSlotted(state) } else {
showSleepPatternBarGraphSlotted(state)
}
} }
*/
GraphType.MEDICINE_EVENTS -> prepareGraph(LunaEvent.Type.MEDICINE) { state -> showMedicineBarGraph(state) }
} }
} }

View File

@@ -58,7 +58,12 @@ class LunaEventRecyclerAdapter: RecyclerView.Adapter<LunaEventRecyclerAdapter.Lu
LunaEvent.Type.NOTE -> item.notes LunaEvent.Type.NOTE -> item.notes
else -> item.getRowItemTitle(context) else -> item.getRowItemTitle(context)
} }
holder.time.text = DateUtils.formatTimeAgo(context, item.getEndTime()) val endTime = if (item.type == LunaEvent.Type.SLEEP) {
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,18 +115,6 @@ 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
} }

View File

@@ -56,11 +56,11 @@ class DateUtils {
return builder.toString() return builder.toString()
} }
if (years != 0L) { if (years > 0) {
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 != 0L) { } else if (days > 0) {
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 != 0L) { } else if (hours > 0) {
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

@@ -7,23 +7,47 @@
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical">
<TextView
android:id="@+id/dialog_date_picker_begin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"/>
<TextView <TextView
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="20dp"
android:textSize="20sp" android:textSize="20sp"
android:text="💤"/> android:text="💤"/>
<LinearLayout
android:id="@+id/duration_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="10dp"
android:orientation="horizontal">
<Button
android:id="@+id/dialog_date_duration_minus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="-5"/>
<Button
android:id="@+id/dialog_date_duration_now"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/now"/>
<Button
android:id="@+id/dialog_date_duration_plus5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="+5"/>
</LinearLayout>
<TextView <TextView
android:id="@+id/dialog_date_picker_end" android:id="@+id/dialog_date_picker"
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:layout_gravity="center"

View File

@@ -13,6 +13,7 @@
<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">
@@ -21,6 +22,7 @@
<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

@@ -131,6 +131,7 @@
<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>