1.3.0 - Graphing Fixes

This commit is contained in:
2025-08-28 00:18:54 -06:00
parent 1aeded35d1
commit 3245937743
5 changed files with 67 additions and 109 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.openclimb" applicationId = "com.atridad.openclimb"
minSdk = 34 minSdk = 34
targetSdk = 36 targetSdk = 36
versionCode = 18 versionCode = 19
versionName = "1.2.0" versionName = "1.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@@ -31,12 +32,12 @@ data class ChartDataPoint(
* Configuration for chart styling * Configuration for chart styling
*/ */
data class ChartStyle( data class ChartStyle(
val lineColor: Color = Color(0xFF6366F1), val lineColor: Color,
val fillColor: Color = Color(0x336366F1), val fillColor: Color,
val lineWidth: Float = 3f, val lineWidth: Float = 3f,
val gridColor: Color = Color(0xFFE5E7EB), val gridColor: Color,
val textColor: Color = Color(0xFF374151), val textColor: Color,
val backgroundColor: Color = Color.White val backgroundColor: Color
) )
/** /**
@@ -46,7 +47,13 @@ data class ChartStyle(
fun LineChart( fun LineChart(
data: List<ChartDataPoint>, data: List<ChartDataPoint>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
style: ChartStyle = ChartStyle(), style: ChartStyle = ChartStyle(
lineColor = MaterialTheme.colorScheme.primary,
fillColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
gridColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
textColor = MaterialTheme.colorScheme.onSurfaceVariant,
backgroundColor = MaterialTheme.colorScheme.surface
),
showGrid: Boolean = true, showGrid: Boolean = true,
xAxisFormatter: (Float) -> String = { it.toString() }, xAxisFormatter: (Float) -> String = { it.toString() },
yAxisFormatter: (Float) -> String = { it.toString() } yAxisFormatter: (Float) -> String = { it.toString() }
@@ -136,18 +143,27 @@ fun LineChart(
) )
} }
// Draw data points // Draw data points - more pronounced
screenPoints.forEach { point -> screenPoints.forEach { point ->
// Draw outer circle (larger)
drawCircle( drawCircle(
color = style.lineColor, color = style.lineColor,
radius = style.lineWidth * 1.5f, radius = 8f,
center = point center = point
) )
// Draw inner circle (white center)
drawCircle( drawCircle(
color = style.backgroundColor, color = style.backgroundColor,
radius = style.lineWidth * 0.8f, radius = 5f,
center = point center = point
) )
// Draw border for better visibility
drawCircle(
color = style.lineColor,
radius = 8f,
center = point,
style = Stroke(width = 2f)
)
} }
} }
} }
@@ -190,18 +206,7 @@ private fun DrawScope.drawGrid(
strokeWidth = 1.dp.toPx() strokeWidth = 1.dp.toPx()
) )
// Draw label // X-axis labels removed per user request
val text = xAxisFormatter(sessionNum.toFloat())
val textSize = textMeasurer.measure(text, textStyle)
drawText(
textMeasurer = textMeasurer,
text = text,
style = textStyle,
topLeft = Offset(
x - textSize.size.width / 2f,
padding + chartHeight + 8.dp.toPx()
)
)
} }
} }

View File

@@ -543,11 +543,18 @@ fun AddEditProblemScreen(
if (selectedDifficultySystem == DifficultySystem.CUSTOM) { if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField( OutlinedTextField(
value = difficultyGrade, value = difficultyGrade,
onValueChange = { difficultyGrade = it }, onValueChange = { newValue ->
// Only allow integers for custom scales
if (newValue.isEmpty() || newValue.all { it.isDigit() }) {
difficultyGrade = newValue
}
},
label = { Text("Grade *") }, label = { Text("Grade *") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
placeholder = { Text("Enter custom grade") } placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") },
supportingText = { Text("Custom grades must be whole numbers") },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
) )
} else { } else {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }

View File

@@ -456,7 +456,10 @@ fun gradeToNumeric(system: DifficultySystem, grade: String): Int {
else -> 0 else -> 0
} }
} }
DifficultySystem.CUSTOM -> 0 DifficultySystem.CUSTOM -> {
// Custom grades are numeric strings, so parse them directly
grade.toIntOrNull() ?: 0
}
} }
} }

View File

@@ -3,6 +3,7 @@ package com.atridad.openclimb.ui.screens
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -25,6 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@@ -35,7 +37,6 @@ import com.atridad.openclimb.ui.theme.CustomIcons
import com.atridad.openclimb.ui.viewmodel.ClimbViewModel import com.atridad.openclimb.ui.viewmodel.ClimbViewModel
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlin.math.roundToInt
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -292,7 +293,7 @@ fun SessionDetailScreen(
// Show stop icon for active sessions, delete icon for completed sessions // Show stop icon for active sessions, delete icon for completed sessions
if (session?.status == SessionStatus.ACTIVE) { if (session?.status == SessionStatus.ACTIVE) {
IconButton(onClick = { IconButton(onClick = {
session?.let { s -> session.let { s ->
viewModel.endSession(context, s.id) viewModel.endSession(context, s.id)
onNavigateBack() onNavigateBack()
} }
@@ -916,11 +917,6 @@ fun GymDetailScreen(
problems.any { problem -> problem.id == attempt.problemId } problems.any { problem -> problem.id == attempt.problemId }
} }
val successfulAttempts =
gymAttempts.filter { it.result in listOf(AttemptResult.SUCCESS, AttemptResult.FLASH) }
val uniqueProblemsClimbed = gymAttempts.map { it.problemId }.toSet().size val uniqueProblemsClimbed = gymAttempts.map { it.problemId }.toSet().size
val totalSessions = sessions.size val totalSessions = sessions.size
val activeSessions = sessions.count { it.status == SessionStatus.ACTIVE } val activeSessions = sessions.count { it.status == SessionStatus.ACTIVE }
@@ -1556,76 +1552,6 @@ private fun formatDate(dateString: String): String {
} }
} }
/**
* Calculate average grade for a specific set of problems, respecting their difficulty systems
*/
private fun calculateAverageGrade(problems: List<Problem>): String? {
if (problems.isEmpty()) return null
// Group problems by difficulty system
val problemsBySystem = problems.groupBy { it.difficulty.system }
val averages = mutableListOf<String>()
problemsBySystem.forEach { (system, systemProblems) ->
when (system) {
DifficultySystem.V_SCALE -> {
val gradeValues = systemProblems.mapNotNull { problem ->
when {
problem.difficulty.grade == "VB" -> 0
else -> problem.difficulty.grade.removePrefix("V").toIntOrNull()
}
}
if (gradeValues.isNotEmpty()) {
val avg = gradeValues.average().roundToInt()
averages.add(if (avg == 0) "VB" else "V$avg")
}
}
DifficultySystem.FONT -> {
val gradeValues = systemProblems.mapNotNull { problem ->
// Extract numeric part from Font grades (e.g., "6A" -> 6, "7C+" -> 7)
problem.difficulty.grade.filter { it.isDigit() }.toIntOrNull()
}
if (gradeValues.isNotEmpty()) {
val avg = gradeValues.average().roundToInt()
averages.add("$avg")
}
}
DifficultySystem.YDS -> {
val gradeValues = systemProblems.mapNotNull { problem ->
// Extract numeric part from YDS grades (e.g., "5.10a" -> 5.10)
val grade = problem.difficulty.grade
if (grade.startsWith("5.")) {
grade.substring(2).toDoubleOrNull()
} else null
}
if (gradeValues.isNotEmpty()) {
val avg = gradeValues.average()
averages.add("5.${String.format("%.1f", avg)}")
}
}
DifficultySystem.CUSTOM -> {
// For custom systems, try to extract numeric values
val gradeValues = systemProblems.mapNotNull { problem ->
problem.difficulty.grade.filter { it.isDigit() || it == '.' || it == '-' }.toDoubleOrNull()
}
if (gradeValues.isNotEmpty()) {
val avg = gradeValues.average()
averages.add(String.format("%.1f", avg))
}
}
}
}
return if (averages.isNotEmpty()) {
if (averages.size == 1) {
averages.first()
} else {
averages.joinToString(" / ")
}
} else null
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun EnhancedAddAttemptDialog( fun EnhancedAddAttemptDialog(
@@ -1835,8 +1761,12 @@ fun EnhancedAddAttemptDialog(
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
TextButton(onClick = { showCreateProblem = false }) { IconButton(onClick = { showCreateProblem = false }) {
Text("← Back", color = MaterialTheme.colorScheme.primary) Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
@@ -1930,9 +1860,14 @@ fun EnhancedAddAttemptDialog(
if (selectedDifficultySystem == DifficultySystem.CUSTOM) { if (selectedDifficultySystem == DifficultySystem.CUSTOM) {
OutlinedTextField( OutlinedTextField(
value = newProblemGrade, value = newProblemGrade,
onValueChange = { newProblemGrade = it }, onValueChange = { newValue ->
// Only allow integers for custom scales
if (newValue.isEmpty() || newValue.all { it.isDigit() }) {
newProblemGrade = newValue
}
},
label = { Text("Grade *") }, label = { Text("Grade *") },
placeholder = { Text("Enter custom grade") }, placeholder = { Text("Enter numeric grade (e.g. 5, 10, 15)") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
colors = colors =
@@ -1943,6 +1878,7 @@ fun EnhancedAddAttemptDialog(
MaterialTheme.colorScheme.outline MaterialTheme.colorScheme.outline
), ),
isError = newProblemGrade.isBlank(), isError = newProblemGrade.isBlank(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
supportingText = supportingText =
if (newProblemGrade.isBlank()) { if (newProblemGrade.isBlank()) {
{ {
@@ -1951,7 +1887,14 @@ fun EnhancedAddAttemptDialog(
color = MaterialTheme.colorScheme.error color = MaterialTheme.colorScheme.error
) )
} }
} else null } else {
{
Text(
"Custom grades must be whole numbers",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
) )
} else { } else {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }