Show list of charts in grade statistics (#689)

This commit is contained in:
Mikołaj Pich 2020-02-29 02:19:48 +01:00 committed by GitHub
parent e61c2bced8
commit be057dd63c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 372 additions and 274 deletions

View File

@ -54,8 +54,10 @@ class GradeStatisticsLocalTest {
)) ))
val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Wszystkie").blockingGet() val stats = gradeStatisticsLocal.getGradesStatistics(getSemester(), false, "Wszystkie").blockingGet()
assertEquals(1, stats.size) assertEquals(3, stats.size)
assertEquals(stats[0].subject, "Wszystkie") assertEquals(stats[0].subject, "Wszystkie")
assertEquals(stats[1].subject, "Matematyka")
assertEquals(stats[2].subject, "Chemia")
} }
@Test @Test
@ -67,7 +69,7 @@ class GradeStatisticsLocalTest {
)) ))
val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet() val stats = gradeStatisticsLocal.getGradesPointsStatistics(getSemester(), "Matematyka").blockingGet()
with(stats) { with(stats[0]) {
assertEquals(subject, "Matematyka") assertEquals(subject, "Matematyka")
assertEquals(others, 5.0) assertEquals(others, 5.0)
assertEquals(student, 5.0) assertEquals(student, 5.0)

View File

@ -11,7 +11,7 @@ import javax.inject.Singleton
interface GradePointsStatisticsDao : BaseDao<GradePointsStatistics> { interface GradePointsStatisticsDao : BaseDao<GradePointsStatistics> {
@Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId AND subject = :subjectName") @Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId AND subject = :subjectName")
fun loadSubject(semesterId: Int, studentId: Int, subjectName: String): Maybe<GradePointsStatistics> fun loadSubject(semesterId: Int, studentId: Int, subjectName: String): Maybe<List<GradePointsStatistics>>
@Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId") @Query("SELECT * FROM GradesPointsStatistics WHERE student_id = :studentId AND semester_id = :semesterId")
fun loadAll(semesterId: Int, studentId: Int): Maybe<List<GradePointsStatistics>> fun loadAll(semesterId: Int, studentId: Int): Maybe<List<GradePointsStatistics>>

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.data.pojos
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
data class GradeStatisticsItem(
val type: ViewType,
val partial: List<GradeStatistics>,
val points: GradePointsStatistics?
)

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.roundToDecimalPlaces
import io.reactivex.Maybe import io.reactivex.Maybe
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -30,23 +29,17 @@ class GradeStatisticsLocal @Inject constructor(
list.groupBy { it.grade }.map { list.groupBy { it.grade }.map {
GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key, GradeStatistics(semester.studentId, semester.semesterId, subjectName, it.key,
it.value.fold(0) { acc, e -> acc + e.amount }, false) it.value.fold(0) { acc, e -> acc + e.amount }, false)
} } + list
} }
else -> gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester) else -> gradeStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName, isSemester)
}.filter { it.isNotEmpty() } }.filter { it.isNotEmpty() }
} }
fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<GradePointsStatistics> { fun getGradesPointsStatistics(semester: Semester, subjectName: String): Maybe<List<GradePointsStatistics>> {
return when (subjectName) { return when (subjectName) {
"Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId).flatMap { list -> "Wszystkie" -> gradePointsStatisticsDb.loadAll(semester.semesterId, semester.studentId)
if (list.isEmpty()) return@flatMap Maybe.empty<GradePointsStatistics>()
Maybe.just(GradePointsStatistics(semester.studentId, semester.semesterId, subjectName,
(list.fold(.0) { acc, e -> acc + e.others } / list.size).roundToDecimalPlaces(2),
(list.fold(.0) { acc, e -> acc + e.student } / list.size).roundToDecimalPlaces(2)
))
}
else -> gradePointsStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName) else -> gradePointsStatisticsDb.loadSubject(semester.semesterId, semester.studentId, subjectName)
} }.filter { it.isNotEmpty() }
} }
fun saveGradesStatistics(gradesStatistics: List<GradeStatistics>) { fun saveGradesStatistics(gradesStatistics: List<GradeStatistics>) {

View File

@ -5,8 +5,9 @@ import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.Inter
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.ui.modules.grade.statistics.ViewType
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import io.reactivex.Maybe
import io.reactivex.Single import io.reactivex.Single
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
@ -19,8 +20,8 @@ class GradeStatisticsRepository @Inject constructor(
private val remote: GradeStatisticsRemote private val remote: GradeStatisticsRemote
) { ) {
fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatistics>> { fun getGradesStatistics(semester: Semester, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false): Single<List<GradeStatisticsItem>> {
return local.getGradesStatistics(semester, isSemester, subjectName).filter { !forceRefresh } return local.getGradesStatistics(semester, isSemester, subjectName).map { it.mapToStatisticItems() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap { .flatMap {
if (it) remote.getGradeStatistics(semester, isSemester) if (it) remote.getGradeStatistics(semester, isSemester)
@ -31,21 +32,43 @@ class GradeStatisticsRepository @Inject constructor(
local.deleteGradesStatistics(old.uniqueSubtract(new)) local.deleteGradesStatistics(old.uniqueSubtract(new))
local.saveGradesStatistics(new.uniqueSubtract(old)) local.saveGradesStatistics(new.uniqueSubtract(old))
} }
}.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).toSingle(emptyList()) }) }.flatMap { local.getGradesStatistics(semester, isSemester, subjectName).map { it.mapToStatisticItems() }.toSingle(emptyList()) })
} }
fun getGradesPointsStatistics(semester: Semester, subjectName: String, forceRefresh: Boolean): Maybe<GradePointsStatistics> { fun getGradesPointsStatistics(semester: Semester, subjectName: String, forceRefresh: Boolean): Single<List<GradeStatisticsItem>> {
return local.getGradesPointsStatistics(semester, subjectName).filter { !forceRefresh } return local.getGradesPointsStatistics(semester, subjectName).map { it.mapToStatisticsItem() }.filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings) .switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMapMaybe { .flatMap {
if (it) remote.getGradePointsStatistics(semester).toMaybe() if (it) remote.getGradePointsStatistics(semester)
else Maybe.error(UnknownHostException()) else Single.error(UnknownHostException())
}.flatMap { new -> }.flatMap { new ->
local.getGradesPointsStatistics(semester).defaultIfEmpty(emptyList()) local.getGradesPointsStatistics(semester).toSingle(emptyList())
.doOnSuccess { old -> .doOnSuccess { old ->
local.deleteGradesPointsStatistics(old.uniqueSubtract(new)) local.deleteGradesPointsStatistics(old.uniqueSubtract(new))
local.saveGradesPointsStatistics(new.uniqueSubtract(old)) local.saveGradesPointsStatistics(new.uniqueSubtract(old))
} }
}.flatMap { local.getGradesPointsStatistics(semester, subjectName) }) }.flatMap { local.getGradesPointsStatistics(semester, subjectName).map { it.mapToStatisticsItem() }.toSingle(emptyList()) })
}
private fun List<GradeStatistics>.mapToStatisticItems(): List<GradeStatisticsItem> {
return groupBy { it.subject }.map {
GradeStatisticsItem(
type = ViewType.PARTIAL,
partial = it.value
.sortedByDescending { item -> item.grade }
.filter { item -> item.amount != 0 },
points = null
)
}
}
private fun List<GradePointsStatistics>.mapToStatisticsItem(): List<GradeStatisticsItem> {
return map {
GradeStatisticsItem(
type = ViewType.POINTS,
partial = emptyList(),
points = it
)
}
} }
} }

View File

@ -0,0 +1,209 @@
package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.components.LegendEntry
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.android.synthetic.main.item_grade_statistics_bar.view.*
import kotlinx.android.synthetic.main.item_grade_statistics_pie.view.*
import javax.inject.Inject
class GradeStatisticsAdapter @Inject constructor() :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items = emptyList<GradeStatisticsItem>()
var theme: String = "vulcan"
private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five,
4 to R.color.grade_vulcan_four,
3 to R.color.grade_vulcan_three,
2 to R.color.grade_vulcan_two,
1 to R.color.grade_vulcan_one
)
private val materialGradeColors = listOf(
6 to R.color.grade_material_six,
5 to R.color.grade_material_five,
4 to R.color.grade_material_four,
3 to R.color.grade_material_three,
2 to R.color.grade_material_two,
1 to R.color.grade_material_one
)
private val gradePointsColors = listOf(
Color.parseColor("#37c69c"),
Color.parseColor("#d8b12a")
)
private val gradeLabels = listOf(
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
)
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return when (items[position].type) {
ViewType.SEMESTER, ViewType.PARTIAL -> R.layout.item_grade_statistics_pie
ViewType.POINTS -> R.layout.item_grade_statistics_bar
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val viewHolder = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
R.layout.item_grade_statistics_bar -> GradeStatisticsBar(viewHolder)
else -> GradeStatisticsPie(viewHolder)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is GradeStatisticsPie -> bindPieChart(holder, items[position].partial)
is GradeStatisticsBar -> bindBarChart(holder, items[position].points!!)
}
}
private fun bindPieChart(holder: GradeStatisticsPie, partials: List<GradeStatistics>) {
with(holder.view.gradeStatisticsPieTitle) {
text = partials.firstOrNull()?.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
val gradeColors = when (theme) {
"vulcan" -> vulcanGradeColors
else -> materialGradeColors
}
val dataset = PieDataSet(partials.map {
PieEntry(it.amount.toFloat(), it.grade.toString())
}, "Legenda")
with(dataset) {
valueTextSize = 12f
sliceSpace = 1f
valueTextColor = Color.WHITE
setColors(partials.map {
gradeColors.single { color -> color.first == it.grade }.second
}.toIntArray(), holder.view.context)
}
with(holder.view.gradeStatisticsPie) {
setTouchEnabled(false)
if (partials.size == 1) animateXY(1000, 1000)
data = PieData(dataset).apply {
setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
}
})
}
with(legend) {
textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
setCustom(gradeLabels.mapIndexed { i, it ->
LegendEntry().apply {
label = it
formColor = ContextCompat.getColor(context, gradeColors[i].second)
form = Legend.LegendForm.SQUARE
}
})
}
minAngleForSlices = 25f
description.isEnabled = false
centerText = partials.fold(0) { acc, it -> acc + it.amount }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
invalidate()
}
}
private fun bindBarChart(holder: GradeStatisticsBar, points: GradePointsStatistics) {
with(holder.view.gradeStatisticsBarTitle) {
text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
val dataset = BarDataSet(listOf(
BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat())
), "Legenda")
with(dataset) {
valueTextSize = 12f
valueTextColor = holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
}
colors = gradePointsColors
}
with(holder.view.gradeStatisticsBar) {
setTouchEnabled(false)
if (items.size == 1) animateXY(1000, 1000)
data = BarData(dataset).apply {
barWidth = 0.5f
setFitBars(true)
}
legend.setCustom(listOf(
LegendEntry().apply {
label = "Średnia klasy"
formColor = gradePointsColors[0]
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = "Uczeń"
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
))
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
description.isEnabled = false
holder.view.context.getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it
axisRight.textColor = it
}
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
invalidate()
}
}
class GradeStatisticsPie(val view: View) : RecyclerView.ViewHolder(view)
class GradeStatisticsBar(val view: View) : RecyclerView.ViewHolder(view)
}

View File

@ -1,31 +1,18 @@
package io.github.wulkanowy.ui.modules.grade.statistics package io.github.wulkanowy.ui.modules.grade.statistics
import android.graphics.Color
import android.graphics.Color.WHITE
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.recyclerview.widget.LinearLayoutManager
import com.github.mikephil.charting.components.Legend
import com.github.mikephil.charting.components.LegendEntry
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.PieData
import com.github.mikephil.charting.data.PieDataSet
import com.github.mikephil.charting.data.PieEntry
import com.github.mikephil.charting.formatter.ValueFormatter
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import kotlinx.android.synthetic.main.fragment_grade_statistics.* import kotlinx.android.synthetic.main.fragment_grade_statistics.*
import javax.inject.Inject import javax.inject.Inject
@ -35,6 +22,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
@Inject @Inject
lateinit var presenter: GradeStatisticsPresenter lateinit var presenter: GradeStatisticsPresenter
@Inject
lateinit var statisticsAdapter: GradeStatisticsAdapter
private lateinit var subjectsAdapter: ArrayAdapter<String> private lateinit var subjectsAdapter: ArrayAdapter<String>
companion object { companion object {
@ -43,9 +33,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
fun newInstance() = GradeStatisticsFragment() fun newInstance() = GradeStatisticsFragment()
} }
override val isPieViewEmpty get() = gradeStatisticsChart.isEmpty override val isViewEmpty get() = statisticsAdapter.items.isEmpty()
override val isBarViewEmpty get() = gradeStatisticsChartPoints.isEmpty
override val currentType override val currentType
get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) { get() = when (gradeStatisticsTypeSwitch.checkedRadioButtonId) {
@ -54,35 +42,6 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
else -> ViewType.POINTS else -> ViewType.POINTS
} }
private lateinit var gradeColors: List<Pair<Int, Int>>
private val vulcanGradeColors = listOf(
6 to R.color.grade_vulcan_six,
5 to R.color.grade_vulcan_five,
4 to R.color.grade_vulcan_four,
3 to R.color.grade_vulcan_three,
2 to R.color.grade_vulcan_two,
1 to R.color.grade_vulcan_one
)
private val materialGradeColors = listOf(
6 to R.color.grade_material_six,
5 to R.color.grade_material_five,
4 to R.color.grade_material_four,
3 to R.color.grade_material_three,
2 to R.color.grade_material_two,
1 to R.color.grade_material_one
)
private val gradePointsColors = listOf(
Color.parseColor("#37c69c"),
Color.parseColor("#d8b12a")
)
private val gradeLabels = listOf(
"6, 6-", "5, 5-, 5+", "4, 4-, 4+", "3, 3-, 3+", "2, 2-, 2+", "1, 1+"
)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_grade_statistics, container, false) return inflater.inflate(R.layout.fragment_grade_statistics, container, false)
} }
@ -94,31 +53,9 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun initView() { override fun initView() {
with(gradeStatisticsChart) { with(gradeStatisticsRecycler) {
description.isEnabled = false layoutManager = LinearLayoutManager(requireContext())
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground)) adapter = statisticsAdapter
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
animateXY(1000, 1000)
minAngleForSlices = 25f
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
}
with(gradeStatisticsChartPoints) {
description.isEnabled = false
animateXY(1000, 1000)
legend.textColor = context.getThemeAttrColor(android.R.attr.textColorPrimary)
with(axisLeft) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
with(axisRight) {
axisMinimum = 0f
axisMaximum = 100f
labelCount = 11
}
} }
subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf()) subjectsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, mutableListOf())
@ -144,86 +81,10 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
} }
override fun updatePieData(items: List<GradeStatistics>, theme: String) { override fun updateData(items: List<GradeStatisticsItem>, theme: String) {
gradeColors = when (theme) { statisticsAdapter.theme = theme
"vulcan" -> vulcanGradeColors statisticsAdapter.items = items
else -> materialGradeColors statisticsAdapter.notifyDataSetChanged()
}
val dataset = PieDataSet(items.map {
PieEntry(it.amount.toFloat(), it.grade.toString())
}, "Legenda").apply {
valueTextSize = 12f
sliceSpace = 1f
valueTextColor = WHITE
setColors(items.map {
gradeColors.single { color -> color.first == it.grade }.second
}.toIntArray(), context)
}
with(gradeStatisticsChart) {
data = PieData(dataset).apply {
setTouchEnabled(false)
setValueFormatter(object : ValueFormatter() {
override fun getPieLabel(value: Float, pieEntry: PieEntry): String {
return resources.getQuantityString(R.plurals.grade_number_item, value.toInt(), value.toInt())
}
})
centerText = items.fold(0) { acc, it -> acc + it.amount }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
}
legend.apply {
setCustom(gradeLabels.mapIndexed { i, it ->
LegendEntry().apply {
label = it
formColor = ContextCompat.getColor(context, gradeColors[i].second)
form = Legend.LegendForm.SQUARE
}
})
}
invalidate()
}
}
override fun updateBarData(item: GradePointsStatistics) {
val dataset = BarDataSet(listOf(
BarEntry(1f, item.others.toFloat()),
BarEntry(2f, item.student.toFloat())
), "Legenda").apply {
valueTextSize = 12f
valueTextColor = requireContext().getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
}
colors = gradePointsColors
}
with(gradeStatisticsChartPoints) {
data = BarData(dataset).apply {
barWidth = 0.5f
setFitBars(true)
}
setTouchEnabled(false)
xAxis.setDrawLabels(false)
xAxis.setDrawGridLines(false)
requireContext().getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it
axisRight.textColor = it
}
legend.setCustom(listOf(
LegendEntry().apply {
label = "Średnia klasy"
formColor = gradePointsColors[0]
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = "Uczeń"
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
))
invalidate()
}
} }
override fun showSubjects(show: Boolean) { override fun showSubjects(show: Boolean) {
@ -232,16 +93,15 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun clearView() { override fun clearView() {
gradeStatisticsChart.clear() statisticsAdapter.items = emptyList()
gradeStatisticsChartPoints.clear()
} }
override fun showPieContent(show: Boolean) { override fun resetView() {
gradeStatisticsChart.visibility = if (show) View.VISIBLE else View.GONE gradeStatisticsScroll.scrollTo(0, 0)
} }
override fun showBarContent(show: Boolean) { override fun showContent(show: Boolean) {
gradeStatisticsChartPoints.visibility = if (show) View.VISIBLE else View.GONE gradeStatisticsRecycler.visibility = if (show) View.VISIBLE else View.GONE
} }
override fun showEmpty(show: Boolean) { override fun showEmpty(show: Boolean) {
@ -273,7 +133,7 @@ class GradeStatisticsFragment : BaseFragment(), GradeStatisticsView, GradeView.G
} }
override fun onParentReselected() { override fun onParentReselected() {
// presenter.onParentViewReselected()
} }
override fun onParentChangeSemester() { override fun onParentChangeSemester() {

View File

@ -48,12 +48,19 @@ class GradeStatisticsPresenter @Inject constructor(
loadDataByType(semesterId, currentSubjectName, currentType, forceRefresh) loadDataByType(semesterId, currentSubjectName, currentType, forceRefresh)
} }
fun onParentViewReselected() {
view?.run {
if (!isViewEmpty) resetView()
}
}
fun onParentViewChangeSemester() { fun onParentViewChangeSemester() {
view?.run { view?.run {
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showRefresh(false) showRefresh(false)
showBarContent(false) showContent(false)
showErrorView(false) showErrorView(false)
showEmpty(false) showEmpty(false)
clearView() clearView()
@ -81,8 +88,7 @@ class GradeStatisticsPresenter @Inject constructor(
fun onSubjectSelected(name: String?) { fun onSubjectSelected(name: String?) {
Timber.i("Select grade stats subject $name") Timber.i("Select grade stats subject $name")
view?.run { view?.run {
showBarContent(false) showContent(false)
showPieContent(false)
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showEmpty(false) showEmpty(false)
@ -99,8 +105,7 @@ class GradeStatisticsPresenter @Inject constructor(
Timber.i("Select grade stats semester: $type") Timber.i("Select grade stats semester: $type")
disposable.clear() disposable.clear()
view?.run { view?.run {
showBarContent(false) showContent(false)
showPieContent(false)
showProgress(true) showProgress(true)
enableSwipe(false) enableSwipe(false)
showEmpty(false) showEmpty(false)
@ -135,20 +140,22 @@ class GradeStatisticsPresenter @Inject constructor(
private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) { private fun loadDataByType(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean = false) {
currentSubjectName = subjectName currentSubjectName = subjectName
currentType = type currentType = type
when (type) { loadData(semesterId, subjectName, type, forceRefresh)
ViewType.SEMESTER -> loadData(semesterId, subjectName, true, forceRefresh)
ViewType.PARTIAL -> loadData(semesterId, subjectName, false, forceRefresh)
ViewType.POINTS -> loadPointsData(semesterId, subjectName, forceRefresh)
}
} }
private fun loadData(semesterId: Int, subjectName: String, isSemester: Boolean, forceRefresh: Boolean = false) { private fun loadData(semesterId: Int, subjectName: String, type: ViewType, forceRefresh: Boolean) {
Timber.i("Loading grade stats data started") Timber.i("Loading grade stats data started")
disposable.add(studentRepository.getCurrentStudent() disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) } .flatMap { semesterRepository.getSemesters(it) }
.flatMap { gradeStatisticsRepository.getGradesStatistics(it.first { item -> item.semesterId == semesterId }, subjectName, isSemester, forceRefresh) } .flatMap {
.map { list -> list.sortedByDescending { it.grade } } val semester = it.first { item -> item.semesterId == semesterId }
.map { list -> list.filter { it.amount != 0 } }
when (type) {
ViewType.SEMESTER -> gradeStatisticsRepository.getGradesStatistics(semester, subjectName, true, forceRefresh)
ViewType.PARTIAL -> gradeStatisticsRepository.getGradesStatistics(semester, subjectName, false, forceRefresh)
ViewType.POINTS -> gradeStatisticsRepository.getGradesPointsStatistics(semester, subjectName, forceRefresh)
}
}
.subscribeOn(schedulers.backgroundThread) .subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread) .observeOn(schedulers.mainThread)
.doFinally { .doFinally {
@ -163,10 +170,9 @@ class GradeStatisticsPresenter @Inject constructor(
Timber.i("Loading grade stats result: Success") Timber.i("Loading grade stats result: Success")
view?.run { view?.run {
showEmpty(it.isEmpty()) showEmpty(it.isEmpty())
showBarContent(false) showContent(it.isNotEmpty())
showPieContent(it.isNotEmpty())
showErrorView(false) showErrorView(false)
updatePieData(it, preferencesRepository.gradeColorTheme) updateData(it, preferencesRepository.gradeColorTheme)
} }
analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh) analytics.logEvent("load_grade_statistics", "items" to it.size, "force_refresh" to forceRefresh)
}) { }) {
@ -175,47 +181,9 @@ class GradeStatisticsPresenter @Inject constructor(
}) })
} }
private fun loadPointsData(semesterId: Int, subjectName: String, forceRefresh: Boolean = false) {
Timber.i("Loading grade points stats data started")
disposable.add(studentRepository.getCurrentStudent()
.flatMap { semesterRepository.getSemesters(it) }
.flatMapMaybe { gradeStatisticsRepository.getGradesPointsStatistics(it.first { item -> item.semesterId == semesterId }, subjectName, forceRefresh) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
enableSwipe(true)
notifyParentDataLoaded(semesterId)
}
}
.subscribe({
Timber.i("Loading grade points stats result: Success")
view?.run {
showEmpty(false)
showPieContent(false)
showBarContent(true)
showErrorView(false)
updateBarData(it)
}
analytics.logEvent("load_grade_points_statistics", "force_refresh" to forceRefresh)
}, {
Timber.e("Loading grade points stats result: An exception occurred")
errorHandler.dispatch(it)
}, {
Timber.d("Loading grade points stats result: No point stats found")
view?.run {
showBarContent(false)
showEmpty(true)
}
})
)
}
private fun showErrorViewOnError(message: String, error: Throwable) { private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run { view?.run {
if ((isBarViewEmpty && currentType == ViewType.POINTS) || (isPieViewEmpty) && currentType != ViewType.POINTS) { if (isViewEmpty) {
lastError = error lastError = error
setErrorDetails(message) setErrorDetails(message)
showErrorView(true) showErrorView(true)

View File

@ -1,14 +1,11 @@
package io.github.wulkanowy.ui.modules.grade.statistics package io.github.wulkanowy.ui.modules.grade.statistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics import io.github.wulkanowy.data.pojos.GradeStatisticsItem
import io.github.wulkanowy.data.db.entities.GradeStatistics
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface GradeStatisticsView : BaseView { interface GradeStatisticsView : BaseView {
val isPieViewEmpty: Boolean val isViewEmpty: Boolean
val isBarViewEmpty: Boolean
val currentType: ViewType val currentType: ViewType
@ -16,9 +13,7 @@ interface GradeStatisticsView : BaseView {
fun updateSubjects(data: ArrayList<String>) fun updateSubjects(data: ArrayList<String>)
fun updatePieData(items: List<GradeStatistics>, theme: String) fun updateData(items: List<GradeStatisticsItem>, theme: String)
fun updateBarData(item: GradePointsStatistics)
fun showSubjects(show: Boolean) fun showSubjects(show: Boolean)
@ -28,9 +23,9 @@ interface GradeStatisticsView : BaseView {
fun clearView() fun clearView()
fun showPieContent(show: Boolean) fun resetView()
fun showBarContent(show: Boolean) fun showContent(show: Boolean)
fun showEmpty(show: Boolean) fun showEmpty(show: Boolean)

View File

@ -1,7 +0,0 @@
package io.github.wulkanowy.utils
import kotlin.math.round
fun Double.roundToDecimalPlaces(places: Int = 2): Double {
return round(this * 10 * places) / (10 * places)
}

View File

@ -37,6 +37,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:id="@+id/gradeStatisticsScroll"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fillViewport="true"> android:fillViewport="true">
@ -90,25 +91,11 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.github.mikephil.charting.charts.PieChart <androidx.recyclerview.widget.RecyclerView
android:id="@+id/gradeStatisticsChart" android:id="@+id/gradeStatisticsRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="10dp" tools:listitem="@layout/item_grade_statistics_pie" />
android:background="?android:windowBackground"
android:minHeight="400dp"
android:visibility="gone"
tools:visibility="visible" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/gradeStatisticsChartPoints"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="10dp"
android:background="?android:windowBackground"
android:minHeight="400dp"
android:visibility="gone"
tools:visibility="visible" />
<me.zhanghai.android.materialprogressbar.MaterialProgressBar <me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/gradeStatisticsProgress" android:id="@+id/gradeStatisticsProgress"

View File

@ -0,0 +1,27 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/gradeStatisticsBarTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorControlHighlight"
android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingBottom="10dp"
android:textSize="18sp"
tools:text="Matematyka" />
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/gradeStatisticsBar"
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_margin="10dp"
android:background="?android:windowBackground"
tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" />
</LinearLayout>

View File

@ -0,0 +1,27 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/gradeStatisticsPieTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorControlHighlight"
android:paddingLeft="20dp"
android:paddingTop="10dp"
android:paddingRight="20dp"
android:paddingBottom="10dp"
android:textSize="18sp"
tools:text="Matematyka" />
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/gradeStatisticsPie"
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_margin="10dp"
android:background="?android:windowBackground"
tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" />
</LinearLayout>