|
@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) |
|
) |
|
} |