From 7ebc53ec75a02004a13c61dc0c064ee441e4c9b0 Mon Sep 17 00:00:00 2001
From: "Moch. Sofyan Firdaus" <13521083@std.stei.itb.ac.id>
Date: Tue, 2 Apr 2024 02:04:29 +0700
Subject: [PATCH] feat: pie chart

---
 app/build.gradle.kts                          |   5 +-
 .../com/onionsquad/bondoman/MainActivity.kt   |  17 ++-
 .../repository/TransactionRepository.kt       |   2 +
 .../bondoman/room/TransactionDao.kt           |   6 ++
 .../bondoman/ui/graph/GraphFragment.kt        | 102 ++++++++++++++++--
 .../bondoman/ui/graph/GraphViewModel.kt       |  19 ++--
 .../res/drawable/ic_baseline_pie_chart_24.xml |   5 +
 .../drawable/ic_baseline_show_chart_24.xml    |   5 -
 app/src/main/res/layout/activity_main.xml     |   8 +-
 app/src/main/res/layout/fragment_graph.xml    |  26 ++++-
 app/src/main/res/menu/bottom_nav_menu.xml     |   2 +-
 app/src/main/res/values/dimens.xml            |   1 +
 app/src/main/res/values/strings.xml           |   1 +
 gradle/libs.versions.toml                     |   4 +-
 14 files changed, 169 insertions(+), 34 deletions(-)
 create mode 100644 app/src/main/res/drawable/ic_baseline_pie_chart_24.xml
 delete mode 100644 app/src/main/res/drawable/ic_baseline_show_chart_24.xml

diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 38f24a2..0b0bdf7 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,12 +51,13 @@ dependencies {
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)
 
-    implementation(libs.github.aachartmodel.core)
-
     // Room
     implementation(libs.room.runtime)
     implementation(libs.room.ktx)
     implementation(libs.room.common)
     annotationProcessor(libs.room.compiler)
     kapt(libs.room.compiler)
+
+    // Chart
+    implementation(libs.mpandroidchart)
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt b/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt
index d92d0a8..4212af3 100644
--- a/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/MainActivity.kt
@@ -1,12 +1,12 @@
 package com.onionsquad.bondoman
 
 import android.os.Bundle
-import com.google.android.material.bottomnavigation.BottomNavigationView
 import androidx.appcompat.app.AppCompatActivity
-import androidx.navigation.findNavController
+import androidx.navigation.fragment.findNavController
 import androidx.navigation.ui.AppBarConfiguration
 import androidx.navigation.ui.setupActionBarWithNavController
 import androidx.navigation.ui.setupWithNavController
+import com.google.android.material.bottomnavigation.BottomNavigationView
 import com.onionsquad.bondoman.databinding.ActivityMainBinding
 
 class MainActivity : AppCompatActivity() {
@@ -21,11 +21,18 @@ class MainActivity : AppCompatActivity() {
 
         val navView: BottomNavigationView = binding.navView
 
-        val navController = findNavController(R.id.nav_host_fragment_activity_main)
+        val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main)
+        val navController = navHostFragment!!.findNavController()
         // Passing each menu ID as a set of Ids because each
         // menu should be considered as top level destinations.
-        val appBarConfiguration = AppBarConfiguration(setOf(
-                R.id.navigation_transaction, R.id.navigation_scan, R.id.navigation_graph, R.id.navigation_settings))
+        val appBarConfiguration = AppBarConfiguration(
+            setOf(
+                R.id.navigation_transaction,
+                R.id.navigation_scan,
+                R.id.navigation_graph,
+                R.id.navigation_settings
+            )
+        )
         setupActionBarWithNavController(navController, appBarConfiguration)
         navView.setupWithNavController(navController)
     }
diff --git a/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt
index 6d31c4a..ac46a92 100644
--- a/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/repository/TransactionRepository.kt
@@ -5,6 +5,8 @@ import androidx.lifecycle.LiveData
 
 class TransactionRepository(private val transactionDao: TransactionDao) {
     val listTransactions: LiveData<List<TransactionEntity>> = transactionDao.getAllTransactions()
+    val listIncomes: LiveData<List<TransactionEntity>> = transactionDao.getAllIncomes()
+    val listOutcomes: LiveData<List<TransactionEntity>> = transactionDao.getAllOutcomes()
 
     suspend fun insertTransaction(transaction: TransactionEntity) {
         transactionDao.insertTransaction(transaction)
diff --git a/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt
index 35e64cc..31e75bd 100644
--- a/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/room/TransactionDao.kt
@@ -16,4 +16,10 @@ interface TransactionDao {
 
     @Delete
     suspend fun deleteTransaction(transaction: TransactionEntity)
+
+    @Query("SELECT * FROM transactions WHERE category = 'INCOME'")
+    fun getAllIncomes(): LiveData<List<TransactionEntity>>
+
+    @Query("SELECT * FROM transactions WHERE category = 'OUTCOME'")
+    fun getAllOutcomes(): LiveData<List<TransactionEntity>>
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphFragment.kt b/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphFragment.kt
index 9e1c929..29090db 100644
--- a/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphFragment.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphFragment.kt
@@ -1,30 +1,58 @@
 package com.onionsquad.bondoman.ui.graph
 
+import android.content.res.Configuration
+import android.graphics.Color
 import android.os.Bundle
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
+import com.github.mikephil.charting.components.Legend
+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 com.github.mikephil.charting.utils.ColorTemplate
 import com.onionsquad.bondoman.databinding.FragmentGraphBinding
+import java.text.DecimalFormat
 
 class GraphFragment : Fragment() {
 
     private var _binding: FragmentGraphBinding? = null
-
-    // This property is only valid between onCreateView and
-    // onDestroyView.
     private val binding get() = _binding!!
+    private var incomeEntry: PieEntry? = null
+    private var outcomeEntry: PieEntry? = null
 
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
         savedInstanceState: Bundle?
     ): View {
-        val graphViewModel =
-            ViewModelProvider(this).get(GraphViewModel::class.java)
-
         _binding = FragmentGraphBinding.inflate(inflater, container, false)
+        setupPieChart()
+
+        ViewModelProvider(this)[GraphViewModel::class.java].apply {
+            incomes.observe(viewLifecycleOwner) {
+                if (it.isNotEmpty()) {
+                    incomeEntry = PieEntry(it.sumOf { e -> e.amount }.toFloat(), "Income")
+                    Log.d("DATA", "$it")
+                } else {
+                    incomeEntry = null
+                }
+                refreshPieChart()
+            }
+            outcomes.observe(viewLifecycleOwner) {
+                if (it.isNotEmpty()) {
+                    outcomeEntry = PieEntry(it.sumOf { e -> e.amount }.toFloat(), "Outcome")
+                    Log.d("DATA", "$it")
+                } else {
+                    outcomeEntry = null
+                }
+                refreshPieChart()
+            }
+        }
 
         return binding.root
     }
@@ -33,4 +61,64 @@ class GraphFragment : Fragment() {
         super.onDestroyView()
         _binding = null
     }
-}
\ No newline at end of file
+
+    private fun setupPieChart() {
+        binding.pieChart.apply {
+            setUsePercentValues(true)
+            description.isEnabled = false
+            setExtraOffsets(5f, 10f, 5f, 5f)
+            dragDecelerationFrictionCoef = .95f
+            isDrawHoleEnabled = true
+            setHoleColor(Color.WHITE)
+            transparentCircleRadius = 58f
+        }
+    }
+
+    private fun refreshPieChart() {
+        binding.apply {
+            if (incomeEntry == null || outcomeEntry == null) {
+                textGraph.visibility = View.VISIBLE
+                pieChart.visibility = View.GONE
+            } else {
+                textGraph.visibility = View.GONE
+                pieChart.apply {
+                    visibility = View.VISIBLE
+                    data = PieData().apply {
+                        dataSet =
+                            PieDataSet(arrayListOf(incomeEntry, outcomeEntry), "Category").apply {
+                                colors = listOf(
+                                    ColorTemplate.rgb("#3498db"),
+                                    ColorTemplate.rgb("#e74c3c")
+                                )
+                            }
+                    }.apply {
+                        if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
+                            setValueTextSize(16f)
+                        else
+                            setValueTextSize(12f)
+                        setValueTextColor(Color.WHITE)
+                        setValueFormatter(object : ValueFormatter() {
+                            override fun getFormattedValue(value: Float): String {
+                                return DecimalFormat("###.0").format(value) + "%"
+                            }
+                        })
+                    }
+                    setDrawEntryLabels(false)
+                    centerText = "Transactions"
+                    if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT)
+                        setCenterTextSize(20f)
+                    legend.apply {
+                        verticalAlignment = Legend.LegendVerticalAlignment.TOP
+                        horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT
+                        orientation = Legend.LegendOrientation.VERTICAL
+                        xEntrySpace = 7f
+                        yEntrySpace = 0f
+                        yOffset = 0f
+                    }
+                    invalidate()
+                }
+            }
+        }
+    }
+}
+
diff --git a/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphViewModel.kt b/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphViewModel.kt
index c3ec681..b73f52f 100644
--- a/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphViewModel.kt
+++ b/app/src/main/java/com/onionsquad/bondoman/ui/graph/GraphViewModel.kt
@@ -1,13 +1,18 @@
 package com.onionsquad.bondoman.ui.graph
 
+import android.app.Application
+import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
+import com.onionsquad.bondoman.repository.TransactionRepository
+import com.onionsquad.bondoman.room.TransactionDatabase
+import com.onionsquad.bondoman.room.TransactionEntity
 
-class GraphViewModel : ViewModel() {
+class GraphViewModel(app: Application) : AndroidViewModel(app) {
+
+    private val database by lazy { TransactionDatabase.getInstance(getApplication<Application>().applicationContext) }
+    private val repository by lazy { TransactionRepository(database.transactionDao()) }
+
+    val incomes: LiveData<List<TransactionEntity>> get() = repository.listIncomes
+    val outcomes: LiveData<List<TransactionEntity>> get() = repository.listOutcomes
 
-    private val _text = MutableLiveData<String>().apply {
-        value = "This is home Fragment"
-    }
-    val text: LiveData<String> = _text
 }
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_pie_chart_24.xml b/app/src/main/res/drawable/ic_baseline_pie_chart_24.xml
new file mode 100644
index 0000000..1a2b7ab
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_pie_chart_24.xml
@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M11,2v20c-5.07,-0.5 -9,-4.79 -9,-10s3.93,-9.5 9,-10zM13.03,2v8.99L22,10.99c-0.47,-4.74 -4.24,-8.52 -8.97,-8.99zM13.03,13.01L13.03,22c4.74,-0.47 8.5,-4.25 8.97,-8.99h-8.97z"/>
+    
+</vector>
diff --git a/app/src/main/res/drawable/ic_baseline_show_chart_24.xml b/app/src/main/res/drawable/ic_baseline_show_chart_24.xml
deleted file mode 100644
index 3a6c503..0000000
--- a/app/src/main/res/drawable/ic_baseline_show_chart_24.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
-      
-    <path android:fillColor="@android:color/white" android:pathData="M3.5,18.49l6,-6.01 4,4L22,6.92l-1.41,-1.41 -7.09,7.97 -4,-4L2,16.99z"/>
-    
-</vector>
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 06ea6ca..08de7cb 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -4,7 +4,7 @@
     android:id="@+id/container"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:paddingTop="?attr/actionBarSize">
+    android:paddingTop="10dp">
 
     <com.google.android.material.bottomnavigation.BottomNavigationView
         android:id="@+id/nav_view"
@@ -18,16 +18,18 @@
         app:layout_constraintRight_toRightOf="parent"
         app:menu="@menu/bottom_nav_menu" />
 
-    <fragment
+    <androidx.fragment.app.FragmentContainerView
         android:id="@+id/nav_host_fragment_activity_main"
         android:name="androidx.navigation.fragment.NavHostFragment"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="0dp"
         app:defaultNavHost="true"
         app:layout_constraintBottom_toTopOf="@id/nav_view"
+        app:layout_constraintHorizontal_bias="1.0"
         app:layout_constraintLeft_toLeftOf="parent"
         app:layout_constraintRight_toRightOf="parent"
         app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="1.0"
         app:navGraph="@navigation/mobile_navigation" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_graph.xml b/app/src/main/res/layout/fragment_graph.xml
index ebd46fe..292afe7 100644
--- a/app/src/main/res/layout/fragment_graph.xml
+++ b/app/src/main/res/layout/fragment_graph.xml
@@ -1,7 +1,29 @@
 <?xml version="1.0" encoding="utf-8"?>
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context=".ui.graph.GraphFragment"/>
\ No newline at end of file
+    tools:context=".ui.graph.GraphFragment">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TextView
+            android:id="@+id/text_graph"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:text="@string/graph_no_data"
+            android:textSize="@dimen/graph_no_data_text"
+            android:visibility="gone"/>
+
+        <com.github.mikephil.charting.charts.PieChart
+            android:id="@+id/pie_chart"
+            android:visibility="gone"
+            android:layout_centerInParent="true"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+    </RelativeLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml
index 1054af7..d1ae298 100644
--- a/app/src/main/res/menu/bottom_nav_menu.xml
+++ b/app/src/main/res/menu/bottom_nav_menu.xml
@@ -13,7 +13,7 @@
 
     <item
         android:id="@+id/navigation_graph"
-        android:icon="@drawable/ic_baseline_show_chart_24"
+        android:icon="@drawable/ic_baseline_pie_chart_24"
         android:title="@string/title_graph" />
 
     <item
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index e00c2dd..f494689 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -2,4 +2,5 @@
     <!-- Default screen margins, per the Android Design guidelines. -->
     <dimen name="activity_horizontal_margin">16dp</dimen>
     <dimen name="activity_vertical_margin">16dp</dimen>
+    <dimen name="graph_no_data_text">20sp</dimen>
 </resources>
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ad704cd..8d737ae 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,4 +4,5 @@
     <string name="title_scan">Scan</string>
     <string name="title_graph">Graph</string>
     <string name="title_settings">Settings</string>
+    <string name="graph_no_data">No data</string>
 </resources>
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 98d875a..10345b9 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,10 +12,10 @@ lifecycleLivedataKtx = "2.6.1"
 lifecycleViewmodelKtx = "2.6.1"
 navigationFragmentKtx = "2.6.0"
 navigationUiKtx = "2.6.0"
-aaChartCore = "7.2.1"
 room = "2.6.1"
 kapt = "1.9.23"
 lifecycleViewmodelCompose = "2.7.0"
+chart = "v3.1.0"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -29,12 +29,12 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy
 androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
 androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
 androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
-github-aachartmodel-core = { group = "com.github.AAChartModel", name = "AAChartCore-Kotlin", version.ref = "aaChartCore" }
 room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
 room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
 room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
 room-common = { group = "androidx.room", name = "room-common", version.ref = "room" }
 androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" }
+mpandroidchart = { group = "com.github.PhilJay", name = "MPAndroidChart", version.ref = "chart" }
 
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }
-- 
GitLab