-
Notifications
You must be signed in to change notification settings - Fork 31
Fix highlight in CombinedChart #687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
1e0c21f to
67e6f7f
Compare
video_2026-01-20_00-05-48.mp4Here is video. Still not see the clicks |
|
OK, the click in the sample works, the click in your app not. That means a deeper debug investigation is needed. |
| xAxisRenderer = XAxisRenderer(viewPortHandler, mXAxis, mLeftAxisTransformer) | ||
|
|
||
| setHighlighter(ChartHighlighter(this)) | ||
| if (highlighter == null) // otherwise it overwrites highlighter from successors |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Paget96 under the hood this was the magic.
I'm not sure what you exactly are using. You have to check, if highlighter is set twice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please share your code ...
Otherwise you are alone
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.paget96.batteryguru.R
import com.paget96.batteryguru.ui.screens.batteryhealth.BatteryHealthTimeRange
import com.paget96.batteryguru.ui.screens.batteryhealth.SessionData
import com.paget96.batteryguru.ui.theme.extended
import com.paget96.batteryguru.utils.DateUtils
import com.paget96.batteryguru.utils.batteryhealth.BatteryHealth.Companion.MINIMUM_PERCENTAGE_FOR_VERIFIED_ESTIMATION
import info.appdev.charting.animation.Easing
import info.appdev.charting.charts.CombinedChart
import info.appdev.charting.charts.ScatterChart
import info.appdev.charting.components.AxisBase
import info.appdev.charting.components.XAxis
import info.appdev.charting.components.YAxis
import info.appdev.charting.data.CombinedData
import info.appdev.charting.data.Entry
import info.appdev.charting.data.LineData
import info.appdev.charting.data.LineDataSet
import info.appdev.charting.data.ScatterData
import info.appdev.charting.data.ScatterDataSet
import info.appdev.charting.formatter.IAxisValueFormatter
import info.appdev.charting.highlight.Highlight
import info.appdev.charting.interfaces.datasets.IScatterDataSet
import info.appdev.charting.listener.OnChartValueSelectedListener
data class BatteryHistoryChartColors(
val onSurface: Color,
val primary: Color,
val secondaryContainer: Color,
val surfaceContainerHigh: Color,
val error: Color,
val success: Color,
val red: Color,
val green: Color
)
data class BatteryHealthHistoryChartConfiguration(
val showHealthCheckSessions: Boolean = true,
val showChargingSessions: Boolean = false,
val timeRange: BatteryHealthTimeRange = BatteryHealthTimeRange.LAST_30_DAYS
)
@Composable
fun BatteryHealthHistoryChart(
modifier: Modifier = Modifier,
entries: List<SessionData>,
colors: BatteryHistoryChartColors? = null,
chartConfiguration: BatteryHealthHistoryChartConfiguration = BatteryHealthHistoryChartConfiguration(),
onSessionSelected: ((SessionData?) -> Unit)
) {
val chartColors = colors ?: BatteryHistoryChartColors(
onSurface = MaterialTheme.colorScheme.onSurface,
primary = MaterialTheme.colorScheme.primary,
secondaryContainer = MaterialTheme.colorScheme.secondaryContainer,
surfaceContainerHigh = MaterialTheme.colorScheme.surfaceContainerHigh,
error = MaterialTheme.colorScheme.error,
success = MaterialTheme.colorScheme.secondary,
red = MaterialTheme.extended.red,
green = MaterialTheme.extended.green,
)
var loadedInitial by remember { mutableStateOf(false) }
val chartAlpha by animateFloatAsState(
targetValue = if (!loadedInitial) 0.3f else 1f,
animationSpec = tween(400)
)
// run the initial highlight only once per new "entries" payload
var didInitialHighlight by remember(entries) { mutableStateOf(false) }
Box(
modifier = modifier
.fillMaxWidth()
.height(192.dp),
contentAlignment = Alignment.Center
) {
AndroidView(
factory = { context ->
CombinedChart(context).apply {
// basic styling
setNoDataText("")
legend.isEnabled = false
description.isEnabled = false
animateXY(250, 250, Easing.easeInOutCubic)
viewPortHandler.setMaximumScaleX(4f)
viewPortHandler.setMaximumScaleY(4f)
setTouchEnabled(true)
isPinchZoom = true
isScaleXEnabled = true
isScaleYEnabled = false
isDoubleTapToZoomEnabled = false
isAutoScaleMinMax = true
isHighlightPerDragEnabled = true
setExtraOffsets(0f, 16f, 0f, 16f)
drawOrder = mutableListOf(
CombinedChart.DrawOrder.LINE,
CombinedChart.DrawOrder.SCATTER
)
isDrawMarkersEnabled = false
// left axis = percentage
axisLeft.apply {
isDrawAxisLine = false
axisMinimum = 0f
axisMaximum = 125f
setLabelCount(6, true)
//TODO: ENABLE WHEN CHART UPDATES
//isDrawGridLinesBehindDataEnabled = true
gridColor = chartColors.onSurface.copy(alpha = 0.25f).toArgb()
enableGridDashedLine(10f, 5f, 0f)
gridLineWidth = 1f
textColor = chartColors.primary.toArgb()
textSize = 10f
valueFormatter = object : IAxisValueFormatter {
override fun getFormattedValue(
value: Float,
axis: AxisBase?
) = "${value.toInt()}%"
}
}
// right axis unused
axisRight.isEnabled = false
// X axis = date
xAxis.apply {
isDrawAxisLine = false
//TODO: ENABLE WHEN CHART UPDATES
//isDrawGridLinesBehindDataEnabled = true
gridColor = chartColors.onSurface.copy(alpha = 0.25f).toArgb()
enableGridDashedLine(10f, 5f, 0f)
gridLineWidth = 0.5f
position = XAxis.XAxisPosition.BOTTOM
textColor = chartColors.primary.toArgb()
textSize = 10f
yOffset = 20f
setLabelCount(3, false)
valueFormatter = object : IAxisValueFormatter {
override fun getFormattedValue(
value: Float,
axis: AxisBase?
) = DateUtils.convertMsToDate(
context,
value.toLong(),
showTime = false,
showYear = false,
useSeconds = false,
useMilliseconds = false,
useShortDate = true
)
}
}
setOnChartValueSelectedListener(object : OnChartValueSelectedListener {
override fun onValueSelected(entry: Entry, highlight: Highlight) {
onSessionSelected(entry.data as? SessionData)
}
override fun onNothingSelected() {
onSessionSelected(null)
}
})
}
},
update = { chart ->
// 1) sort and split entries
val sorted = entries.sortedBy { it.timestamp }
val verifiedPoints = mutableListOf<Entry>()
val otherPoints = mutableListOf<Entry>()
val cumulativeLine = mutableListOf<Entry>()
var cumulativeCapacitySum = 0f
var cumulativePercentageSum = 0f
sorted.forEach { e ->
val perc = e.percentageScreenOn + e.percentageScreenOff
val cap = e.capacityScreenOn + e.capacityScreenOff
val design = e.batteryDesignCapacity.toFloat()
val estSess = e.estimatedCapacity.toFloat()
if (perc >= MINIMUM_PERCENTAGE_FOR_VERIFIED_ESTIMATION) {
cumulativeCapacitySum += cap
cumulativePercentageSum += perc
val estCum = cumulativeCapacitySum / cumulativePercentageSum * 100f
val sessPct = (estSess / design) * 100f
val cumPct = (estCum / design) * 100f
verifiedPoints.add(Entry(e.timestamp.toFloat(), sessPct, e))
cumulativeLine.add(Entry(e.timestamp.toFloat(), cumPct, e))
} else {
val sessPct = (estSess / design) * 100f
otherPoints.add(Entry(e.timestamp.toFloat(), sessPct, e))
}
}
// 2) build scatter data sets
val scatterDataSets = mutableListOf<IScatterDataSet>()
if (verifiedPoints.isNotEmpty() && chartConfiguration.showHealthCheckSessions) {
scatterDataSets += ScatterDataSet(verifiedPoints, "Verified").apply {
setScatterShape(ScatterChart.ScatterShape.X)
color = chartColors.primary.toArgb()
scatterShapeSize = 8f
isDrawValues = false
}
}
if (otherPoints.isNotEmpty() && chartConfiguration.showChargingSessions) {
scatterDataSets += ScatterDataSet(otherPoints, "Other").apply {
setScatterShape(ScatterChart.ScatterShape.TRIANGLE)
color = chartColors.error.toArgb()
scatterShapeSize = 8f
isDrawValues = false
}
}
val scatterData = ScatterData(scatterDataSets)
// 3) build line data set
val lineData = if (cumulativeLine.isNotEmpty()) {
LineData(LineDataSet(cumulativeLine, "Health (%)").apply {
color = chartColors.primary.toArgb()
axisDependency = YAxis.AxisDependency.LEFT
lineWidth = 2.5f
isDrawCircles = false
isDrawValues = false
lineMode = LineDataSet.Mode.CUBIC_BEZIER
})
} else LineData()
// 4) combine and set
CombinedData().also { cd ->
cd.setData(scatterData)
cd.setData(lineData)
chart.data = cd
if (!didInitialHighlight) {
val combined = chart.data
val scatter = combined?.scatterData
val target = scatter?.dataSets
?.firstOrNull { it.isVisible && it.entryCount > 0 }
if (target != null) {
val lastIdx = target.entryCount - 1
val last = target.getEntryForIndex(lastIdx)
val dsIndexInScatter = scatter.getIndexOfDataSet(target)
val dataIndexInCombined = combined.allData.indexOf(scatter)
if (last != null) {
val hl = Highlight(
last.x,
last.y,
dsIndexInScatter,
dataIndexInCombined
)
chart.post {
chart.highlightValue(hl, true)
chart.moveViewToX(last.x)
}
}
} else {
chart.post {
chart.highlightValue(null, true)
}
onSessionSelected(null)
}
didInitialHighlight = true
}
}
// 5) animate + invalidate
chart.animateXY(250, 250, Easing.easeInOutCubic)
chart.invalidate()
},
modifier = Modifier
.fillMaxSize()
.alpha(chartAlpha)
)
// show “measuring…” until we have any entries
LaunchedEffect(entries) {
if (entries.isNotEmpty())
loadedInitial = true
}
AnimatedVisibility(
visible = !loadedInitial,
enter = fadeIn(tween(400)),
exit = fadeOut(tween(400))
) {
Text(
text = stringResource(R.string.measuring_or_not_enough_data_to_display),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
maxLines = 2,
modifier = Modifier
.align(Alignment.Center)
.padding(dimensionResource(R.dimen.card_inner_padding))
)
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think issue here is difference in X and Y axis value types. As i understand on some is float on some is long.
Seems like the highlighter cannot found anything on X axis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I wrote "Please share your code ..." this means I need something to debug.
I don't do bug searching on code snippets. Mostly it's waste of time and relays on too much assumptions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mhm, the only difference I see now is that bar chart and line chart configuration uses round numbers as 1.0f, 2.0f for X indexes.
For this combined one, it relies on more precise X axis, which is timestamp.
![]()
If you made any changes to be mor estrict, it could be due to this, in simple words, X value don't match.
I have tried to many ways to solve it but no highlight. PIE chart works, line as well, barchart works, only issues are on combined. IN my app, only this combined chart uses timestamp, I'll update you later whether X value is issue.
Everything worked well before moving to v4.x.x, I have run through my commits on my project, changes are straightforward, only conversion to renamed functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have tried many code changes, including making a simple combined chart with data from the app, click on entries don't work at all, X axis values are not the problem. From what I can see, it seems like it act like we click outside of the entry and triggers onNothingSelected(). Need to mention that this is debug build, no code obfuscations.






It needs #665 in advance