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