From deed522867c99541a0590ce203ecc043b3bbbe8b Mon Sep 17 00:00:00 2001 From: razeeman Date: Sat, 21 Dec 2024 10:48:05 +0300 Subject: [PATCH] add count goals to excess deficit graph on detailed statistics --- .../core/mapper/RecordTypeViewDataMapper.kt | 1 - .../domain/mapper/RangeMapper.kt | 4 - .../StatisticsDetailChartInteractor.kt | 51 ++++++----- .../StatisticsDetailGoalsInteractor.kt | 52 ++++++++--- .../mapper/StatisticsDetailViewDataMapper.kt | 88 +++++++++++-------- .../model/ChartMode.kt | 6 ++ 6 files changed, 127 insertions(+), 75 deletions(-) create mode 100644 features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/model/ChartMode.kt diff --git a/core/src/main/java/com/example/util/simpletimetracker/core/mapper/RecordTypeViewDataMapper.kt b/core/src/main/java/com/example/util/simpletimetracker/core/mapper/RecordTypeViewDataMapper.kt index c359efefc..77191e379 100644 --- a/core/src/main/java/com/example/util/simpletimetracker/core/mapper/RecordTypeViewDataMapper.kt +++ b/core/src/main/java/com/example/util/simpletimetracker/core/mapper/RecordTypeViewDataMapper.kt @@ -184,7 +184,6 @@ class RecordTypeViewDataMapper @Inject constructor( val valueLeft = goalValue - current val isLimit = goal?.subtype == RecordTypeGoal.Subtype.Limit - // TODO GOAL excess for goal count? // TODO GOAL detailed stats, excess graph, count deficit when should have a goal. // TODO GOAL streaks, skip count days when should not have a goal (daily goals). return if (goal != null) { diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/mapper/RangeMapper.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/mapper/RangeMapper.kt index 7a9838564..c84569cb0 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/mapper/RangeMapper.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/mapper/RangeMapper.kt @@ -70,10 +70,6 @@ class RangeMapper @Inject constructor() { return ranges.sumOf(Range::duration) } - fun mapRecordsToDuration(records: List): Long { - return records.sumOf(RecordBase::duration) - } - private fun clampNormalRecordToRange( record: Record, range: Range, diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailChartInteractor.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailChartInteractor.kt index 9bb7a6ac9..324d4725a 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailChartInteractor.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailChartInteractor.kt @@ -25,6 +25,7 @@ import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartB import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartBarDataRange import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartGrouping import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartLength +import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartMode import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartSplitSortMode import com.example.util.simpletimetracker.feature_statistics_detail.viewData.StatisticsDetailChartCompositeViewData import kotlinx.coroutines.Dispatchers @@ -95,6 +96,7 @@ class StatisticsDetailChartInteractor @Inject constructor( val typesOrder = types.map(RecordType::id) val canSplitByActivity = canSplitByActivity(filter) val canComparisonSplitByActivity = canSplitByActivity(compare) + val chartMode = ChartMode.DURATIONS val compositeData = getChartRangeSelectionData( currentChartGrouping = currentChartGrouping, @@ -116,6 +118,7 @@ class StatisticsDetailChartInteractor @Inject constructor( typesOrder = typesOrder, typesMap = typesMap, isDarkTheme = isDarkTheme, + chartMode = chartMode, splitByActivity = splitByActivity && canSplitByActivity, splitSortMode = splitSortMode, ) @@ -130,6 +133,7 @@ class StatisticsDetailChartInteractor @Inject constructor( typesOrder = typesOrder, typesMap = typesMap, isDarkTheme = isDarkTheme, + chartMode = chartMode, splitSortMode = splitSortMode, ) val compareData = getChartData( @@ -138,6 +142,7 @@ class StatisticsDetailChartInteractor @Inject constructor( typesOrder = typesOrder, typesMap = typesMap, isDarkTheme = isDarkTheme, + chartMode = chartMode, splitByActivity = splitByActivity && canComparisonSplitByActivity, splitSortMode = splitSortMode, ) @@ -164,38 +169,20 @@ class StatisticsDetailChartInteractor @Inject constructor( appliedChartGrouping = compositeData.appliedChartGrouping, availableChartLengths = compositeData.availableChartLengths, appliedChartLength = compositeData.appliedChartLength, + chartMode = chartMode, useProportionalMinutes = useProportionalMinutes, showSeconds = showSeconds, isDarkTheme = isDarkTheme, ) } - fun getGoalValue( - goals: List, - appliedChartGrouping: ChartGrouping, - ): Long { - return getGoal( - goals = goals, - appliedChartGrouping = appliedChartGrouping, - ).value * 1000 - } - - fun getGoalSubtype( - goals: List, - appliedChartGrouping: ChartGrouping, - ): RecordTypeGoal.Subtype { - return getGoal( - goals = goals, - appliedChartGrouping = appliedChartGrouping, - )?.subtype ?: RecordTypeGoal.Subtype.Goal - } - fun getChartData( allRecords: List, ranges: List, typesOrder: List, typesMap: Map, isDarkTheme: Boolean, + chartMode: ChartMode, splitByActivity: Boolean, splitSortMode: ChartSplitSortMode, ): List { @@ -203,6 +190,13 @@ class StatisticsDetailChartInteractor @Inject constructor( return ranges.map { ChartBarDataDuration(legend = it.legend, durations = listOf(0L to 0)) } } + fun mapRangesToValue(list: List): Long { + return when (chartMode) { + ChartMode.DURATIONS -> rangeMapper.mapToDuration(list) + ChartMode.COUNTS -> list.size.toLong() + } + } + val unknownColor = colorMapper.toUntrackedColor(isDarkTheme) val records = rangeMapper.getRecordsFromRange( @@ -221,7 +215,7 @@ class StatisticsDetailChartInteractor @Inject constructor( val durations = if (!splitByActivity) { rangeMapper.getRecordsFromRange(records, range) .map { record -> rangeMapper.clampToRange(record, range) } - .let(rangeMapper::mapToDuration) + .let(::mapRangesToValue) .let { listOf(it to 0) } } else { rangeMapper.getRecordsFromRange(records, range) @@ -229,7 +223,7 @@ class StatisticsDetailChartInteractor @Inject constructor( .toList() .map { (id, records) -> val value = records.map { record -> rangeMapper.clampToRange(record, range) } - .let(rangeMapper::mapToDuration) + .let(::mapRangesToValue) value to id } .run { @@ -381,6 +375,7 @@ class StatisticsDetailChartInteractor @Inject constructor( typesOrder: List, typesMap: Map, isDarkTheme: Boolean, + chartMode: ChartMode, splitSortMode: ChartSplitSortMode, ): List { return if (rangeLength != RangeLength.All) { @@ -399,6 +394,7 @@ class StatisticsDetailChartInteractor @Inject constructor( typesMap = typesMap, isDarkTheme = isDarkTheme, splitByActivity = false, + chartMode = chartMode, splitSortMode = splitSortMode, ) } else { @@ -563,6 +559,17 @@ class StatisticsDetailChartInteractor @Inject constructor( filter.getTypeIds().size > 1 } + private fun getGoalValue( + goals: List, + appliedChartGrouping: ChartGrouping, + ): Long { + // Currently only duration goals are on chart. + return getGoal( + goals = goals, + appliedChartGrouping = appliedChartGrouping, + ).value * 1000 + } + private fun getGoal( goals: List, appliedChartGrouping: ChartGrouping, diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailGoalsInteractor.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailGoalsInteractor.kt index 08ff870cc..672118857 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailGoalsInteractor.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailGoalsInteractor.kt @@ -1,5 +1,9 @@ package com.example.util.simpletimetracker.feature_statistics_detail.interactor +import com.example.util.simpletimetracker.domain.extension.getDaily +import com.example.util.simpletimetracker.domain.extension.getMonthly +import com.example.util.simpletimetracker.domain.extension.getWeekly +import com.example.util.simpletimetracker.domain.extension.value import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor import com.example.util.simpletimetracker.domain.model.DayOfWeek @@ -12,6 +16,7 @@ import com.example.util.simpletimetracker.feature_statistics_detail.interactor.S import com.example.util.simpletimetracker.feature_statistics_detail.mapper.StatisticsDetailViewDataMapper import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartGrouping import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartLength +import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartMode import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartSplitSortMode import com.example.util.simpletimetracker.feature_statistics_detail.viewData.StatisticsDetailGoalsCompositeViewData import kotlinx.coroutines.Dispatchers @@ -53,7 +58,15 @@ class StatisticsDetailGoalsInteractor @Inject constructor( firstDayOfWeek = firstDayOfWeek, goals = goals, ) - + val goal = getGoal( + goals = goals, + appliedChartGrouping = compositeData.appliedChartGrouping, + ) + val chartMode = when (goal?.type) { + is RecordTypeGoal.Type.Duration -> ChartMode.DURATIONS + is RecordTypeGoal.Type.Count -> ChartMode.COUNTS + null -> ChartMode.DURATIONS + } val ranges = chartInteractor.getRanges( compositeData = compositeData, rangeLength = rangeLength, @@ -68,6 +81,7 @@ class StatisticsDetailGoalsInteractor @Inject constructor( typesOrder = typesOrder, typesMap = typesMap, isDarkTheme = isDarkTheme, + chartMode = chartMode, splitByActivity = false, splitSortMode = ChartSplitSortMode.ACTIVITY_ORDER, ) @@ -82,16 +96,11 @@ class StatisticsDetailGoalsInteractor @Inject constructor( typesOrder = typesOrder, typesMap = typesMap, isDarkTheme = isDarkTheme, + chartMode = chartMode, splitSortMode = ChartSplitSortMode.ACTIVITY_ORDER, ) - val goalValue = chartInteractor.getGoalValue( - goals = goals, - appliedChartGrouping = compositeData.appliedChartGrouping, - ) - val goalSubtype = chartInteractor.getGoalSubtype( - goals = goals, - appliedChartGrouping = compositeData.appliedChartGrouping, - ) + val goalValue = getGoalValue(goal) + val goalSubtype = goal?.subtype ?: RecordTypeGoal.Subtype.Goal return@withContext StatisticsDetailGoalsCompositeViewData( viewData = statisticsDetailViewDataMapper.mapGoalChartViewData( @@ -113,6 +122,7 @@ class StatisticsDetailGoalsInteractor @Inject constructor( appliedChartGrouping = compositeData.appliedChartGrouping, availableChartLengths = compositeData.availableChartLengths, appliedChartLength = compositeData.appliedChartLength, + chartMode = chartMode, useProportionalMinutes = useProportionalMinutes, showSeconds = showSeconds, isDarkTheme = isDarkTheme, @@ -137,7 +147,7 @@ class StatisticsDetailGoalsInteractor @Inject constructor( ) val availableChartGroupings = mainData.availableChartGroupings - .filter { chartInteractor.getGoalValue(goals, it) != 0L } + .filter { getGoal(goals, it).value != 0L } .takeUnless { it.isEmpty() } ?: listOf(ChartGrouping.DAILY) @@ -149,4 +159,26 @@ class StatisticsDetailGoalsInteractor @Inject constructor( ?: ChartGrouping.DAILY, ) } + + private fun getGoal( + goals: List, + appliedChartGrouping: ChartGrouping, + ): RecordTypeGoal? { + return when (appliedChartGrouping) { + ChartGrouping.DAILY -> goals.getDaily() + ChartGrouping.WEEKLY -> goals.getWeekly() + ChartGrouping.MONTHLY -> goals.getMonthly() + ChartGrouping.YEARLY -> null + } + } + + private fun getGoalValue( + goal: RecordTypeGoal?, + ): Long { + return when (goal?.type) { + is RecordTypeGoal.Type.Duration -> goal.value * 1000 + is RecordTypeGoal.Type.Count -> goal.value + null -> 0L + } + } } \ No newline at end of file diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/mapper/StatisticsDetailViewDataMapper.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/mapper/StatisticsDetailViewDataMapper.kt index 3fb8fd658..c581bbf72 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/mapper/StatisticsDetailViewDataMapper.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/mapper/StatisticsDetailViewDataMapper.kt @@ -32,6 +32,7 @@ import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartB import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartBarDataRange import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartGrouping import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartLength +import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartMode import com.example.util.simpletimetracker.feature_statistics_detail.model.ChartSplitSortMode import com.example.util.simpletimetracker.feature_statistics_detail.model.SplitChartGrouping import com.example.util.simpletimetracker.feature_statistics_detail.viewData.StatisticsDetailCardInternalViewData @@ -224,6 +225,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( appliedChartGrouping: ChartGrouping, availableChartLengths: List, appliedChartLength: ChartLength, + chartMode: ChartMode, useProportionalMinutes: Boolean, showSeconds: Boolean, isDarkTheme: Boolean, @@ -235,6 +237,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( data = data, goal = goalValue, rangeLength = rangeLength, + chartMode = chartMode, showSelectedBarOnStart = true, useSingleColor = !chartIsSplitByActivity, drawRoundCaps = !chartIsSplitByActivity, @@ -243,6 +246,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( data = compareData, goal = compareGoalValue, rangeLength = rangeLength, + chartMode = chartMode, showSelectedBarOnStart = false, useSingleColor = !chartComparisonIsSplitByActivity, drawRoundCaps = !chartComparisonIsSplitByActivity, @@ -254,6 +258,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( showComparison = showComparison, rangeLength = rangeLength, chartGrouping = appliedChartGrouping, + chartMode = chartMode, useProportionalMinutes = useProportionalMinutes, showSeconds = showSeconds, isDarkTheme = isDarkTheme, @@ -466,6 +471,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( showComparison: Boolean, rangeLength: RangeLength, chartGrouping: ChartGrouping, + chartMode: ChartMode, useProportionalMinutes: Boolean, showSeconds: Boolean, isDarkTheme: Boolean, @@ -478,6 +484,19 @@ class StatisticsDetailViewDataMapper @Inject constructor( return data.sumOf { it.durations.map { it.first }.sum() } / data.size } + fun formatInterval( + interval: Long, + ): String { + return when (chartMode) { + ChartMode.DURATIONS -> timeMapper.formatInterval( + interval = interval, + forceSeconds = showSeconds, + useProportionalMinutes = useProportionalMinutes, + ) + ChartMode.COUNTS -> interval.toString() + } + } + val average = getAverage(data) val nonEmptyData = data.filter { it.durations.sumOf { it.first } != 0L } val averageByNonEmpty = getAverage(nonEmptyData) @@ -497,27 +516,14 @@ class StatisticsDetailViewDataMapper @Inject constructor( val rangeAverages = listOf( StatisticsDetailCardInternalViewData( - value = average.let { - timeMapper.formatInterval( - interval = it, - forceSeconds = showSeconds, - useProportionalMinutes = useProportionalMinutes, - ) - }, + value = formatInterval(average), valueChange = mapValueChange( average = average, prevAverage = prevAverage, rangeLength = rangeLength, isDarkTheme = isDarkTheme, ), - secondValue = comparisonAverage - .let { - timeMapper.formatInterval( - interval = it, - forceSeconds = showSeconds, - useProportionalMinutes = useProportionalMinutes, - ) - } + secondValue = formatInterval(comparisonAverage) .let { "($it)" } .takeIf { showComparison } .orEmpty(), @@ -526,27 +532,14 @@ class StatisticsDetailViewDataMapper @Inject constructor( subtitleTextSizeSp = 12, ), StatisticsDetailCardInternalViewData( - value = averageByNonEmpty.let { - timeMapper.formatInterval( - interval = it, - forceSeconds = showSeconds, - useProportionalMinutes = useProportionalMinutes, - ) - }, + value = formatInterval(averageByNonEmpty), valueChange = mapValueChange( average = averageByNonEmpty, prevAverage = prevAverageByNonEmpty, rangeLength = rangeLength, isDarkTheme = isDarkTheme, ), - secondValue = comparisonAverageByNonEmpty - .let { - timeMapper.formatInterval( - interval = it, - forceSeconds = showSeconds, - useProportionalMinutes = useProportionalMinutes, - ) - } + secondValue = formatInterval(comparisonAverageByNonEmpty) .let { "($it)" } .takeIf { showComparison } .orEmpty(), @@ -628,17 +621,28 @@ class StatisticsDetailViewDataMapper @Inject constructor( data: List, goal: Long, rangeLength: RangeLength, + chartMode: ChartMode, showSelectedBarOnStart: Boolean, useSingleColor: Boolean, drawRoundCaps: Boolean, ): StatisticsDetailChartViewData { - val (legendSuffix, isMinutes) = mapLegendSuffix(data) + val (legendSuffix, isMinutes) = when (chartMode) { + ChartMode.DURATIONS -> mapLegendSuffix(data) + ChartMode.COUNTS -> "" to false + } + + fun formatInterval(interval: Long): Float { + return when (chartMode) { + ChartMode.DURATIONS -> formatInterval(interval, isMinutes) + ChartMode.COUNTS -> interval.toFloat() + } + } return StatisticsDetailChartViewData( visible = data.size > 1, data = data.map { val value = it.durations.map { (duration, color) -> - formatInterval(duration, isMinutes) to color + formatInterval(duration) to color } BarChartView.ViewData( value = value, @@ -658,7 +662,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( -> data.size <= 10 }, showSelectedBarOnStart = showSelectedBarOnStart, - goalValue = formatInterval(goal, isMinutes = isMinutes), + goalValue = formatInterval(goal), useSingleColor = useSingleColor, drawRoundCaps = drawRoundCaps, animate = OneShotValue(true), @@ -705,6 +709,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( appliedChartGrouping: ChartGrouping, availableChartLengths: List, appliedChartLength: ChartLength, + chartMode: ChartMode, useProportionalMinutes: Boolean, showSeconds: Boolean, isDarkTheme: Boolean, @@ -717,6 +722,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( data = goalData, goal = 0, // Don't show goal on goal graph. rangeLength = rangeLength, + chartMode = chartMode, showSelectedBarOnStart = true, useSingleColor = false, drawRoundCaps = true, @@ -728,6 +734,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( showComparison = false, rangeLength = rangeLength, chartGrouping = appliedChartGrouping, + chartMode = chartMode, useProportionalMinutes = useProportionalMinutes, showSeconds = showSeconds, isDarkTheme = isDarkTheme, @@ -742,6 +749,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( ) val goalTotals = mapGoalExcessDeficitTotals( goalData = goalData, + chartMode = chartMode, useProportionalMinutes = useProportionalMinutes, showSeconds = showSeconds, ) @@ -869,6 +877,7 @@ class StatisticsDetailViewDataMapper @Inject constructor( private fun mapGoalExcessDeficitTotals( goalData: List, + chartMode: ChartMode, useProportionalMinutes: Boolean, showSeconds: Boolean, ): List { @@ -880,11 +889,14 @@ class StatisticsDetailViewDataMapper @Inject constructor( fun formatInterval( interval: Long, ): String { - return timeMapper.formatInterval( - interval = interval, - forceSeconds = showSeconds, - useProportionalMinutes = useProportionalMinutes, - ) + return when (chartMode) { + ChartMode.DURATIONS -> timeMapper.formatInterval( + interval = interval, + forceSeconds = showSeconds, + useProportionalMinutes = useProportionalMinutes, + ) + ChartMode.COUNTS -> interval.toString() + } } return listOf( diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/model/ChartMode.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/model/ChartMode.kt new file mode 100644 index 000000000..0060c7a01 --- /dev/null +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/model/ChartMode.kt @@ -0,0 +1,6 @@ +package com.example.util.simpletimetracker.feature_statistics_detail.model + +enum class ChartMode { + DURATIONS, + COUNTS, +} \ No newline at end of file