Stats and graphs: Added intelligence to provide labels for legends

feature/top10
Laurent 7 years ago
parent 0be3cd5527
commit e0cbc7b00a
  1. 66
      app/src/main/java/net/pokeranalytics/android/calculus/Calculator.kt
  2. 15
      app/src/main/java/net/pokeranalytics/android/calculus/ComputableGroup.kt
  3. 117
      app/src/main/java/net/pokeranalytics/android/calculus/Stat.kt
  4. 3
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Timed.kt
  5. 1
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  6. 47
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  7. 16
      app/src/main/java/net/pokeranalytics/android/model/realm/SessionSet.kt
  8. 4
      app/src/main/java/net/pokeranalytics/android/ui/activity/GraphActivity.kt
  9. 2
      app/src/main/java/net/pokeranalytics/android/ui/activity/HomeActivity.kt
  10. 45
      app/src/main/java/net/pokeranalytics/android/ui/fragment/GraphFragment.kt
  11. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/HistoryFragment.kt
  12. 66
      app/src/main/java/net/pokeranalytics/android/ui/graph/GraphExtensions.kt
  13. 1
      app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SessionRow.kt
  14. 13
      app/src/main/res/layout/fragment_graph.xml

@ -1,7 +1,5 @@
package net.pokeranalytics.android.calculus package net.pokeranalytics.android.calculus
import android.os.Parcel
import android.os.Parcelable
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.calculus.Stat.* import net.pokeranalytics.android.calculus.Stat.*
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.ComputableResult
@ -10,31 +8,6 @@ import net.pokeranalytics.android.model.realm.SessionSet
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
class ParcelableString(var string: String) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readString()) {
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(string)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ParcelableString> {
override fun createFromParcel(parcel: Parcel): ParcelableString {
return ParcelableString(parcel)
}
override fun newArray(size: Int): Array<ParcelableString?> {
return arrayOfNulls(size)
}
}
}
/** /**
* The class performing stats computation * The class performing stats computation
*/ */
@ -92,8 +65,6 @@ class Calculator {
var computableGroups: MutableList<ComputableGroup> = mutableListOf() var computableGroups: MutableList<ComputableGroup> = mutableListOf()
filters.forEach { filter -> filters.forEach { filter ->
val results = filter.results<ComputableResult>()
val sets = filter.results<SessionSet>()
val group = ComputableGroup(filter.name, filter.filterConditions.map { it.queryCondition }) val group = ComputableGroup(filter.name, filter.filterConditions.map { it.queryCondition })
computableGroups.add(group) computableGroups.add(group)
@ -175,20 +146,20 @@ class Calculator {
tBuyinSum += computable.ratedBuyin tBuyinSum += computable.ratedBuyin
tHands += computable.estimatedHands tHands += computable.estimatedHands
val sessionId = ParcelableString(computable.session?.id ?: throw IllegalStateException("Computing lone ComputableResult")) val session = computable.session ?: throw IllegalStateException("Computing lone ComputableResult")
results.addEvolutionValue(tSum, NETRESULT, sessionId) results.addEvolutionValue(tSum, NETRESULT, session)
results.addEvolutionValue(tSum / index, AVERAGE, sessionId) results.addEvolutionValue(tSum / index, AVERAGE, session)
results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES, sessionId) results.addEvolutionValue(index.toDouble(), NUMBER_OF_GAMES, session)
results.addEvolutionValue(tBBSum / tBBSessionCount, AVERAGE_NET_BB, sessionId) results.addEvolutionValue(tBBSum / tBBSessionCount, AVERAGE_NET_BB, session)
results.addEvolutionValue((tWinningSessionCount / index).toDouble(), WIN_RATIO, sessionId) results.addEvolutionValue((tWinningSessionCount / index).toDouble(), WIN_RATIO, session)
results.addEvolutionValue(tBuyinSum / index, AVERAGE_BUYIN, sessionId) results.addEvolutionValue(tBuyinSum / index, AVERAGE_BUYIN, session)
Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 -> Stat.netBBPer100Hands(tBBSum, tHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, NET_BB_PER_100_HANDS, sessionId) results.addEvolutionValue(netBB100, NET_BB_PER_100_HANDS, session)
} }
Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi -> Stat.returnOnInvestment(tSum, tBuyinSum)?.let { roi ->
results.addEvolutionValue(roi, ROI, sessionId) results.addEvolutionValue(roi, ROI, session)
} }
} }
@ -208,7 +179,7 @@ class Calculator {
val gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble() val gBBSum = sessionSets.sum(SessionSet.Field.BB_NET.identifier).toDouble()
val hourlyRate = gSum / gHourlyDuration val hourlyRate = gSum / gHourlyDuration
// var hourlyRateBB = gBBSum / gDuration // var bbHourlyRate = gBBSum / gDuration
when (options.evolutionValues) { when (options.evolutionValues) {
Options.EvolutionValues.DATED -> { Options.EvolutionValues.DATED -> {
@ -231,17 +202,16 @@ class Calculator {
tHourlyRate = gSum / tHourlyDuration tHourlyRate = gSum / tHourlyDuration
tHourlyRateBB = gBBSum / tHourlyDuration tHourlyRateBB = gBBSum / tHourlyDuration
val id = ParcelableString(sessionSet.id) results.addEvolutionValue(tSum, tHourlyDuration, NETRESULT, sessionSet)
results.addEvolutionValue(tSum, tHourlyDuration, NETRESULT, id) results.addEvolutionValue(tSum / tHourlyDuration, tHourlyDuration, HOURLY_RATE, sessionSet)
results.addEvolutionValue(tSum / tHourlyDuration, tHourlyDuration, HOURLY_RATE, id) results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, sessionSet)
results.addEvolutionValue(tHourlyRate, tHourlyDuration, HOURLY_RATE, id) results.addEvolutionValue(tIndex.toDouble(), tHourlyDuration, NUMBER_OF_SETS, sessionSet)
results.addEvolutionValue(tIndex.toDouble(), tHourlyDuration, NUMBER_OF_SETS, id) results.addEvolutionValue(sessionSet.netDuration.toDouble(), tHourlyDuration, DURATION, sessionSet)
results.addEvolutionValue(sessionSet.netDuration.toDouble(), tHourlyDuration, DURATION, id) results.addEvolutionValue(tHourlyDuration / tIndex, tHourlyDuration, AVERAGE_DURATION, sessionSet)
results.addEvolutionValue(tHourlyDuration / tIndex, tHourlyDuration, AVERAGE_DURATION, id) results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, sessionSet)
results.addEvolutionValue(tHourlyRateBB, tHourlyDuration, HOURLY_RATE_BB, id)
Stat.netBBPer100Hands(gBBSum, gTotalHands)?.let { netBB100 -> Stat.netBBPer100Hands(gBBSum, gTotalHands)?.let { netBB100 ->
results.addEvolutionValue(netBB100, tHourlyDuration, NET_BB_PER_100_HANDS, id) results.addEvolutionValue(netBB100, tHourlyDuration, NET_BB_PER_100_HANDS, sessionSet)
} }
} }

@ -5,6 +5,7 @@ import com.github.mikephil.charting.data.Entry
import io.realm.Realm import io.realm.Realm
import io.realm.RealmResults import io.realm.RealmResults
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.model.realm.ComputableResult import net.pokeranalytics.android.model.realm.ComputableResult
import net.pokeranalytics.android.model.realm.Filter import net.pokeranalytics.android.model.realm.Filter
import net.pokeranalytics.android.model.realm.SessionSet import net.pokeranalytics.android.model.realm.SessionSet
@ -113,8 +114,9 @@ class ComputedResults(group: ComputableGroup) {
this._addEvolutionValue(Point(value, data), stat = stat) this._addEvolutionValue(Point(value, data), stat = stat)
} }
fun addEvolutionValue(value: Double, duration: Double, stat: Stat, data: Any) { fun addEvolutionValue(value: Double, duration: Double, stat: Stat, data: Timed) {
this._addEvolutionValue(Point(value, y = duration, data = data), stat = stat) stat.underlyingClass = data::class.java
this._addEvolutionValue(Point(value, y = duration, data = data.id), stat = stat)
} }
private fun _addEvolutionValue(point: Point, stat: Stat) { private fun _addEvolutionValue(point: Point, stat: Stat) {
@ -169,7 +171,7 @@ class ComputedResults(group: ComputableGroup) {
// MPAndroidChart // MPAndroidChart
fun defaultStatEntries(stat: Stat): List<out Entry> { fun defaultStatEntries(stat: Stat): List<Entry> {
return when (stat) { return when (stat) {
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES -> this.barEntries(stat) Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES -> this.barEntries(stat)
else -> this.singleLineEntries(stat) else -> this.singleLineEntries(stat)
@ -177,7 +179,7 @@ class ComputedResults(group: ComputableGroup) {
} }
fun singleLineEntries(stat: Stat): List<Entry> { fun singleLineEntries(stat: Stat): List<Entry> {
var entries = mutableListOf<Entry>() val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points -> this._evolutionValues[stat]?.let { points ->
points.forEachIndexed { index, p -> points.forEachIndexed { index, p ->
entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data)) entries.add(Entry(index.toFloat(), p.y.toFloat(), p.data))
@ -187,7 +189,7 @@ class ComputedResults(group: ComputableGroup) {
} }
fun durationEntries(stat: Stat): List<Entry> { fun durationEntries(stat: Stat): List<Entry> {
var entries = mutableListOf<Entry>() val entries = mutableListOf<Entry>()
this._evolutionValues[stat]?.let { points -> this._evolutionValues[stat]?.let { points ->
points.forEach { p -> points.forEach { p ->
entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data)) entries.add(Entry(p.x.toFloat(), p.y.toFloat(), p.data))
@ -198,8 +200,7 @@ class ComputedResults(group: ComputableGroup) {
fun barEntries(stat: Stat): List<BarEntry> { fun barEntries(stat: Stat): List<BarEntry> {
var entries = mutableListOf<BarEntry>() val entries = mutableListOf<BarEntry>()
this._evolutionValues[stat]?.let { points -> this._evolutionValues[stat]?.let { points ->
points.forEach { p -> points.forEach { p ->
entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data)) entries.add(BarEntry(p.x.toFloat(), p.y.toFloat(), p.data))

@ -1,8 +1,10 @@
package net.pokeranalytics.android.calculus package net.pokeranalytics.android.calculus
import android.content.Context import android.content.Context
import io.realm.RealmModel
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.FormattingException import net.pokeranalytics.android.exceptions.FormattingException
import net.pokeranalytics.android.model.interfaces.Timed
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.CurrencyUtils import net.pokeranalytics.android.util.CurrencyUtils
@ -11,10 +13,20 @@ import net.pokeranalytics.android.util.extensions.formatted
import net.pokeranalytics.android.util.extensions.formattedHourlyDuration import net.pokeranalytics.android.util.extensions.formattedHourlyDuration
import java.util.* import java.util.*
class StatFormattingException(message: String) : Exception(message) {
}
interface StatBase : RealmModel {
fun formattedValue(stat: Stat, context: Context): TextFormat
}
/** /**
* An enum representing all the types of Session statistics * An enum representing all the types of Session statistics
*/ */
enum class Stat : RowRepresentable { enum class Stat(var underlyingClass: Class<out Timed>? = null) : RowRepresentable {
NETRESULT, NETRESULT,
HOURLY_RATE, HOURLY_RATE,
@ -37,7 +49,7 @@ enum class Stat : RowRepresentable {
/** /**
* Returns whether the stat evolution numericValues requires a distribution sorting * Returns whether the stat evolution numericValues requires a distribution sorting
*/ */
fun hasDistributionSorting() : Boolean { fun hasDistributionSorting(): Boolean {
return when (this) { return when (this) {
STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true STANDARD_DEVIATION, STANDARD_DEVIATION_HOURLY, STANDARD_DEVIATION_BB_PER_100_HANDS -> true
else -> false else -> false
@ -46,14 +58,14 @@ enum class Stat : RowRepresentable {
companion object { companion object {
fun returnOnInvestment(netResult: Double, buyin: Double) : Double? { fun returnOnInvestment(netResult: Double, buyin: Double): Double? {
if (buyin == 0.0) { if (buyin == 0.0) {
return null return null
} }
return netResult / buyin return netResult / buyin
} }
fun netBBPer100Hands(netBB: Double, numberOfHands: Double) : Double? { fun netBBPer100Hands(netBB: Double, numberOfHands: Double): Double? {
if (numberOfHands == 0.0) { if (numberOfHands == 0.0) {
return null return null
} }
@ -85,6 +97,48 @@ enum class Stat : RowRepresentable {
} }
} }
/**
* Formats the value of the stat to be suitable for display
*/
fun format(value: Double, currency: Currency? = null, context: Context): TextFormat {
if (value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white)
}
when (this) {
// Amounts + red/green
Stat.NETRESULT, Stat.HOURLY_RATE, Stat.AVERAGE -> {
val numberFormat = CurrencyUtils.getCurrencyFormatter(context, currency)
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(numberFormat.format(value), color)
}
// Red/green numericValues
Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.NET_BB_PER_100_HANDS -> {
val color = if (value >= this.threshold) R.color.green else R.color.red
return TextFormat(value.formatted(), color)
}
// white integers
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HANDS_PLAYED -> {
return TextFormat("${value.toInt()}")
} // white durations
Stat.DURATION, Stat.AVERAGE_DURATION -> {
return TextFormat(value.formattedHourlyDuration())
} // red/green percentages
Stat.WIN_RATIO, Stat.ROI -> {
val color = if (value * 100 >= this.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted()}%", color)
} // white amountsr
Stat.AVERAGE_BUYIN, Stat.STANDARD_DEVIATION, Stat.STANDARD_DEVIATION_HOURLY,
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> {
val numberFormat = CurrencyUtils.getCurrencyFormatter(context, currency)
return TextFormat(numberFormat.format(value))
}
else -> throw FormattingException("Stat formatting of ${this.name} not handled")
}
}
val threshold: Double val threshold: Double
get() { get() {
return when (this) { return when (this) {
@ -94,6 +148,23 @@ enum class Stat : RowRepresentable {
} }
fun cumulativeLabelResId(context: Context) : String {
val resId = when (this) {
AVERAGE, AVERAGE_DURATION, NETRESULT, NET_BB_PER_100_HANDS,
HOURLY_RATE_BB, AVERAGE_NET_BB, ROI, WIN_RATIO, HOURLY_RATE -> R.string.average
NETRESULT, DURATION -> R.string.total
STANDARD_DEVIATION -> R.string.net_result
STANDARD_DEVIATION_HOURLY -> R.string.hour_rate_without_pauses
STANDARD_DEVIATION_BB_PER_100_HANDS -> R.string.net_result_bb_per_100_hands
else -> null
}
resId?.let {
return context.getString(it)
} ?: run {
return NULL_TEXT
}
}
override val viewType: Int = RowViewType.TITLE_VALUE.ordinal override val viewType: Int = RowViewType.TITLE_VALUE.ordinal
} }
@ -117,47 +188,13 @@ class ComputedStat(var stat: Stat, var value: Double, var currency: Currency? =
* Formats the value of the stat to be suitable for display * Formats the value of the stat to be suitable for display
*/ */
fun format(context: Context): TextFormat { fun format(context: Context): TextFormat {
return this.stat.format(this.value, this.currency, context)
if (this.value.isNaN()) {
return TextFormat(NULL_TEXT, R.color.white)
}
when (this.stat) {
// Amounts + red/green
Stat.NETRESULT, Stat.HOURLY_RATE, Stat.AVERAGE -> {
val numberFormat= CurrencyUtils.getCurrencyFormatter(context, currency)
val color = if (this.value >= this.stat.threshold) R.color.green else R.color.red
return TextFormat(numberFormat.format(this.value), color)
}
// Red/green numericValues
Stat.HOURLY_RATE_BB, Stat.AVERAGE_NET_BB, Stat.NET_BB_PER_100_HANDS -> {
val color = if (this.value >= this.stat.threshold) R.color.green else R.color.red
return TextFormat(this.value.formatted(), color)
}
// white integers
Stat.NUMBER_OF_SETS, Stat.NUMBER_OF_GAMES, Stat.HANDS_PLAYED -> {
return TextFormat("${value.toInt()}")
} // white durations
Stat.DURATION, Stat.AVERAGE_DURATION -> {
return TextFormat(value.formattedHourlyDuration())
} // red/green percentages
Stat.WIN_RATIO, Stat.ROI -> {
val color = if (value * 100 >= this.stat.threshold) R.color.green else R.color.red
return TextFormat("${(value * 100).formatted()}%", color)
} // white amountsr
Stat.AVERAGE_BUYIN, Stat.STANDARD_DEVIATION, Stat.STANDARD_DEVIATION_HOURLY,
Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> {
val numberFormat= CurrencyUtils.getCurrencyFormatter(context, currency)
return TextFormat(numberFormat.format(this.value))
}
else -> throw FormattingException("Stat formatting of ${this.stat.name} not handled")
}
} }
/** /**
* Returns a TextFormat instance for an evolution value located at the specified [index] * Returns a TextFormat instance for an evolution value located at the specified [index]
*/ */
fun evolutionValueFormat(index: Int) : TextFormat { fun evolutionValueFormat(index: Int): TextFormat {
return TextFormat("undef ${index}") return TextFormat("undef ${index}")
} }

@ -1,8 +1,9 @@
package net.pokeranalytics.android.model.interfaces package net.pokeranalytics.android.model.interfaces
import net.pokeranalytics.android.calculus.StatBase
import java.util.* import java.util.*
interface Timed { interface Timed : StatBase, Identifiable {
fun startDate() : Date? fun startDate() : Date?

@ -43,7 +43,6 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("SessionSet")?.let { schema.get("SessionSet")?.let {
it.addPrimaryKey("id") it.addPrimaryKey("id")
} }
currentVersion++ currentVersion++
} }

@ -13,6 +13,8 @@ import io.realm.kotlin.where
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.ComputedStat import net.pokeranalytics.android.calculus.ComputedStat
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.model.Limit import net.pokeranalytics.android.model.Limit
import net.pokeranalytics.android.model.LiveData import net.pokeranalytics.android.model.LiveData
@ -365,6 +367,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
throw ModelException("Session should have an existing Result relationship") throw ModelException("Session should have an existing Result relationship")
} }
val bbHourlyRate: Double
get() {
return this.bbNet / this.hourlyDuration
}
// Manageable // Manageable
override fun isValidForSave(): Boolean { override fun isValidForSave(): Boolean {
@ -822,5 +829,45 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
} }
// Stat Base
override fun formattedValue(stat: Stat, context: Context) : TextFormat {
this.result?.let { result ->
val value: Double? = when (stat) {
Stat.NETRESULT, Stat.AVERAGE, Stat.STANDARD_DEVIATION -> result.net
Stat.NUMBER_OF_GAMES, Stat.NUMBER_OF_SETS -> 1.0
Stat.AVERAGE_BUYIN -> result.buyin
Stat.ROI -> {
result.buyin?.let {
Stat.returnOnInvestment(result.net, it)
} ?: run {
null
}
}
Stat.HOURLY_RATE_BB -> this.bbHourlyRate
Stat.NET_BB_PER_100_HANDS, Stat.STANDARD_DEVIATION_BB_PER_100_HANDS -> Stat.netBBPer100Hands(this.bbNet, this.estimatedHands)
Stat.AVERAGE_NET_BB -> this.bbNet
Stat.DURATION, Stat.AVERAGE_DURATION -> this.netDuration.toDouble()
Stat.HOURLY_RATE, Stat.STANDARD_DEVIATION_HOURLY -> this.hourlyRate
Stat.HANDS_PLAYED -> this.estimatedHands
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
}
value?.let {
return stat.format(it, CurrencyUtils.getCurrency(this.bankroll), context)
} ?: run {
return TextFormat(NULL_TEXT)
}
} ?: run {
throw java.lang.IllegalStateException("Asking for stats on Session without Result")
}
}
} }

@ -1,11 +1,15 @@
package net.pokeranalytics.android.model.realm package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.Realm import io.realm.Realm
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults import io.realm.RealmResults
import io.realm.annotations.Ignore import io.realm.annotations.Ignore
import io.realm.annotations.LinkingObjects import io.realm.annotations.LinkingObjects
import io.realm.annotations.PrimaryKey import io.realm.annotations.PrimaryKey
import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.calculus.StatFormattingException
import net.pokeranalytics.android.calculus.TextFormat
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import net.pokeranalytics.android.model.interfaces.Timed import net.pokeranalytics.android.model.interfaces.Timed
@ -15,7 +19,7 @@ import java.util.*
open class SessionSet() : RealmObject(), Timed, Filterable { open class SessionSet() : RealmObject(), Timed, Filterable {
@PrimaryKey @PrimaryKey
var id = UUID.randomUUID().toString() override var id = UUID.randomUUID().toString()
var startDate: Date = Date() var startDate: Date = Date()
set(value) { set(value) {
@ -74,6 +78,16 @@ open class SessionSet() : RealmObject(), Timed, Filterable {
var bbNet: Double = 0.0 var bbNet: Double = 0.0
override fun formattedValue(stat: Stat, context: Context) : TextFormat {
return when (stat) {
Stat.NETRESULT, Stat.AVERAGE -> stat.format(this.ratedNet, null, context)
Stat.DURATION, Stat.AVERAGE_DURATION -> stat.format(this.netDuration.toDouble(), null, context)
Stat.HOURLY_RATE -> stat.format(this.hourlyRate, null, context)
Stat.HANDS_PLAYED -> stat.format(this.estimatedHands, null, context)
else -> throw StatFormattingException("format undefined for stat ${stat.name}")
}
}
enum class Field(val identifier: String) { enum class Field(val identifier: String) {
RATED_NET("ratedNet"), RATED_NET("ratedNet"),
HOURLY_RATE("hourlyRate"), HOURLY_RATE("hourlyRate"),

@ -9,9 +9,9 @@ import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.GraphFragment import net.pokeranalytics.android.ui.fragment.GraphFragment
class GraphParameters(stat: Stat, entries: List<out Entry>) { class GraphParameters(stat: Stat, entries: List<Entry>) {
var stat: Stat = stat var stat: Stat = stat
var entries: List<out Entry> = entries var entries: List<Entry> = entries
} }
class GraphActivity : PokerAnalyticsActivity() { class GraphActivity : PokerAnalyticsActivity() {

@ -78,7 +78,7 @@ class HomeActivity : PokerAnalyticsActivity() {
// observe currency changes // observe currency changes
this.currencies = realm.where(Currency::class.java).findAll() this.currencies = realm.where(Currency::class.java).findAll()
this.currencies.addChangeListener { t, set -> this.currencies.addChangeListener { t, _ ->
realm.beginTransaction() realm.beginTransaction()
t.forEach { t.forEach {

@ -7,17 +7,20 @@ import android.view.ViewGroup
import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.highlight.Highlight
import com.github.mikephil.charting.listener.OnChartValueSelectedListener
import kotlinx.android.synthetic.main.fragment_graph.* import kotlinx.android.synthetic.main.fragment_graph.*
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.ui.graph.setStyle
interface GraphDataSource { interface GraphDataSource {
} }
class GraphFragment : PokerAnalyticsFragment() { class GraphFragment : PokerAnalyticsFragment(), OnChartValueSelectedListener {
lateinit var dataSource: GraphDataSource lateinit var dataSource: GraphDataSource
@ -46,8 +49,48 @@ class GraphFragment : PokerAnalyticsFragment() {
private fun initUI() { private fun initUI() {
val dataSet = LineDataSet(this.entries, this.stat.name) val dataSet = LineDataSet(this.entries, this.stat.name)
val colors = arrayOf(R.color.green_light).toIntArray()
dataSet.setColors(colors, context)
val lineData = LineData(listOf(dataSet)) val lineData = LineData(listOf(dataSet))
this.chart.setStyle()
this.chart.data = lineData this.chart.data = lineData
this.chart.setOnChartValueSelectedListener(this)
}
// OnChartValueSelectedListener
override fun onNothingSelected() {
// nothing to do
}
override fun onValueSelected(e: Entry?, h: Highlight?) {
e?.let { entry ->
h?.let { highlight ->
val id = entry.data as String
val item = getRealm().where(this.stat.underlyingClass).equalTo("id", id).findAll().firstOrNull()
item?.let {
val date = it.startDate()
val entryStatName = this.stat.localizedTitle(requireContext())
val entryValue = it.formattedValue(this.stat, requireContext())
val totalStatName = this.stat.cumulativeLabelResId(requireContext())
val totalStatValue = this.stat.format(e.y.toDouble(), null, requireContext())
}
this.text.text = ""
}
}
} }

@ -112,7 +112,7 @@ class HistoryFragment : PokerAnalyticsFragment(), LiveRowRepresentableDataSource
private fun initData() { private fun initData() {
this.realmSessions = getRealm().where<Session>().findAll().sort("startDate", Sort.DESCENDING) this.realmSessions = getRealm().where<Session>().findAll().sort("startDate", Sort.DESCENDING)
this.realmSessions.addChangeListener { t, changeSet -> this.realmSessions.addChangeListener { _, changeSet ->
this.historyAdapter.refreshData() this.historyAdapter.refreshData()
this.historyAdapter.notifyDataSetChanged() this.historyAdapter.notifyDataSetChanged()
} }

@ -0,0 +1,66 @@
package net.pokeranalytics.android.ui.graph
import com.github.mikephil.charting.charts.BarChart
import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.LineChart
fun BarChart.setStyle() {
GraphHelper.setStyle(this)
}
fun LineChart.setStyle() {
GraphHelper.setStyle(this)
}
class GraphHelper {
companion object {
fun setStyle(chart: BarLineChartBase<*>) {
// this.xAxis.axisLineColor = ContextCompat.getColor(context, R.color.) ChartAppearance.defaultColor
// this.xAxis.axisLineWidth = ChartAppearance.lineWidth
// this.xAxis.enableGridDashedLine(3.0f, 5.0f, 1.0f)
//
// this.xAxis.labelTextColor = ChartAppearance.defaultColor
// this.xAxis.labelFont = Fonts.graphAxis
// this.xAxis.labelCount = 4
// this.xAxis.labelPosition = .bottom
//
// this.xAxis.drawLabelsEnabled = true
// this.xAxis.drawGridLinesEnabled = true
// this.xAxis.granularity = 1.0
// this.xAxis.granularityEnabled = true
// this.xAxis.enabled = true
//
// // Y Axis
// this.leftAxis.drawAxisLineEnabled = false
// this.leftAxis.drawGridLinesEnabled = true
// this.leftAxis.gridLineDashLengths = [3.0, 5.0]
//
// this.leftAxis.drawZeroLineEnabled = true
// this.leftAxis.zeroLineWidth = ChartAppearance.lineWidth
// this.leftAxis.zeroLineColor = ChartAppearance.defaultColor
//
// this.leftAxis.granularityEnabled = true
// this.leftAxis.granularity = 1.0
//
// this.leftAxis.labelTextColor = ChartAppearance.defaultColor
// this.leftAxis.labelFont = Fonts.graphAxis
// this.leftAxis.labelCount = small ? 1 : 7 // @todo not great if interval is [0..2] for number of records as we get decimals
//
// if timeYAxis {
// this.leftAxis.valueFormatter = HourValueFormatter()
// } else {
// this.leftAxis.valueFormatter = LargeNumberFormatter()
// }
//
// this.rightAxis.enabled = false
//
// this.legend.enabled = false
}
}
}

@ -62,7 +62,6 @@ enum class SessionRow : RowRepresentable {
START_DATE, END_DATE, BREAK_TIME, COMMENT START_DATE, END_DATE, BREAK_TIME, COMMENT
) )
} }
else -> arrayListOf()
} }
} }
Session.Type.CASH_GAME.ordinal -> { Session.Type.CASH_GAME.ordinal -> {

@ -6,9 +6,20 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView
android:id="@+id/text"
android:textColor="@color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="8dp"
app:layout_constraintTop_toTopOf="@+id/chart"
android:layout_marginTop="8dp"/>
<com.github.mikephil.charting.charts.LineChart <com.github.mikephil.charting.charts.LineChart
android:id="@+id/chart" android:id="@+id/chart"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save