Skip to content

Instantly share code, notes, and snippets.

@brkckr
Last active January 5, 2026 13:48
Show Gist options
  • Select an option

  • Save brkckr/2fa6ffd16ea41c3cdcc6f5649ca3c867 to your computer and use it in GitHub Desktop.

Select an option

Save brkckr/2fa6ffd16ea41c3cdcc6f5649ca3c867 to your computer and use it in GitHub Desktop.
Cheap copy of EuroBasket's Lead Tracker
@Composable
fun LeadTracker(differences: List<Int>) {
// Guard against empty list
if (differences.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No data available", color = Color.Gray)
}
return
}
// Find the maximum absolute difference for symmetric scaling
val maxAbsDifference = remember(differences) {
differences.maxOf { abs(it) }.coerceAtLeast(1) // Avoid division by zero
}
// Round up to the nearest multiple of 10
val maxDifference = remember(maxAbsDifference) {
ceil(maxAbsDifference.toDouble() / 10).toInt() * 10
}
val halfMaxDifference = maxDifference / 2
Column(
modifier = Modifier
.fillMaxWidth()
.height(300.dp)
) {
// Top row: Quarter labels (Q1, Q2, Q3, Q4)
Row(
modifier = Modifier
.weight(2f)
.fillMaxWidth()
) {
Spacer(modifier = Modifier.weight(3f))
Row(
modifier = Modifier
.weight(20f)
.fillMaxHeight(),
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically
) {
listOf("Q1", "Q2", "Q3", "Q4").forEach { quarter ->
Text(
text = quarter,
textAlign = TextAlign.Center,
fontSize = 14.sp,
color = Color(0xFF727F82),
fontWeight = FontWeight.Bold,
modifier = Modifier.weight(1f)
)
}
}
Spacer(modifier = Modifier.weight(2f))
}
// Middle row: Main chart area
Row(
modifier = Modifier
.weight(20f)
.fillMaxWidth()
) {
// Left column: Flag-like circles (green top, blue bottom)
Column(
modifier = Modifier
.weight(3f)
.fillMaxHeight()
.padding(10.dp),
verticalArrangement = Arrangement.SpaceBetween
) {
Circle(color = Color(0xFF39724F)) // Positive direction indicator
Circle(color = Color(0xFF236CAA)) // Negative direction indicator
}
// Center: Bar chart with background grid
Box(modifier = Modifier.weight(20f).fillMaxHeight()) {
// Alternating horizontal background bands
Column(modifier = Modifier.fillMaxSize()) {
Box(modifier = Modifier.weight(1f).fillMaxWidth().background(Color(0xFFE9E9EA)))
Box(modifier = Modifier.weight(1f).fillMaxWidth().background(Color(0xFFF8F8F8)))
Box(modifier = Modifier.height(1.dp).fillMaxWidth().background(Color(0xFFD8D9DA)))
Box(modifier = Modifier.weight(1f).fillMaxWidth().background(Color(0xFFF8F8F8)))
Box(modifier = Modifier.weight(1f).fillMaxWidth().background(Color(0xFFE9E9EA)))
}
// Vertical grid lines (quarter dividers)
Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween) {
repeat(5) {
Box(
modifier = Modifier
.width(1.dp)
.fillMaxHeight()
.background(Color(0xFFD8D9DA))
)
}
}
// Bar chart drawn with Canvas
Canvas(modifier = Modifier.fillMaxSize()) {
val canvasWidth = size.width
val canvasHeight = size.height
val barCount = differences.size
val gapPx = 1.dp.toPx()
val totalGapWidth = gapPx * (barCount - 1)
val barWidth = (canvasWidth - totalGapWidth) / barCount
differences.forEachIndexed { index, value ->
val normalizedHeight = (value.toFloat() / maxDifference) * (canvasHeight / 2)
val barHeight = abs(normalizedHeight)
val x = index * (barWidth + gapPx)
val y = if (value >= 0) {
canvasHeight / 2 - barHeight // Positive: grows upward
} else {
canvasHeight / 2 // Negative: grows downward
}
val barColor = if (value >= 0) Color(0xFF39724F) else Color(0xFF236CAA)
drawRect(
color = barColor,
topLeft = Offset(x, y),
size = Size(barWidth, barHeight)
)
}
}
}
// Right column: Y-axis scale labels
Column(
modifier = Modifier
.weight(2f)
.fillMaxHeight()
.padding(start = 2.dp)
) {
// +max
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.TopStart) {
Text(text = "+$maxDifference", fontSize = 12.sp, color = Color(0xFF727F82))
}
// +half
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.TopStart) {
Text(text = "+$halfMaxDifference", fontSize = 12.sp, color = Color(0xFF727F82))
}
// 0
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
Text(text = "0", fontSize = 12.sp, color = Color(0xFF727F82))
}
// -half
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.BottomStart) {
Text(text = "-$halfMaxDifference", fontSize = 12.sp, color = Color(0xFF727F82))
}
// -max
Box(modifier = Modifier.weight(1f), contentAlignment = Alignment.BottomStart) {
Text(text = "-$maxDifference", fontSize = 14.sp, color = Color(0xFF727F82))
}
}
}
// Bottom row: Reserved for future use (e.g., legend, totals)
Row(modifier = Modifier.weight(2f).fillMaxWidth()) {
// Empty for now – can add summary text or icons later
}
}
}
// Reusable circle component for flag indicators
@Composable
private fun Circle(color: Color) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
.clip(CircleShape)
.background(color)
)
}

EuroBasket Inspired Lead Tracker – Jetpack Compose

A clean, reusable Jetpack Compose component that recreates the iconic EuroBasket lead tracker graphic seen in FIBA game summaries.

Screenshot_20260105_164117

Usage

@Preview
@Composable
fun PreviewLeadTracker() {
    val sampleData = List(40) { Random.nextInt(-15, 15) }
    LeadTracker(differences = sampleData)
}

// In your code
LeadTracker(differences = yourListOfInts)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment