Skip to content

Commit

Permalink
add day calendar to statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
Razeeman committed Aug 30, 2024
1 parent 769b740 commit 69acbb5
Show file tree
Hide file tree
Showing 15 changed files with 641 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.util.simpletimetracker.core.mapper

import com.example.util.simpletimetracker.core.R
import com.example.util.simpletimetracker.core.extension.setToStartOfDay
import com.example.util.simpletimetracker.core.extension.setWeekToFirstDay
import com.example.util.simpletimetracker.core.extension.shift
import com.example.util.simpletimetracker.domain.provider.CurrentTimestampProvider
Expand Down Expand Up @@ -648,6 +649,18 @@ class TimeMapper @Inject constructor(
}.timeInMillis
}

fun mapFromStartOfDay(
timeStamp: Long,
calendar: Calendar,
): Long {
return calendar.apply {
timeInMillis = timeStamp
setToStartOfDay()
}.let {
timeStamp - it.timeInMillis
}
}

fun getDayOfWeek(
timestamp: Long,
calendar: Calendar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.example.util.simpletimetracker.core.utils

import android.os.Build

// TODO remove
object BuildVersions {

fun isLollipopOrHigher(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.example.util.simpletimetracker.core.utils

object CalendarIntersectionCalculator {

fun <T> execute(
res: List<Data<T>>,
): List<Data<T>> {
// Calculate intersections.
val points: MutableList<Triple<Long, Boolean, Data<T>>> = mutableListOf()
res.forEach { item ->
// Start of range marked with true.
points.add(Triple(item.start, true, item))
points.add(Triple(item.end, false, item))
}

// Sort by range edge (start or end) when put starts first.
points.sortWith(compareBy({ it.first }, { it.second }))
var currentCounter = 0
var currentColumnCount = 1
val freeColumns = mutableListOf(1)

fun calculateColumns(
point: Pair<Int, Triple<Long, Boolean, Data<T>>>,
): Pair<Int, Triple<Long, Boolean, Data<T>>> {
val (counter, triple) = point

// New separate column.
if (counter == 0) {
currentColumnCount = 1
} else if (counter > currentColumnCount) {
currentColumnCount = counter
}
if (currentColumnCount > triple.third.columnCount) {
triple.third.columnCount = currentColumnCount
}

return counter to triple
}

points.map { (time, isStart, item) ->
if (isStart) {
currentCounter++
val columnNumber = freeColumns.minOrNull()!!
item.columnNumber = columnNumber
freeColumns.remove(columnNumber)
if (freeColumns.isEmpty()) freeColumns.add(columnNumber + 1)
} else {
currentCounter--
freeColumns.add(item.columnNumber)
}
currentCounter to Triple(time, isStart, item)
}
// Find max column count and pass it further and back down the list.
.map(::calculateColumns)
.reversed()
.map(::calculateColumns)

return res
}

data class Data<T>(
val start: Long,
val end: Long,
val point: T,
// Set after the fact.
var columnCount: Int = 1,
var columnNumber: Int = 1,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package com.example.util.simpletimetracker.core.view.dayCalendar

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import com.example.util.simpletimetracker.core.utils.CalendarIntersectionCalculator
import com.example.util.simpletimetracker.feature_views.extension.dpToPx
import java.util.concurrent.TimeUnit

class DayCalendarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : View(
context,
attrs,
defStyleAttr,
) {

private var chartLeftBound: Float = 0f
private var chartRightBound: Float = 0f
private var chartTopBound: Float = 0f
private var chartBottomBound: Float = 0f
private var chartHeight: Float = 0f
private var chartWidth: Float = 0f

private val recordCornerRadius: Float = 2.dpToPx().toFloat()
private val dayInMillis = TimeUnit.DAYS.toMillis(1)
private val hourInMillis = TimeUnit.HOURS.toMillis(1)
private val recordPaint: Paint = Paint()
private val recordBounds: RectF = RectF(0f, 0f, 0f, 0f)
private var data: List<Data> = emptyList()

init {
initPaint()
initEditMode()
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val w = resolveSize(0, widthMeasureSpec)
val h = resolveSize(w, heightMeasureSpec)

setMeasuredDimension(w, h)
}

override fun onDraw(canvas: Canvas) {
if (data.isEmpty()) return

val w = width.toFloat()
val h = height.toFloat()

calculateDimensions(w, h)
drawData(canvas, data)
}

fun setData(viewData: DayCalendarViewData) {
data = viewData.data.let(::processData)
invalidate()
}

private fun initPaint() {
recordPaint.apply {
isAntiAlias = true
}
}

private fun calculateDimensions(w: Float, h: Float) {
chartLeftBound = 0f
chartRightBound = w
chartWidth = chartRightBound - chartLeftBound

chartTopBound = 0f
chartBottomBound = h
chartHeight = chartBottomBound - chartTopBound
}

private fun drawData(
canvas: Canvas,
data: List<Data>,
) {
var boxHeight: Float
var boxShift: Float
var boxWidth: Float
var boxLeft: Float
var boxRight: Float
var boxTop: Float
var boxBottom: Float

data.forEach { item ->
recordPaint.color = item.point.data.color
boxHeight = chartHeight / item.columnCount
boxWidth = chartWidth * (item.point.end - item.point.start) / dayInMillis
boxShift = chartWidth * item.point.start / dayInMillis
boxLeft = chartLeftBound + boxShift
boxRight = boxLeft + boxWidth
boxTop = chartTopBound + boxHeight * (item.columnNumber - 1)
boxBottom = boxTop + boxHeight

recordBounds.set(
boxLeft,
boxTop,
boxRight,
boxBottom,
)
canvas.drawRoundRect(
recordBounds,
recordCornerRadius,
recordCornerRadius,
recordPaint,
)
}
}

private fun initEditMode() {
if (isInEditMode) {
var currentStart = 0L
(0 until 5)
.map {
currentStart += hourInMillis * it
val start = currentStart
val end = currentStart + hourInMillis * (it + 1)
DayCalendarViewData.Point(
start = start,
end = end,
data = DayCalendarViewData.Point.Data(
color = Color.RED,
),
)
}.let {
DayCalendarViewData(data = it)
}.let(::setData)
}
}

private fun processData(data: List<DayCalendarViewData.Point>): List<Data> {
val res = mutableListOf<Data>()

// Raw data.
data.forEach { point ->
res += Data(point)
}

// Calculate intersections.
res.map {
CalendarIntersectionCalculator.Data(
start = it.point.start,
end = it.point.end,
point = it,
)
}.let(
CalendarIntersectionCalculator::execute,
).forEach {
it.point.columnCount = it.columnCount
it.point.columnNumber = it.columnNumber
}

return res
}

private inner class Data(
val point: DayCalendarViewData.Point,
// Set after the fact.
var columnCount: Int = 1,
var columnNumber: Int = 1,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.util.simpletimetracker.core.view.dayCalendar

data class DayCalendarViewData(
val data: List<Point>,
) {

data class Point(
val start: Long,
val end: Long,
val data: Data,
) {

data class Data(
val color: Int,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class StatisticsCategoryInteractor @Inject constructor(
return emptyList()
}

private suspend fun getCategoryRecords(
suspend fun getCategoryRecords(
allRecords: List<RecordBase>,
): Map<Long, List<RecordBase>> {
val recordTypeCategories = recordTypeCategoryInteractor.getAll()
Expand All @@ -66,7 +66,7 @@ class StatisticsCategoryInteractor @Inject constructor(
.filterValues(List<RecordBase>::isNotEmpty)
}

private suspend fun getUncategorized(
suspend fun getUncategorized(
allRecords: List<RecordBase>,
): List<RecordBase> {
val recordTypeCategories = recordTypeCategoryInteractor.getAll().map { it.recordTypeId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class StatisticsTagInteractor @Inject constructor(
return emptyList()
}

private suspend fun getTagRecords(
suspend fun getTagRecords(
allRecords: List<RecordBase>,
): Map<Long, List<RecordBase>> {
val recordTags = recordTagInteractor.getAll().map(RecordTag::id)
Expand All @@ -64,7 +64,7 @@ class StatisticsTagInteractor @Inject constructor(
.filterValues(List<RecordBase>::isNotEmpty)
}

private fun getUntagged(
fun getUntagged(
allRecords: List<RecordBase>,
): List<RecordBase> {
return allRecords.filter { it.tagIds.isEmpty() }
Expand Down
Loading

0 comments on commit 69acbb5

Please sign in to comment.