From 3e353baf484524663b21f6a0cbb232634595fc33 Mon Sep 17 00:00:00 2001 From: Guillaume Jacquart Date: Tue, 27 Dec 2022 08:43:14 +0100 Subject: 5422: add time graduations on tracker graph on dashboard. --- .../e/privacycentralapp/common/GraphHolder.kt | 171 ++++++++++++++++----- .../domain/entities/TrackersPeriodicStatistics.kt | 3 +- .../domain/usecases/TrackersStatisticsUseCase.kt | 22 ++- .../features/dashboard/DashboardFragment.kt | 1 + .../features/dashboard/DashboardState.kt | 1 + .../features/dashboard/DashboardViewModel.kt | 1 + 6 files changed, 154 insertions(+), 45 deletions(-) (limited to 'app') diff --git a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt index d7a9dd0..5622806 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/common/GraphHolder.kt @@ -29,15 +29,19 @@ import androidx.core.content.ContextCompat import androidx.core.text.toSpannable import androidx.core.view.isVisible import com.github.mikephil.charting.charts.BarChart +import com.github.mikephil.charting.components.AxisBase import com.github.mikephil.charting.components.MarkerView import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.components.YAxis +import com.github.mikephil.charting.components.YAxis.AxisDependency import com.github.mikephil.charting.data.BarData import com.github.mikephil.charting.data.BarDataSet import com.github.mikephil.charting.data.BarEntry import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.formatter.ValueFormatter import com.github.mikephil.charting.highlight.Highlight import com.github.mikephil.charting.listener.OnChartValueSelectedListener +import com.github.mikephil.charting.renderer.XAxisRenderer import com.github.mikephil.charting.utils.MPPointF import foundation.e.privacycentralapp.R import foundation.e.privacycentralapp.common.extensions.dpToPxF @@ -50,60 +54,145 @@ class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbov } var labels = emptyList() + var graduations: List? = null + private var isHighlighted = false init { - barChart.apply { - description = null - setTouchEnabled(true) - setScaleEnabled(false) + barChart.description = null + barChart.setTouchEnabled(true) + barChart.setScaleEnabled(false) + + barChart.setDrawGridBackground(false) + barChart.setDrawBorders(false) + barChart.axisLeft.isEnabled = false + barChart.axisRight.isEnabled = false + + barChart.legend.isEnabled = false + + if (isMarkerAbove) prepareXAxisDashboardDay() else prepareXAxisMarkersBelow() + + val periodMarker = PeriodMarkerView(context, isMarkerAbove) + periodMarker.chartView = barChart + barChart.marker = periodMarker + + barChart.setOnChartValueSelectedListener(object : OnChartValueSelectedListener { + override fun onValueSelected(e: Entry?, h: Highlight?) { + h?.let { + val index = it.x.toInt() + if (index >= 0 && + index < labels.size && + index < this@GraphHolder.data.size + ) { + val period = labels[index] + val (blocked, leaked) = this@GraphHolder.data[index] + periodMarker.setLabel(period, blocked, leaked) + } + } + isHighlighted = true + } - setDrawGridBackground(false) - setDrawBorders(false) - axisLeft.isEnabled = false - axisRight.isEnabled = false + override fun onNothingSelected() { + isHighlighted = false + } + }) + } - legend.isEnabled = false + private fun prepareXAxisDashboardDay() { + barChart.extraTopOffset = 44f - if (isMarkerAbove) { - extraTopOffset = 44f - } else { - extraBottomOffset = 44f - } + barChart.offsetTopAndBottom(0) + + barChart.setXAxisRenderer(object : XAxisRenderer(barChart.viewPortHandler, barChart.xAxis, barChart.getTransformer(AxisDependency.LEFT)) { + override fun renderAxisLine(c: Canvas) { + mAxisLinePaint.color = mXAxis.axisLineColor + mAxisLinePaint.strokeWidth = mXAxis.axisLineWidth + mAxisLinePaint.pathEffect = mXAxis.axisLineDashPathEffect + + // Top line + c.drawLine( + mViewPortHandler.contentLeft(), + mViewPortHandler.contentTop(), mViewPortHandler.contentRight(), + mViewPortHandler.contentTop(), mAxisLinePaint + ) - offsetTopAndBottom(0) - xAxis.apply { - isEnabled = true - position = XAxis.XAxisPosition.BOTH_SIDED - setDrawGridLines(false) - setDrawLabels(false) - setDrawValueAboveBar(false) + // Bottom line + c.drawLine( + mViewPortHandler.contentLeft(), + mViewPortHandler.contentBottom() - 7.dpToPxF(context), + mViewPortHandler.contentRight(), + mViewPortHandler.contentBottom() - 7.dpToPxF(context), + mAxisLinePaint + ) } - val periodMarker = PeriodMarkerView(context, isMarkerAbove) - periodMarker.chartView = this - marker = periodMarker - - setOnChartValueSelectedListener(object : OnChartValueSelectedListener { - override fun onValueSelected(e: Entry?, h: Highlight?) { - h?.let { - val index = it.x.toInt() - if (index >= 0 && - index < labels.size && - index < this@GraphHolder.data.size - ) { - val period = labels[index] - val (blocked, leaked) = this@GraphHolder.data[index] - periodMarker.setLabel(period, blocked, leaked) - } + override fun renderGridLines(c: Canvas) { + if (!mXAxis.isDrawGridLinesEnabled || !mXAxis.isEnabled) return + val clipRestoreCount = c.save() + c.clipRect(gridClippingRect) + if (mRenderGridLinesBuffer.size != mAxis.mEntryCount * 2) { + mRenderGridLinesBuffer = FloatArray(mXAxis.mEntryCount * 2) + } + val positions = mRenderGridLinesBuffer + run { + var i = 0 + while (i < positions.size) { + positions[i] = mXAxis.mEntries[i / 2] + positions[i + 1] = mXAxis.mEntries[i / 2] + i += 2 } - isHighlighted = true } - override fun onNothingSelected() { - isHighlighted = false + mTrans.pointValuesToPixel(positions) + setupGridPaint() + val gridLinePath = mRenderGridLinesPath + gridLinePath.reset() + var i = 0 + while (i < positions.size) { + val bottomY = if (graduations?.getOrNull(i / 2) != null) 0 else 3 + val x = positions[i] + gridLinePath.moveTo(x, mViewPortHandler.contentBottom() - 7.dpToPxF(context)) + gridLinePath.lineTo(x, mViewPortHandler.contentBottom() - bottomY.dpToPxF(context)) + + c.drawPath(gridLinePath, mGridPaint) + + gridLinePath.reset() + + i += 2 + } + c.restoreToCount(clipRestoreCount) + } + }) + + barChart.setDrawValueAboveBar(false) + barChart.xAxis.apply { + isEnabled = true + position = XAxis.XAxisPosition.BOTTOM + + setDrawGridLines(true) + setDrawLabels(true) + setCenterAxisLabels(false) + setLabelCount(25, true) + + valueFormatter = object : ValueFormatter() { + override fun getAxisLabel(value: Float, axis: AxisBase?): String { + return graduations?.getOrNull(value.toInt() + 1) ?: "" } - }) + } + } + } + + private fun prepareXAxisMarkersBelow() { + barChart.extraBottomOffset = 44f + + barChart.offsetTopAndBottom(0) + barChart.setDrawValueAboveBar(false) + + barChart.xAxis.apply { + isEnabled = true + position = XAxis.XAxisPosition.BOTH_SIDED + setDrawGridLines(false) + setDrawLabels(false) } } diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt index b3a6ade..8ce55dd 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/entities/TrackersPeriodicStatistics.kt @@ -20,5 +20,6 @@ package foundation.e.privacycentralapp.domain.entities data class TrackersPeriodicStatistics( val callsBlockedNLeaked: List>, val periods: List, - val trackersCount: Int + val trackersCount: Int, + val graduations: List? = null ) diff --git a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt index 57ab1a4..5103eb2 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/domain/usecases/TrackersStatisticsUseCase.kt @@ -69,7 +69,8 @@ class TrackersStatisticsUseCase( return TrackersPeriodicStatistics( callsBlockedNLeaked = trackTrackersPrivacyModule.getPastDayTrackersCalls(), periods = buildDayLabels(), - trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount() + trackersCount = trackTrackersPrivacyModule.getPastDayTrackersCount(), + graduations = buildDayGraduations(), ) to trackTrackersPrivacyModule.getTrackersCount() } @@ -83,15 +84,30 @@ class TrackersStatisticsUseCase( fun getDayTrackersCount() = trackTrackersPrivacyModule.getPastDayTrackersCount() + private fun buildDayGraduations(): List { + val formatter = DateTimeFormatter.ofPattern( + resources.getString(R.string.trackers_graph_hours_period_format) + ) + + val periods = mutableListOf() + var end = ZonedDateTime.now() + for (i in 1..24) { + val start = end.truncatedTo(ChronoUnit.HOURS) + periods.add(if (start.hour % 6 == 0) formatter.format(start) else null) + end = start.minus(1, ChronoUnit.MINUTES) + } + return periods.reversed() + } + private fun buildDayLabels(): List { - val formater = DateTimeFormatter.ofPattern( + val formatter = DateTimeFormatter.ofPattern( resources.getString(R.string.trackers_graph_hours_period_format) ) val periods = mutableListOf() var end = ZonedDateTime.now() for (i in 1..24) { val start = end.truncatedTo(ChronoUnit.HOURS) - periods.add("${formater.format(start)} - ${formater.format(end)}") + periods.add("${formatter.format(start)} - ${formatter.format(end)}") end = start.minus(1, ChronoUnit.MINUTES) } return periods.reversed() diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt index 8a0a3d4..0dc24e8 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardFragment.kt @@ -259,6 +259,7 @@ class DashboardFragment : NavToolbarFragment(R.layout.fragment_dashboard) { binding.graphEmpty.isVisible = false state.dayStatistics?.let { graphHolder?.data = it } state.dayLabels?.let { graphHolder?.labels = it } + state.dayGraduations?.let { graphHolder?.graduations = it } binding.graphLegend.text = Html.fromHtml( getString( diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt index 937fa22..0e3521d 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardState.kt @@ -33,4 +33,5 @@ data class DashboardState( val allowedTrackersCount: Int? = null, val dayStatistics: List>? = null, val dayLabels: List? = null, + val dayGraduations: List? = null, ) diff --git a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt index 18b4212..ead01a5 100644 --- a/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt +++ b/app/src/main/java/foundation/e/privacycentralapp/features/dashboard/DashboardViewModel.kt @@ -114,6 +114,7 @@ class DashboardViewModel( s.copy( dayStatistics = dayStatistics.callsBlockedNLeaked, dayLabels = dayStatistics.periods, + dayGraduations = dayStatistics.graduations, leakedTrackersCount = dayStatistics.trackersCount, trackersCount = trackersCount, allowedTrackersCount = nonBlockedTrackersCount -- cgit v1.2.1