/*
* 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)
}
}