diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 026b6df..bfe3c94 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -83,6 +83,9 @@ dependencies { implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + // MPAndroidChart for progress visualization + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + // Room implementation("androidx.room:room-runtime:2.6.1") implementation("androidx.room:room-ktx:2.6.1") diff --git a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt index 416a3d6..ecc25c0 100644 --- a/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt +++ b/app/src/main/java/com/accidentalproductions/tetristats/data/ScoreDao.kt @@ -12,6 +12,9 @@ interface ScoreDao { @Query("SELECT * FROM scores WHERE gameVersion = :gameVersion") fun getScoresForGame(gameVersion: String): LiveData> + @Query("SELECT * FROM scores WHERE gameVersion = :gameVersion ORDER BY dateRecorded ASC") + fun getScoresForGameByDate(gameVersion: String): LiveData> + @Query("SELECT DISTINCT gameVersion FROM scores") fun getGamesWithScores(): LiveData> diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt index 520981c..94a7097 100644 --- a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsFragment.kt @@ -1,5 +1,6 @@ package com.accidentalproductions.tetristats.ui.stats +import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,7 +9,16 @@ import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager +import com.accidentalproductions.tetristats.R import com.accidentalproductions.tetristats.databinding.FragmentStatsBinding +import com.github.mikephil.charting.components.XAxis +import com.github.mikephil.charting.data.Entry +import com.github.mikephil.charting.data.LineData +import com.github.mikephil.charting.data.LineDataSet +import com.github.mikephil.charting.formatter.ValueFormatter +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale class StatsFragment : Fragment() { private var _binding: FragmentStatsBinding? = null @@ -30,6 +40,7 @@ class StatsFragment : Fragment() { setupRecyclerView() setupGameFilter() + setupProgressChart() observeStats() } @@ -52,6 +63,64 @@ class StatsFragment : Fragment() { viewModel.setSelectedGame(selectedGame) } } + + private fun setupProgressChart() { + with(binding.chartProgress) { + description.isEnabled = false + legend.isEnabled = true + setTouchEnabled(true) + setDrawGridBackground(false) + isDragEnabled = true + setScaleEnabled(true) + setPinchZoom(true) + + axisRight.isEnabled = false + + xAxis.position = XAxis.XAxisPosition.BOTTOM + xAxis.granularity = 1f + xAxis.setDrawGridLines(false) + + axisLeft.setDrawGridLines(true) + axisLeft.axisMinimum = 0f + } + } + + private fun updateProgressChart(scores: List, dates: List) { + if (scores.isEmpty()) { + binding.chartProgress.clear() + binding.chartProgress.invalidate() + return + } + + val dataSet = LineDataSet(scores, "Score Progress").apply { + mode = LineDataSet.Mode.CUBIC_BEZIER + color = resources.getColor(R.color.tetris_navy, null) + lineWidth = 2f + setDrawCircles(true) + setCircleColor(resources.getColor(R.color.tetris_navy, null)) + circleRadius = 4f + setDrawValues(false) + highLightColor = Color.rgb(244, 117, 117) + } + + val lineData = LineData(dataSet) + binding.chartProgress.data = lineData + + // Format X-axis labels (dates) + val dateFormat = SimpleDateFormat("MM/dd", Locale.getDefault()) + binding.chartProgress.xAxis.valueFormatter = object : ValueFormatter() { + override fun getFormattedValue(value: Float): String { + val index = value.toInt() + return if (index >= 0 && index < dates.size) { + dateFormat.format(Date(dates[index])) + } else { + "" + } + } + } + + binding.chartProgress.invalidate() + } private fun observeStats() { viewModel.filteredScores.observe(viewLifecycleOwner) { scores -> @@ -65,6 +134,24 @@ class StatsFragment : Fragment() { viewModel.highScore.observe(viewLifecycleOwner) { highScore -> binding.textViewHighScore.text = "%,d".format(highScore) } + + viewModel.scoresByDate.observe(viewLifecycleOwner) { scores -> + // Convert scores to entries for the chart + if (scores.isNotEmpty()) { + val entries = mutableListOf() + val dates = mutableListOf() + + scores.forEachIndexed { index, score -> + entries.add(Entry(index.toFloat(), score.scoreValue.toFloat())) + dates.add(score.dateRecorded) + } + + updateProgressChart(entries, dates) + } else { + binding.chartProgress.clear() + binding.chartProgress.invalidate() + } + } } override fun onDestroyView() { diff --git a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt index dbd3bd7..c656adb 100644 --- a/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt +++ b/app/src/main/java/com/accidentalproductions/tetristats/ui/stats/StatsViewModel.kt @@ -18,6 +18,10 @@ class StatsViewModel(application: Application) : AndroidViewModel(application) { scoreDao.getScoresForGame(game) } + val scoresByDate: LiveData> = _selectedGame.switchMap { game -> + scoreDao.getScoresForGameByDate(game) + } + val averageScore: LiveData = _selectedGame.switchMap { game -> scoreDao.getAverageScore(game) } diff --git a/app/src/main/res/layout/fragment_stats.xml b/app/src/main/res/layout/fragment_stats.xml index 426a01d..2cf4a0b 100644 --- a/app/src/main/res/layout/fragment_stats.xml +++ b/app/src/main/res/layout/fragment_stats.xml @@ -31,6 +31,33 @@ android:inputType="none"/> + + + + + + + + + +