From e107a1c51b28a8310f252028d7581821aafbdea5 Mon Sep 17 00:00:00 2001 From: Mahmoud-Ibrahim-750 <104442642+Mahmoud-Ibrahim-750@users.noreply.github.com> Date: Sun, 11 Jun 2023 10:19:33 +0300 Subject: [PATCH 1/3] View Skipped Days In The Bar Chart I noticed that the skipped days appear in the history chart but not in the bar chart so I modified the code adding a piece of code to pass the skipped days data from database to the view and drawing a bar with a slightly transparent color -to let it be different from the main column- accordingly. --- .../habits/show/views/BarCardView.kt | 1 + .../ui/screens/habits/show/views/BarCard.kt | 10 ++++ .../isoron/uhabits/core/ui/views/BarChart.kt | 49 ++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt index fa3122ed3..12ceb49d7 100644 --- a/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt +++ b/uhabits-android/src/main/java/org/isoron/uhabits/activities/habits/show/views/BarCardView.kt @@ -40,6 +40,7 @@ class BarCardView(context: Context, attrs: AttributeSet) : LinearLayout(context, val androidColor = state.theme.color(state.color).toInt() binding.chart.view = BarChart(state.theme, JavaLocalDateFormatter(Locale.US)).apply { series = mutableListOf(state.entries.map { it.value / 1000.0 }) + skippedSeries = mutableListOf(state.skippedEntries.map { it.value / 1.0 }) colors = mutableListOf(theme.color(state.color.paletteIndex)) axis = state.entries.map { it.timestamp.toLocalDate() } } diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt index 5bace9378..c43d605af 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/screens/habits/show/views/BarCard.kt @@ -22,6 +22,7 @@ package org.isoron.uhabits.core.ui.screens.habits.show.views import org.isoron.uhabits.core.models.Entry import org.isoron.uhabits.core.models.Habit import org.isoron.uhabits.core.models.PaletteColor +import org.isoron.uhabits.core.models.countSkippedDays import org.isoron.uhabits.core.models.groupedSum import org.isoron.uhabits.core.preferences.Preferences import org.isoron.uhabits.core.ui.views.Theme @@ -33,6 +34,7 @@ data class BarCardState( val bucketSize: Int, val color: PaletteColor, val entries: List, + val skippedEntries: List, val isNumerical: Boolean, val numericalSpinnerPosition: Int, ) @@ -64,9 +66,17 @@ class BarCardPresenter( firstWeekday = firstWeekday, isNumerical = habit.isNumerical, ) + // get all data from the oldest date till today, then count skipped days, and + // finally group days according to the truncate value (by weeks for example) + val skippedEntries = habit.computedEntries.getByInterval(oldest, today).countSkippedDays( + truncateField = ScoreCardPresenter.getTruncateField(bucketSize), + firstWeekday = firstWeekday + ) + return BarCardState( theme = theme, entries = entries, + skippedEntries = skippedEntries, bucketSize = bucketSize, color = habit.color, isNumerical = habit.isNumerical, diff --git a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt index 493ec68dc..72fde8716 100644 --- a/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt +++ b/uhabits-core/src/jvmMain/java/org/isoron/uhabits/core/ui/views/BarChart.kt @@ -36,6 +36,7 @@ class BarChart( // Data var series = mutableListOf>() + var skippedSeries = mutableListOf>() var colors = mutableListOf() var axis = listOf() override var dataOffset = 0 @@ -83,28 +84,64 @@ class BarChart( dataColumn < 0 || dataColumn >= series[s].size -> 0.0 else -> series[s][dataColumn] } - if (value <= 0) return + val skippedValue = when { + dataColumn < 0 || dataColumn >= series[s].size -> 0.0 + else -> skippedSeries[s][dataColumn] + } + + if (value <= 0 && skippedValue <= 0.0) return // no value to be drawn val perc = value / maxValue + val skipPerc = skippedValue / maxValue val barHeight = round(maxBarHeight * perc) + val skipBarHeight = round(maxBarHeight * skipPerc) val x = barOffset(c, s) val y = height - footerHeight - barHeight + val skipY = y - skipBarHeight canvas.setColor(colors[s]) val r = round(barWidth * 0.15) if (2 * r < barHeight) { + // draw the main column canvas.fillRect(x, y + r, barWidth, barHeight - r) - canvas.fillRect(x + r, y, barWidth - 2 * r, r + 1) - canvas.fillCircle(x + r, y + r, r) - canvas.fillCircle(x + barWidth - r, y + r, r) } else { canvas.fillRect(x, y, barWidth, barHeight) } + + // draw the skipped part + if (skippedValue > 0) { + // make the skipped day square color lighter by the half + canvas.setColor(colors[s].blendWith(theme.cardBackgroundColor, 0.5)) + // if only skipped days are drawn, no separation (r) space is needed anymore + val h = if (value <= 0) skipBarHeight - r + else skipBarHeight + canvas.fillRect(x, skipY + r, barWidth, h) + } + + // draw a small (rect) on top of the column + // draw this before the skipped part to draw the grid above + val rectY: Double + if (skippedValue > 0) { + rectY = skipY + canvas.setColor(colors[s].blendWith(theme.cardBackgroundColor, 0.5)) + } else { + rectY = y + canvas.setColor(colors[s]) + } + canvas.fillRect(x + r, rectY, barWidth - 2 * r, r + 1) + // draw two circles to make the column top rounded on both sides + canvas.fillCircle(x + r, rectY + r, r) + canvas.fillCircle(x + barWidth - r, rectY + r, r) + + // draw the number above the column canvas.setFontSize(theme.smallTextSize) canvas.setTextAlign(TextAlign.CENTER) canvas.setColor(colors[s]) + val textY: Double = if (skippedValue > 0) skipY + else y + val textValue = value + skippedValue canvas.drawText( - value.toShortString(), + textValue.toShortString(), x + barWidth / 2, - y - theme.smallTextSize * 0.80 + textY - theme.smallTextSize * 0.80 ) } From e5516496fc87f0df643dbe7662f192cc2279261d Mon Sep 17 00:00:00 2001 From: Mahmoud-Ibrahim-750 <104442642+Mahmoud-Ibrahim-750@users.noreply.github.com> Date: Tue, 11 Jul 2023 22:31:50 +0300 Subject: [PATCH 2/3] Update BarChartTest.kt Updated the tests to provide the data for skipped entries too. --- .../java/org/isoron/uhabits/core/ui/views/BarChartTest.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt index ff608a49b..0ccb0dd88 100644 --- a/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt +++ b/uhabits-core/src/jvmTest/java/org/isoron/uhabits/core/ui/views/BarChartTest.kt @@ -34,10 +34,12 @@ class BarChartTest { val component = BarChart(theme, fmt) private val axis = (0..100).map { today.minus(it) } private val series1 = listOf(200.0, 0.0, 150.0, 137.0, 0.0, 0.0, 500.0, 30.0, 100.0, 0.0, 300.0) + private val series2 = listOf(10.0, 0.0, 50.0, 0.0, 0.0, 0.0, 100.0, 0.0, 60.0, 0.0, 150.0) init { component.axis = axis component.series.add(series1) + component.skippedSeries.add(series2) component.colors.add(theme.color(8)) } From 0f1fc234da6368c1654f21f25810ff39fe4cfc86 Mon Sep 17 00:00:00 2001 From: Mahmoud-Ibrahim-750 <104442642+Mahmoud-Ibrahim-750@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:02:19 +0200 Subject: [PATCH 3/3] Update the expected image I updated the expected image in the test to take into account the skipped days as well as the completed days. I also changed the test input in BarChartTest.kt to let it better represent possible cases. --- .../assets/test/views/BarChart/base.png | Bin 10193 -> 15279 bytes .../uhabits/core/ui/views/BarChartTest.kt | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uhabits-core/assets/test/views/BarChart/base.png b/uhabits-core/assets/test/views/BarChart/base.png index e800b1c843c69fbdc5ca61bc385f321c45dbd0f6..16e58d5ca66f5d82a7178ca3b74be2c37147152e 100644 GIT binary patch literal 15279 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 XT4p0pggOF?5ITL*_(a|@hp_(uCfesg literal 10193 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 l|NJW39_~#W%jOM8f