/* * Copyright (C) 2021 E FOUNDATION * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package foundation.e.advancedprivacy.common import android.content.Context import android.graphics.Canvas import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.DynamicDrawableSpan import android.text.style.ImageSpan import android.view.View import android.widget.TextView 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.advancedprivacy.R import foundation.e.advancedprivacy.common.extensions.dpToPxF import kotlin.math.floor class GraphHolder(val barChart: BarChart, val context: Context, val isMarkerAbove: Boolean = true) { var data = emptyList>() set(value) { field = value refreshDataSet() } var labels = emptyList() var graduations: List? = null private var isHighlighted = false init { 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 } override fun onNothingSelected() { isHighlighted = false } }) } private fun prepareXAxisDashboardDay() { barChart.extraTopOffset = 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 ) // Bottom line c.drawLine( mViewPortHandler.contentLeft(), mViewPortHandler.contentBottom() - 7.dpToPxF(context), mViewPortHandler.contentRight(), mViewPortHandler.contentBottom() - 7.dpToPxF(context), mAxisLinePaint ) } 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 } } 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) textColor = context.getColor(R.color.primary_text) valueFormatter = object : ValueFormatter() { override fun getAxisLabel(value: Float, axis: AxisBase?): String { return graduations?.getOrNull(floor(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) } } fun highlightIndex(index: Int) { if (index >= 0 && index < data.size) { val xPx = barChart.getTransformer(YAxis.AxisDependency.LEFT) .getPixelForValues(index.toFloat(), 0f) .x val highlight = Highlight( index.toFloat(), 0f, xPx.toFloat(), 0f, 0, YAxis.AxisDependency.LEFT ) barChart.highlightValue(highlight, true) } } private fun refreshDataSet() { val trackersDataSet = BarDataSet( data.mapIndexed { index, value -> BarEntry( index.toFloat(), floatArrayOf(value.first.toFloat(), value.second.toFloat()) ) }, "" ).apply { val blockedColor = ContextCompat.getColor(context, R.color.accent) val leakedColor = ContextCompat.getColor(context, R.color.red_off) colors = listOf( blockedColor, leakedColor ) setDrawValues(false) } barChart.data = BarData(trackersDataSet) barChart.invalidate() } } class PeriodMarkerView(context: Context, private val isMarkerAbove: Boolean = true) : MarkerView(context, R.layout.chart_tooltip) { enum class ArrowPosition { LEFT, CENTER, RIGHT } private val arrowMargins = 10.dpToPxF(context) private val mOffset2 = MPPointF(0f, 0f) private fun getArrowPosition(posX: Float): ArrowPosition { val halfWidth = width / 2 return chartView?.let { chart -> if (posX < halfWidth) { ArrowPosition.LEFT } else if (chart.width - posX < halfWidth) { ArrowPosition.RIGHT } else { ArrowPosition.CENTER } } ?: ArrowPosition.CENTER } private fun showArrow(position: ArrowPosition?) { val ids = listOf( R.id.arrow_top_left, R.id.arrow_top_center, R.id.arrow_top_right, R.id.arrow_bottom_left, R.id.arrow_bottom_center, R.id.arrow_bottom_right ) val toShow = if (isMarkerAbove) when (position) { ArrowPosition.LEFT -> R.id.arrow_bottom_left ArrowPosition.CENTER -> R.id.arrow_bottom_center ArrowPosition.RIGHT -> R.id.arrow_bottom_right else -> null } else when (position) { ArrowPosition.LEFT -> R.id.arrow_top_left ArrowPosition.CENTER -> R.id.arrow_top_center ArrowPosition.RIGHT -> R.id.arrow_top_right else -> null } ids.forEach { id -> val showIt = id == toShow findViewById(id)?.let { if (it.isVisible != showIt) { it.isVisible = showIt } } } } fun setLabel(period: String, blocked: Int, leaked: Int) { val span = SpannableStringBuilder(period) span.append(": $blocked ") span.setSpan( ImageSpan(context, R.drawable.ic_legend_blocked, DynamicDrawableSpan.ALIGN_BASELINE), span.length - 1, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) span.append(" $leaked ") span.setSpan( ImageSpan(context, R.drawable.ic_legend_leaked, DynamicDrawableSpan.ALIGN_BASELINE), span.length - 1, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) findViewById(R.id.label).text = span.toSpannable() } override fun refreshContent(e: Entry?, highlight: Highlight?) { highlight?.let { showArrow(getArrowPosition(highlight.xPx)) } super.refreshContent(e, highlight) } override fun getOffsetForDrawingAtPoint(posX: Float, posY: Float): MPPointF { val x = when (getArrowPosition(posX)) { ArrowPosition.LEFT -> -arrowMargins ArrowPosition.RIGHT -> -width + arrowMargins ArrowPosition.CENTER -> -width.toFloat() / 2 } mOffset2.x = x mOffset2.y = if (isMarkerAbove) -posY else -posY + (chartView?.height?.toFloat() ?: 0f) - height return mOffset2 } override fun draw(canvas: Canvas?, posX: Float, posY: Float) { super.draw(canvas, posX, posY) } }