Add chart for actual vs expected income/expenses
This commit is contained in:
parent
e2b6593a80
commit
c7cdf2fe9c
5 changed files with 138 additions and 45 deletions
|
@ -54,6 +54,9 @@ dependencies {
|
|||
implementation "com.google.dagger:dagger:$dagger"
|
||||
kapt "com.google.dagger:dagger-compiler:$dagger"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
|
||||
|
||||
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
|
|
|
@ -6,9 +6,16 @@ import android.util.Log
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.github.mikephil.charting.components.AxisBase
|
||||
import com.github.mikephil.charting.components.XAxis
|
||||
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.formatter.ValueFormatter
|
||||
import com.wbrawner.budget.AllowanceApplication
|
||||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.R
|
||||
|
@ -30,6 +37,24 @@ class OverviewFragment : Fragment() {
|
|||
): View? = inflater.inflate(R.layout.fragment_overview, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
expectedActualChart.apply {
|
||||
setFitBars(true)
|
||||
xAxis.valueFormatter = object : ValueFormatter() {
|
||||
override fun getFormattedValue(value: Float): String {
|
||||
return if (value == 0f) "Income" else "Expenses"
|
||||
}
|
||||
|
||||
override fun getBarLabel(barEntry: BarEntry?): String {
|
||||
return super.getBarLabel(barEntry)
|
||||
}
|
||||
}
|
||||
xAxis.position = XAxis.XAxisPosition.BOTTOM
|
||||
xAxis.granularity = 1f
|
||||
xAxis.isGranularityEnabled = true
|
||||
setDrawGridBackground(false)
|
||||
axisLeft.axisMinimum = 0f
|
||||
axisRight.axisMinimum = 0f
|
||||
}
|
||||
viewModel.state.observe(viewLifecycleOwner, Observer { state ->
|
||||
when (state) {
|
||||
is AsyncState.Loading -> {
|
||||
|
@ -43,6 +68,30 @@ class OverviewFragment : Fragment() {
|
|||
progressBar.visibility = View.GONE
|
||||
activity?.title = state.data.budget.name
|
||||
balance.text = state.data.balance.toAmountSpannable(view.context)
|
||||
val expectedIncome = (state.data.expectedIncome / 100).toFloat()
|
||||
val expectedExpenses = (state.data.expectedExpenses / 100).toFloat()
|
||||
val actualIncome = (state.data.actualIncome / 100).toFloat()
|
||||
val actualExpenses = (state.data.actualExpenses / 100).toFloat()
|
||||
val max = maxOf(expectedIncome, expectedExpenses, actualIncome, actualExpenses)
|
||||
expectedActualChart.axisLeft.axisMaximum = max
|
||||
expectedActualChart.axisRight.axisMaximum = max
|
||||
expectedActualChart.data = BarData(
|
||||
BarDataSet(
|
||||
listOf(BarEntry(0f, expectedIncome), BarEntry(1f, expectedExpenses)),
|
||||
"Expected"
|
||||
).apply {
|
||||
color = ResourcesCompat.getColor(resources, R.color.colorSecondary, requireContext().theme)
|
||||
},
|
||||
BarDataSet(
|
||||
listOf(BarEntry(0f, actualIncome), BarEntry(1f, actualExpenses)),
|
||||
"Actual"
|
||||
).apply {
|
||||
color = ResourcesCompat.getColor(resources, R.color.colorAccent, requireContext().theme)
|
||||
}
|
||||
).apply {
|
||||
barWidth = 0.25f
|
||||
}
|
||||
expectedActualChart.groupBars(0f, 0.06f, 0.01f)
|
||||
}
|
||||
is AsyncState.Error -> {
|
||||
overviewContent.visibility = View.GONE
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.*
|
|||
import com.wbrawner.budget.AsyncState
|
||||
import com.wbrawner.budget.common.budget.Budget
|
||||
import com.wbrawner.budget.common.budget.BudgetRepository
|
||||
import com.wbrawner.budget.common.category.CategoryRepository
|
||||
import com.wbrawner.budget.common.transaction.TransactionRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
@ -14,6 +15,9 @@ class OverviewViewModel : ViewModel() {
|
|||
@Inject
|
||||
lateinit var budgetRepo: BudgetRepository
|
||||
|
||||
@Inject
|
||||
lateinit var categoryRepo: CategoryRepository
|
||||
|
||||
@Inject
|
||||
lateinit var transactionRepo: TransactionRepository
|
||||
|
||||
|
@ -27,9 +31,27 @@ class OverviewViewModel : ViewModel() {
|
|||
state.postValue(AsyncState.Loading)
|
||||
try {
|
||||
// TODO: Load expected and actual income/expense amounts as well
|
||||
var expectedExpenses = 0L
|
||||
var expectedIncome = 0L
|
||||
var actualExpenses = 0L
|
||||
var actualIncome = 0L
|
||||
categoryRepo.findAll(arrayOf(budget.id!!)).forEach { category ->
|
||||
val balance = categoryRepo.getBalance(category.id!!)
|
||||
if (category.expense) {
|
||||
expectedExpenses += category.amount
|
||||
actualExpenses += (balance * -1)
|
||||
} else {
|
||||
expectedIncome += category.amount
|
||||
actualIncome += balance
|
||||
}
|
||||
}
|
||||
state.postValue(AsyncState.Success(OverviewState(
|
||||
budget,
|
||||
budgetRepo.getBalance(budget.id!!)
|
||||
budgetRepo.getBalance(budget.id!!),
|
||||
expectedIncome,
|
||||
expectedExpenses,
|
||||
actualIncome,
|
||||
actualExpenses
|
||||
)))
|
||||
} catch (e: Exception) {
|
||||
state.postValue(AsyncState.Error(e))
|
||||
|
@ -41,5 +63,9 @@ class OverviewViewModel : ViewModel() {
|
|||
|
||||
data class OverviewState(
|
||||
val budget: Budget,
|
||||
val balance: Long
|
||||
val balance: Long,
|
||||
val expectedIncome: Long,
|
||||
val expectedExpenses: Long,
|
||||
val actualIncome: Long,
|
||||
val actualExpenses: Long,
|
||||
)
|
|
@ -1,50 +1,64 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/overviewContent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balanceLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/label_current_balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textSize="36sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/noData"
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/overview_no_data"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
android:layout_gravity="center">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/overviewContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balanceLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/label_current_balance" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textSize="36sp" />
|
||||
|
||||
<com.github.mikephil.charting.charts.HorizontalBarChart
|
||||
android:id="@+id/expectedActualChart"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
android:id="@+id/noData"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/overview_no_data"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
</ScrollView>
|
|
@ -8,4 +8,5 @@
|
|||
<color name="colorTextGreen">#388e3c</color>
|
||||
<color name="colorTextRed">#d32f2f</color>
|
||||
<color name="colorTextPrimaryInverted">#FFDADADA</color>
|
||||
<color name="colorSecondary">#888888</color>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue