From 7a1ed4f83ecc476849f7b582f069c449b8cff580 Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Mon, 4 Mar 2024 00:02:41 -0600 Subject: [PATCH] feat: more charts | code cleanup --- .../dantotsu/connections/anilist/api/Data.kt | 2 +- .../java/ani/dantotsu/profile/ChartBuilder.kt | 321 ++++++++ .../java/ani/dantotsu/profile/ChartItem.kt | 26 + .../ani/dantotsu/profile/ProfileActivity.kt | 14 +- .../ani/dantotsu/profile/ProfileFragment.kt | 26 +- .../ani/dantotsu/profile/StatsFragment.kt | 736 ++++++++---------- app/src/main/res/drawable/ic_open_24.xml | 9 + .../main/res/layout/fragment_statistics.xml | 384 +-------- app/src/main/res/layout/item_chart.xml | 54 ++ 9 files changed, 782 insertions(+), 790 deletions(-) create mode 100644 app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt create mode 100644 app/src/main/java/ani/dantotsu/profile/ChartItem.kt create mode 100644 app/src/main/res/drawable/ic_open_24.xml create mode 100644 app/src/main/res/layout/item_chart.xml diff --git a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt index c5e5c7e4..98dd1e7d 100644 --- a/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt +++ b/app/src/main/java/ani/dantotsu/connections/anilist/api/Data.kt @@ -211,7 +211,7 @@ class Query { val statistics: NNUserStatisticTypes, @SerialName("siteUrl") val siteUrl: String, - ) + ): java.io.Serializable @Serializable data class NNUserStatisticTypes( diff --git a/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt b/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt new file mode 100644 index 00000000..5d19b026 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt @@ -0,0 +1,321 @@ +package ani.dantotsu.profile + +import android.content.Context +import android.graphics.Color +import android.util.TypedValue +import com.github.aachartmodel.aainfographics.aachartcreator.AAChartModel +import com.github.aachartmodel.aainfographics.aachartcreator.AAChartStackingType +import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType +import com.github.aachartmodel.aainfographics.aachartcreator.AAChartZoomType +import com.github.aachartmodel.aainfographics.aachartcreator.AADataElement +import com.github.aachartmodel.aainfographics.aachartcreator.AAOptions +import com.github.aachartmodel.aainfographics.aachartcreator.AASeriesElement +import com.github.aachartmodel.aainfographics.aachartcreator.aa_toAAOptions +import com.github.aachartmodel.aainfographics.aaoptionsmodel.AADataLabels +import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAItemStyle +import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAScrollablePlotArea +import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAStyle +import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAYAxis +import com.github.aachartmodel.aainfographics.aatools.AAColor + +class ChartBuilder { + companion object { + enum class ChartType { + OneDimensional, TwoDimensional + } + + enum class StatType { + COUNT, TIME, MEAN_SCORE + } + + enum class MediaType { + ANIME, MANGA + } + + fun buildChart( + context: Context, + chartType: ChartType, + aaChartType: AAChartType, + statType: StatType, + mediaType: MediaType, + names: List, + statData: List, + xAxisName: String = "X Axis", + xAxisTickInterval: Int? = null, + polar: Boolean = false, + categories: List? = null, + scrollPos: Float? = null, + ): AAOptions { + val typedValue = TypedValue() + context.theme.resolveAttribute( + com.google.android.material.R.attr.colorPrimary, + typedValue, + true + ) + val primaryColor = typedValue.data + val palette = generateColorPalette(primaryColor, names.size) + val aaChartModel = when (chartType) { + ChartType.OneDimensional -> { + val chart = AAChartModel() + .chartType(aaChartType) + .subtitle(getTypeName(statType, mediaType)) + .zoomType(AAChartZoomType.None) + .dataLabelsEnabled(true) + .series( + get1DElements( + names, + statData, + palette, + primaryColor + ) + ) + xAxisTickInterval?.let { chart.xAxisTickInterval(it) } + categories?.let { chart.categories(it.toTypedArray()) } + chart + } + + ChartType.TwoDimensional -> { + val hexColorsArray: Array = + palette.map { String.format("#%06X", 0xFFFFFF and it) }.toTypedArray() + val chart = AAChartModel() + .chartType(aaChartType) + .subtitle(getTypeName(statType, mediaType)) + .zoomType(AAChartZoomType.None) + .dataLabelsEnabled(false) + .yAxisTitle(getTypeName(statType, mediaType)) + .stacking(AAChartStackingType.Normal) + .series(get2DElements(names, statData, primaryColor)) + .colorsTheme(hexColorsArray) + + xAxisTickInterval?.let { chart.xAxisTickInterval(it) } + categories?.let { chart.categories(it.toTypedArray()) } + + chart + } + } + val aaOptions = aaChartModel.aa_toAAOptions() + aaOptions.chart?.polar = polar + aaOptions.tooltip?.apply { + headerFormat + formatter( + getToolTipFunction( + chartType, + xAxisName, + getTypeName(statType, mediaType) + ) + ) + } + aaOptions.legend?.apply { + enabled(true) + .labelFormat = "{name}: {y}" + } + aaOptions.plotOptions?.series?.connectNulls(true) + aaOptions.chart?.panning = true + + scrollPos?.let { + aaOptions.chart?.scrollablePlotArea(AAScrollablePlotArea().scrollPositionX(scrollPos)) + aaOptions.chart?.scrollablePlotArea?.minWidth((context.resources.displayMetrics.widthPixels.toFloat() / context.resources.displayMetrics.density) * (names.size.toFloat() / 18.0f)) + } + val min = statData.minOfOrNull { it.toDouble() } ?: 0.0 + val max = statData.maxOfOrNull { it.toDouble() } ?: 0.0 + val aaYaxis = AAYAxis().min(min).max(max) + val tickInterval = when (max) { + in 0.0..10.0 -> 1.0 + in 10.0..30.0 -> 5.0 + in 30.0..100.0 -> 10.0 + in 100.0..1000.0 -> 100.0 + in 1000.0..10000.0 -> 1000.0 + else -> 10000.0 + } + aaYaxis.tickInterval(tickInterval) + aaOptions.yAxis(aaYaxis) + + setColors(aaOptions, context, primaryColor) + + return aaOptions + } + + private fun get2DElements( + names: List, + statData: List, + primaryColor: Int + ): Array { + val statValues = mutableListOf>() + for (i in statData.indices) { + statValues.add(arrayOf(names[i], statData[i], statData[i])) + } + return arrayOf( + AASeriesElement().name("Score") + .data(statValues.toTypedArray()) + .dataLabels( + AADataLabels() + .enabled(false) + ) + .colorByPoint(true) + .fillColor(AAColor.rgbaColor( + Color.red(primaryColor), + Color.green(primaryColor), + Color.blue(primaryColor), + 0.9f + )) + ) + } + + private fun get1DElements( + names: List, + statData: List, + colors: List, + primaryColor: Int + ): Array { + val statDataElements = mutableListOf() + for (i in statData.indices) { + val element = AADataElement() + .y(statData[i]) + .color( + AAColor.rgbaColor( + Color.red(colors[i]), + Color.green(colors[i]), + Color.blue(colors[i]), + 0.9f + ) + ) + if (names[i] is Number) { + element.x(names[i] as Number) + element.dataLabels( + AADataLabels() + .enabled(false) + .format("{point.y}") + .backgroundColor(AAColor.rgbaColor(255, 255, 255, 0.0f)) + ) + } else { + element.x(i) + element.name(names[i] as String) + } + statDataElements.add(element) + } + return arrayOf( + AASeriesElement().name("Score").color(primaryColor) + .data(statDataElements.toTypedArray()) + ) + } + + private fun getTypeName(statType: StatType, mediaType: MediaType): String { + return when (statType) { + StatType.COUNT -> "Count" + StatType.TIME -> if (mediaType == MediaType.ANIME) "Hours Watched" else "Chapters Read" + StatType.MEAN_SCORE -> "Mean Score" + } + } + + private fun generateColorPalette( + baseColor: Int, + size: Int, + hueDelta: Float = 8f, + saturationDelta: Float = 2.02f, + valueDelta: Float = 2.02f + ): List { + val palette = mutableListOf() + val hsv = FloatArray(3) + Color.colorToHSV(baseColor, hsv) + + for (i in 0 until size) { + val newHue = + (hsv[0] + hueDelta * i) % 360 // Ensure hue stays within the 0-360 range + val newSaturation = (hsv[1] + saturationDelta * i).coerceIn(0f, 1f) + val newValue = (hsv[2] + valueDelta * i).coerceIn(0f, 1f) + + val newHsv = floatArrayOf(newHue, newSaturation, newValue) + palette.add(Color.HSVToColor(newHsv)) + } + + return palette + } + + private fun setColors(aaOptions: AAOptions, context: Context, primaryColor: Int) { + val backgroundColor = TypedValue() + context.theme.resolveAttribute( + com.google.android.material.R.attr.colorSurfaceVariant, + backgroundColor, + true + ) + val backgroundStyle = AAStyle().color( + AAColor.rgbaColor( + Color.red(backgroundColor.data), + Color.green(backgroundColor.data), + Color.blue(backgroundColor.data), + 1f + ) + ) + val colorOnBackground = TypedValue() + context.theme.resolveAttribute( + com.google.android.material.R.attr.colorOnSurface, + colorOnBackground, + true + ) + val onBackgroundStyle = AAStyle().color( + AAColor.rgbaColor( + Color.red(colorOnBackground.data), + Color.green(colorOnBackground.data), + Color.blue(colorOnBackground.data), + 0.9f + ) + ) + + + aaOptions.chart?.backgroundColor(backgroundStyle.color) + aaOptions.tooltip?.backgroundColor( + AAColor.rgbaColor( + Color.red(primaryColor), + Color.green(primaryColor), + Color.blue(primaryColor), + 0.9f + ) + ) + aaOptions.title?.style(onBackgroundStyle) + aaOptions.subtitle?.style(onBackgroundStyle) + aaOptions.tooltip?.style(backgroundStyle) + aaOptions.credits?.style(onBackgroundStyle) + aaOptions.xAxis?.labels?.style(onBackgroundStyle) + aaOptions.yAxis?.labels?.style(onBackgroundStyle) + aaOptions.plotOptions?.series?.dataLabels?.style(onBackgroundStyle) + aaOptions.plotOptions?.series?.dataLabels?.backgroundColor(backgroundStyle.color) + aaOptions.legend?.itemStyle(AAItemStyle().color(onBackgroundStyle.color)) + + aaOptions.touchEventEnabled(true) + } + + private fun getToolTipFunction( + chartType: ChartType, + type: String, + typeName: String + ): String { + return when (chartType) { + ChartType.OneDimensional -> { + """ + function () { + return this.point.name + + ':
' + + ' ' + + this.y + + ', ' + + (this.percentage).toFixed(2) + + '%' + } + """.trimIndent() + } + + ChartType.TwoDimensional -> { + """ + function () { + return '$type: ' + + this.x + + '
' + + ' $typeName ' + + this.y + } + """.trimIndent() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/ChartItem.kt b/app/src/main/java/ani/dantotsu/profile/ChartItem.kt new file mode 100644 index 00000000..009c44f3 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/profile/ChartItem.kt @@ -0,0 +1,26 @@ +package ani.dantotsu.profile + +import android.view.View +import ani.dantotsu.R +import ani.dantotsu.databinding.ItemChartBinding +import com.github.aachartmodel.aainfographics.aachartcreator.AAOptions +import com.xwray.groupie.viewbinding.BindableItem + +class ChartItem( + private val title: String, + private val aaOptions: AAOptions): BindableItem() { + private lateinit var binding: ItemChartBinding + override fun bind(viewBinding: ItemChartBinding, position: Int) { + binding = viewBinding + binding.typeText.text = title + binding.chartView.aa_drawChartWithChartOptions(aaOptions) + } + + override fun getLayout(): Int { + return R.layout.item_chart + } + + override fun initializeViewBinding(view: View): ItemChartBinding { + return ItemChartBinding.bind(view) + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt index 05a452f0..6e556449 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileActivity.kt @@ -13,6 +13,7 @@ import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.api.Query @@ -39,6 +40,7 @@ class ProfileActivity : AppCompatActivity(){ private lateinit var binding: ActivityProfileBinding private var selected: Int = 0 private lateinit var navBar: AnimatedBottomBar + @SuppressLint("SetTextI18n") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -65,7 +67,7 @@ class ProfileActivity : AppCompatActivity(){ } withContext(Dispatchers.Main) { binding.profileViewPager.updateLayoutParams { bottomMargin = navBarHeight } - binding.profileViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, user, this@ProfileActivity) + binding.profileViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, user) navBar.visibility = View.VISIBLE navBar.selectTabAt(selected) navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener { @@ -129,20 +131,18 @@ class ProfileActivity : AppCompatActivity(){ } super.onResume() } - private class ViewPagerAdapter( fragmentManager: FragmentManager, lifecycle: Lifecycle, - private val user: Query.UserProfile, - private val activity: ProfileActivity + private val user: Query.UserProfile ) : FragmentStateAdapter(fragmentManager, lifecycle) { override fun getItemCount(): Int = 2 override fun createFragment(position: Int): Fragment = when (position) { - 0 -> ProfileFragment(user, activity) - 1 -> StatsFragment(user, activity) - else -> ProfileFragment(user, activity) + 0 -> ProfileFragment.newInstance(user) + 1 -> StatsFragment.newInstance(user) + else -> ProfileFragment.newInstance(user) } } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt index 7ff3f0cc..e8965f19 100644 --- a/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/ProfileFragment.kt @@ -13,7 +13,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import ani.dantotsu.bottomBar import ani.dantotsu.buildMarkwon import ani.dantotsu.connections.anilist.ProfileViewModel import ani.dantotsu.connections.anilist.api.Query @@ -27,8 +26,10 @@ import ani.dantotsu.setSlideUp import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -class ProfileFragment(private val user: Query.UserProfile, private val activity: ProfileActivity): Fragment() { +class ProfileFragment(): Fragment() { lateinit var binding: FragmentProfileBinding + private lateinit var activity: ProfileActivity + private lateinit var user: Query.UserProfile override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -41,6 +42,8 @@ class ProfileFragment(private val user: Query.UserProfile, private val activity: val model: ProfileViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + activity = requireActivity() as ProfileActivity + user = arguments?.getSerializable("user") as Query.UserProfile val markwon = buildMarkwon(activity, false) markwon.setMarkdown(binding.profileUserBio, user.about?:"") binding.userInfoContainer.visibility = if (user.about != null) View.VISIBLE else View.GONE @@ -96,6 +99,14 @@ class ProfileFragment(private val user: Query.UserProfile, private val activity: binding.profileFavManga ) } + + override fun onResume() { + super.onResume() + if (this::binding.isInitialized) { + binding.root.requestLayout() + } + } + private fun initRecyclerView( mode: LiveData>, container: View, @@ -135,4 +146,15 @@ class ProfileFragment(private val user: Query.UserProfile, private val activity: } } + companion object { + fun newInstance(query: Query.UserProfile): ProfileFragment { + val args = Bundle().apply { + putSerializable("user", query) + } + return ProfileFragment().apply { + arguments = args + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt index 8bd8b671..7049383f 100644 --- a/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt +++ b/app/src/main/java/ani/dantotsu/profile/StatsFragment.kt @@ -1,48 +1,37 @@ package ani.dantotsu.profile -import android.content.Context -import android.graphics.Color import android.os.Bundle -import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager import ani.dantotsu.R import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.databinding.FragmentStatisticsBinding -import com.github.aachartmodel.aainfographics.aachartcreator.AAChartModel -import com.github.aachartmodel.aainfographics.aachartcreator.AAChartStackingType +import ani.dantotsu.profile.ChartBuilder.Companion.ChartType +import ani.dantotsu.profile.ChartBuilder.Companion.StatType +import ani.dantotsu.profile.ChartBuilder.Companion.MediaType import com.github.aachartmodel.aainfographics.aachartcreator.AAChartType -import com.github.aachartmodel.aainfographics.aachartcreator.AAChartZoomType -import com.github.aachartmodel.aainfographics.aachartcreator.AADataElement -import com.github.aachartmodel.aainfographics.aachartcreator.AAOptions -import com.github.aachartmodel.aainfographics.aachartcreator.AASeriesElement -import com.github.aachartmodel.aainfographics.aachartcreator.aa_toAAOptions -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAArea -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAChart -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AADataLabels -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAItemStyle -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAMarker -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAPlotOptions -import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAStyle import com.github.aachartmodel.aainfographics.aaoptionsmodel.AAYAxis -import com.github.aachartmodel.aainfographics.aatools.AAColor +import com.xwray.groupie.GroupieAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale -class StatsFragment(private val user: Query.UserProfile, private val activity: ProfileActivity) : +class StatsFragment() : Fragment() { private lateinit var binding: FragmentStatisticsBinding + private var adapter: GroupieAdapter = GroupieAdapter() private var stats: Query.StatisticsResponse? = null private var type: MediaType = MediaType.ANIME private var statType: StatType = StatType.COUNT - private var primaryColor: Int = 0 + private lateinit var user: Query.UserProfile + private lateinit var activity: ProfileActivity override fun onCreateView( inflater: LayoutInflater, @@ -55,14 +44,14 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + activity = requireActivity() as ProfileActivity + user = arguments?.getSerializable("user") as Query.UserProfile - val typedValue = TypedValue() - activity.theme.resolveAttribute( - com.google.android.material.R.attr.colorPrimary, - typedValue, - true - ) - primaryColor = typedValue.data + binding.statisticList.adapter = adapter + binding.statisticList.setHasFixedSize(true) + binding.statisticList.isNestedScrollingEnabled = false + binding.statisticList.layoutManager = LinearLayoutManager(requireContext()) + binding.statisticProgressBar.visibility = View.VISIBLE binding.sourceType.setAdapter( ArrayAdapter( @@ -94,161 +83,39 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P } loadStats(type == MediaType.ANIME) binding.statisticProgressBar.visibility = View.GONE - binding.chartsContainer.visibility = View.VISIBLE } } } override fun onResume() { super.onResume() + if (this::binding.isInitialized) { + binding.root.requestLayout() + } loadStats(type == MediaType.ANIME) } private fun loadStats(anime: Boolean) { - val formatChartModel = getFormatChartModel(anime) - if (formatChartModel != null) { - binding.formatChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(formatChartModel) - binding.formatChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.formatChartView.visibility = View.GONE - } - - val statusChartModel = getStatusChartModel(anime) - if (statusChartModel != null) { - binding.statusChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(statusChartModel) - binding.statusChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.statusChartView.visibility = View.GONE - } - - val scoreChartModel = getScoreChartModel(anime) - if (scoreChartModel != null) { - binding.scoreChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(scoreChartModel, false, """ - function () { - return 'score: ' + - this.x + - '
' + - ' ${getTypeName()} ' + - this.y - } - """.trimIndent() - ) - binding.scoreChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.scoreChartView.visibility = View.GONE - } - - val lengthChartModel = getLengthChartModel(anime) - if (lengthChartModel != null) { - binding.lengthChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(lengthChartModel) - binding.lengthChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.lengthChartView.visibility = View.GONE - } - - val releaseYearChartModel = getReleaseYearChartModel(anime) - if (releaseYearChartModel != null) { - binding.releaseYearChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(releaseYearChartModel, false, """ - function () { - return 'Year: ' + - this.x + - '
' + - ' ${getTypeName()} ' + - this.y - } - """.trimIndent() - ) - binding.releaseYearChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.releaseYearChartView.visibility = View.GONE - } - - val startYearChartModel = getStartYearChartModel(anime) - if (startYearChartModel != null) { - binding.startYearChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(startYearChartModel, false, """ - function () { - return 'Year: ' + - this.x + - '
' + - ' ${getTypeName()} ' + - this.y - } - """.trimIndent() - ) - binding.startYearChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.startYearChartView.visibility = View.GONE - } - - val genreChartModel = getGenreChartModel(anime) - if (genreChartModel.first != null) { - binding.genreChartView.visibility = View.VISIBLE - val aaOptions = buildOptions(genreChartModel.first!!, true, """ - function () { - return 'Genre: ' + - this.x + - '
' + - ' ${getTypeName()} ' + - this.y - } - """.trimIndent() - ) - val min = genreChartModel.second.first - val max = genreChartModel.second.second - aaOptions.yAxis = AAYAxis().min(min).max(max).tickInterval(if (max > 100) 20 else 10) - binding.genreChartView.aa_drawChartWithChartOptions(aaOptions) - } else { - binding.genreChartView.visibility = View.GONE - } - - + binding.statisticProgressBar.visibility = View.VISIBLE + binding.statisticList.visibility = View.GONE + adapter.clear() + loadFormatChart(anime) + loadScoreChart(anime) + loadStatusChart(anime) + loadReleaseYearChart(anime) + loadStartYearChart(anime) + loadLengthChart(anime) + loadGenreChart(anime) + loadTagChart(anime) + loadCountryChart(anime) + loadVoiceActorsChart(anime) + loadStudioChart(anime) + loadStaffChart(anime) + binding.statisticProgressBar.visibility = View.GONE + binding.statisticList.visibility = View.VISIBLE } - private fun buildOptions( - aaChartModel: AAChartModel, - polar: Boolean = true, - formatting: String? = null - ): AAOptions { - val aaOptions = aaChartModel.aa_toAAOptions() - aaOptions.chart?.zoomType = "xy" - aaOptions.chart?.pinchType = "xy" - aaOptions.chart?.polar = polar - aaOptions.tooltip?.apply { - headerFormat - if (formatting != null) { - formatter(formatting) - } else { - formatter( - """ - function () { - return this.point.name - + ':
' - + ' ' - + this.y - + ', ' - + (this.percentage).toFixed(2) - + '%' - } - """.trimIndent() - ) - } - } - aaOptions.legend?.apply { - enabled(true) - .labelFormat = "{name}: {y}" - } - aaOptions.plotOptions?.series?.connectNulls(true) - setColors(aaOptions) - return aaOptions - } - - private fun getFormatChartModel(anime: Boolean): AAChartModel? { + private fun loadFormatChart(anime: Boolean) { val names: List = if (anime) { stats?.data?.user?.statistics?.anime?.formats?.map { it.format } ?: emptyList() } else { @@ -267,19 +134,21 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.formats?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return null - val primaryColor = primaryColor - val palette = generateColorPalette(primaryColor, names.size) - return AAChartModel() - .chartType(AAChartType.Pie) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(true) - .series(getElements(names, values, palette)) + if (names.isNotEmpty() || values.isNotEmpty()) { + val formatChart = ChartBuilder.buildChart( + activity, + ChartType.OneDimensional, + AAChartType.Pie, + statType, + type, + names, + values + ) + adapter.add(ChartItem("Format", formatChart)) + } } - private fun getStatusChartModel(anime: Boolean): AAChartModel? { + private fun loadStatusChart(anime: Boolean) { val names: List = if (anime) { stats?.data?.user?.statistics?.anime?.statuses?.map { it.status } ?: emptyList() } else { @@ -298,18 +167,21 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.statuses?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return null - val palette = generateColorPalette(primaryColor, names.size) - return AAChartModel() - .chartType(AAChartType.Funnel) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(true) - .series(getElements(names, values, palette)) + if (names.isNotEmpty() || values.isNotEmpty()) { + val statusChart = ChartBuilder.buildChart( + activity, + ChartType.OneDimensional, + AAChartType.Funnel, + statType, + type, + names, + values + ) + adapter.add(ChartItem("Status", statusChart)) + } } - private fun getScoreChartModel(anime: Boolean): AAChartModel? { + private fun loadScoreChart(anime: Boolean) { val names: List = if (anime) { stats?.data?.user?.statistics?.anime?.scores?.map { it.score } ?: emptyList() } else { @@ -328,25 +200,28 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.scores?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return null - val palette = generateColorPalette(primaryColor, names.size) - return AAChartModel() - .chartType(AAChartType.Column) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(false) - .yAxisTitle(getTypeName()) - .xAxisTickInterval(10) - .stacking(AAChartStackingType.Normal) - .series(getElements(names, values, palette)) + if (names.isNotEmpty() || values.isNotEmpty()) { + val scoreChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Column, + statType, + type, + names, + values, + xAxisName = "Score", + ) + adapter.add(ChartItem("Score", scoreChart)) + } } - private fun getLengthChartModel(anime: Boolean): AAChartModel? { + private fun loadLengthChart(anime: Boolean) { val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.lengths?.map { it.length?: "unknown" } ?: emptyList() + stats?.data?.user?.statistics?.anime?.lengths?.map { it.length ?: "unknown" } + ?: emptyList() } else { - stats?.data?.user?.statistics?.manga?.lengths?.map { it.length?: "unknown" } ?: emptyList() + stats?.data?.user?.statistics?.manga?.lengths?.map { it.length ?: "unknown" } + ?: emptyList() } val values: List = if (anime) { when (statType) { @@ -361,23 +236,28 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.lengths?.map { it.meanScore } } ?: emptyList() } - //clear nulls from names - if (names.isEmpty() || values.isEmpty()) - return null - val palette = generateColorPalette(primaryColor, names.size) - return AAChartModel() - .chartType(AAChartType.Pyramid) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(true) - .series(getElements(names, values, palette)) + if (names.isNotEmpty() || values.isNotEmpty()) { + val lengthChart = ChartBuilder.buildChart( + activity, + ChartType.OneDimensional, + AAChartType.Pyramid, + statType, + type, + names, + values, + xAxisName = "Length", + ) + adapter.add(ChartItem("Length", lengthChart)) + } } - private fun getReleaseYearChartModel(anime: Boolean): AAChartModel? { + private fun loadReleaseYearChart(anime: Boolean) { val names: List = if (anime) { - stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.releaseYear } ?: emptyList() + stats?.data?.user?.statistics?.anime?.releaseYears?.map { it.releaseYear } + ?: emptyList() } else { - stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.releaseYear } ?: emptyList() + stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.releaseYear } + ?: emptyList() } val values: List = if (anime) { when (statType) { @@ -392,22 +272,22 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.releaseYears?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return null - val palette = generateColorPalette(primaryColor, names.size) - val hexColorsArray: Array = palette.map { String.format("#%06X", 0xFFFFFF and it) }.toTypedArray() - return AAChartModel() - .chartType(AAChartType.Bubble) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(false) - .yAxisTitle(getTypeName()) - .stacking(AAChartStackingType.Normal) - .series(getElementsSimple(names, values)) - .colorsTheme(hexColorsArray) + if (names.isNotEmpty() || values.isNotEmpty()) { + val releaseYearChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Bubble, + statType, + type, + names, + values, + xAxisName = "Year", + ) + adapter.add(ChartItem("Release Year", releaseYearChart)) + } } - private fun getStartYearChartModel(anime: Boolean): AAChartModel? { + private fun loadStartYearChart(anime: Boolean) { val names: List = if (anime) { stats?.data?.user?.statistics?.anime?.startYears?.map { it.startYear } ?: emptyList() } else { @@ -426,22 +306,22 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.startYears?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return null - val palette = generateColorPalette(primaryColor, names.size) - val hexColorsArray: Array = palette.map { String.format("#%06X", 0xFFFFFF and it) }.toTypedArray() - return AAChartModel() - .chartType(AAChartType.Bar) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(false) - .yAxisTitle(getTypeName()) - .stacking(AAChartStackingType.Normal) - .series(getElementsSimple(names, values)) - .colorsTheme(hexColorsArray) + if (names.isNotEmpty() || values.isNotEmpty()) { + val startYearChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Bar, + statType, + type, + names, + values, + xAxisName = "Year", + ) + adapter.add(ChartItem("Start Year", startYearChart)) + } } - private fun getGenreChartModel(anime: Boolean): Pair> { + private fun loadGenreChart(anime: Boolean) { val names: List = if (anime) { stats?.data?.user?.statistics?.anime?.genres?.map { it.genre } ?: emptyList() } else { @@ -460,163 +340,219 @@ class StatsFragment(private val user: Query.UserProfile, private val activity: P StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.genres?.map { it.meanScore } } ?: emptyList() } - if (names.isEmpty() || values.isEmpty()) - return Pair(null, Pair(0, 0)) - val palette = generateColorPalette(primaryColor, names.size) - val hexColorsArray: Array = palette.map { String.format("#%06X", 0xFFFFFF and it) }.toTypedArray() - return Pair(AAChartModel() - .chartType(AAChartType.Area) - .subtitle(getTypeName()) - .zoomType(AAChartZoomType.XY) - .dataLabelsEnabled(false) - .legendEnabled(false) - .yAxisTitle(getTypeName()) - .stacking(AAChartStackingType.Normal) - .series(getElementsSimple(names, values)) - .colorsTheme(hexColorsArray) - .categories(names.toTypedArray()), - Pair(values.minOf { it.toInt() }, values.maxOf { it.toInt() })) - } - - enum class StatType { - COUNT, TIME, MEAN_SCORE - } - - enum class MediaType { - ANIME, MANGA - } - - private fun getTypeName(): String { - return when (statType) { - StatType.COUNT -> "Count" - StatType.TIME -> if (type == MediaType.ANIME) "Hours Watched" else "Chapters Read" - StatType.MEAN_SCORE -> "Mean Score" + if (names.isNotEmpty() || values.isNotEmpty()) { + val genreChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Areaspline, + statType, + type, + names.take(15), + values.take(15), + xAxisName = "Genre", + polar = true, + categories = names + ) + adapter.add(ChartItem("Genre", genreChart)) } } - private fun getElements( - names: List, - statData: List, - colors: List - ): Array { - val statDataElements = mutableListOf() - for (i in statData.indices) { - val element = AADataElement() - .y(statData[i]) - .color( - AAColor.rgbaColor( - Color.red(colors[i]), - Color.green(colors[i]), - Color.blue(colors[i]), - 0.9f - ) - ) - if (names[i] is Number) { - element.x(names[i] as Number) - element.dataLabels(AADataLabels() - .enabled(false) - .format("{point.y}") - .backgroundColor(AAColor.rgbaColor(255, 255, 255, 0.0f)) - ) - } else { - element.x(i) - element.name(names[i] as String) + private fun loadTagChart(anime: Boolean) { + val names: List = if (anime) { + stats?.data?.user?.statistics?.anime?.tags?.map { it.tag.name } ?: emptyList() + } else { + stats?.data?.user?.statistics?.manga?.tags?.map { it.tag.name } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.anime?.tags?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.anime?.tags?.map { it.minutesWatched / 60 } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.tags?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.manga?.tags?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.manga?.tags?.map { it.chaptersRead } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.tags?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + val min = values.minOf { it.toInt() } + val max = values.maxOf { it.toInt() } + val tagChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Areaspline, + statType, + type, + names, + values, + xAxisName = "Tag", + polar = false, + categories = names, + scrollPos = 0.0f + ) + tagChart.yAxis = AAYAxis().min(min).max(max).tickInterval(if (max > 100) 20 else 10) + adapter.add(ChartItem("Tag", tagChart)) + } + } + + private fun loadCountryChart(anime: Boolean) { + val names: List = if (anime) { + stats?.data?.user?.statistics?.anime?.countries?.map { it.country } ?: emptyList() + } else { + stats?.data?.user?.statistics?.manga?.countries?.map { it.country } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.anime?.countries?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.anime?.countries?.map { it.minutesWatched / 60 } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.countries?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.manga?.countries?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.manga?.countries?.map { it.chaptersRead } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.countries?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + val countryChart = ChartBuilder.buildChart( + activity, + ChartType.OneDimensional, + AAChartType.Pie, + statType, + type, + names, + values, + xAxisName = "Country", + polar = false, + categories = names, + scrollPos = null + ) + adapter.add(ChartItem("Country", countryChart)) + } + } + + private fun loadVoiceActorsChart(anime: Boolean) { + val names: List = if (anime) { + stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() + } else { + stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.voiceActor.name.full?:"unknown" } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.minutesWatched / 60 } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.voiceActors?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.chaptersRead } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.voiceActors?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + val voiceActorsChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Column, + statType, + type, + names, + values, + xAxisName = "Voice Actor", + polar = false, + categories = names, + scrollPos = 0.0f + ) + adapter.add(ChartItem("Voice Actor", voiceActorsChart)) + } + } + + private fun loadStaffChart(anime: Boolean) { + val names: List = if (anime) { + stats?.data?.user?.statistics?.anime?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() + } else { + stats?.data?.user?.statistics?.manga?.staff?.map { it.staff.name.full?:"unknown" } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.anime?.staff?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.anime?.staff?.map { it.minutesWatched / 60 } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.staff?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.manga?.staff?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.manga?.staff?.map { it.chaptersRead } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.staff?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + val staffChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Line, + statType, + type, + names, + values, + xAxisName = "Staff", + polar = false, + categories = names, + scrollPos = 0.0f + ) + adapter.add(ChartItem("Staff", staffChart)) + } + } + + private fun loadStudioChart(anime: Boolean) { + val names: List = if (anime) { + stats?.data?.user?.statistics?.anime?.studios?.map { it.studio.name } ?: emptyList() + } else { + stats?.data?.user?.statistics?.manga?.studios?.map { it.studio.name } ?: emptyList() + } + val values: List = if (anime) { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.anime?.studios?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.anime?.studios?.map { it.minutesWatched / 60 } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.anime?.studios?.map { it.meanScore } + } ?: emptyList() + } else { + when (statType) { + StatType.COUNT -> stats?.data?.user?.statistics?.manga?.studios?.map { it.count } + StatType.TIME -> stats?.data?.user?.statistics?.manga?.studios?.map { it.chaptersRead } + StatType.MEAN_SCORE -> stats?.data?.user?.statistics?.manga?.studios?.map { it.meanScore } + } ?: emptyList() + } + if (names.isNotEmpty() || values.isNotEmpty()) { + val studioChart = ChartBuilder.buildChart( + activity, + ChartType.TwoDimensional, + AAChartType.Spline, + statType, + type, + names.take(15), + values.take(15), + xAxisName = "Studio", + polar = true, + categories = names, + scrollPos = null + ) + adapter.add(ChartItem("Studio", studioChart)) + } + } + + companion object { + fun newInstance(user: Query.UserProfile): StatsFragment { + val args = Bundle().apply { + putSerializable("user", user) + } + return StatsFragment().apply { + arguments = args } - statDataElements.add(element) } - return arrayOf( - AASeriesElement().name("Score").color(primaryColor) - .data(statDataElements.toTypedArray()) - ) - } - - private fun getElementsSimple( - names: List, - statData: List - ): Array { - val statValues = mutableListOf>() - for (i in statData.indices) { - statValues.add(arrayOf(names[i], statData[i], statData[i])) - } - return arrayOf( - AASeriesElement().name("Score") - .data(statValues.toTypedArray()) - .dataLabels(AADataLabels() - .enabled(false) - ) - .colorByPoint(true) - ) - } - - private fun setColors(aaOptions: AAOptions) { - val backgroundColor = TypedValue() - activity.theme.resolveAttribute(com.google.android.material.R.attr.colorSurfaceVariant, backgroundColor, true) - val backgroundStyle = AAStyle().color( - AAColor.rgbaColor( - Color.red(backgroundColor.data), - Color.green(backgroundColor.data), - Color.blue(backgroundColor.data), - 1f - ) - ) - val colorOnBackground = TypedValue() - activity.theme.resolveAttribute( - com.google.android.material.R.attr.colorOnSurface, - colorOnBackground, - true - ) - val onBackgroundStyle = AAStyle().color( - AAColor.rgbaColor( - Color.red(colorOnBackground.data), - Color.green(colorOnBackground.data), - Color.blue(colorOnBackground.data), - 0.9f - ) - ) - - - aaOptions.chart?.backgroundColor(backgroundStyle.color) - aaOptions.tooltip?.backgroundColor( - AAColor.rgbaColor( - Color.red(primaryColor), - Color.green(primaryColor), - Color.blue(primaryColor), - 0.9f - ) - ) - aaOptions.title?.style(onBackgroundStyle) - aaOptions.subtitle?.style(onBackgroundStyle) - aaOptions.tooltip?.style(backgroundStyle) - aaOptions.credits?.style(onBackgroundStyle) - aaOptions.xAxis?.labels?.style(onBackgroundStyle) - aaOptions.yAxis?.labels?.style(onBackgroundStyle) - aaOptions.plotOptions?.series?.dataLabels?.style(onBackgroundStyle) - aaOptions.plotOptions?.series?.dataLabels?.backgroundColor(backgroundStyle.color) - aaOptions.legend?.itemStyle(AAItemStyle().color(onBackgroundStyle.color)) - - aaOptions.touchEventEnabled(true) - } - - private fun generateColorPalette( - baseColor: Int, - size: Int, - hueDelta: Float = 8f, - saturationDelta: Float = 2.02f, - valueDelta: Float = 2.02f - ): List { - val palette = mutableListOf() - val hsv = FloatArray(3) - Color.colorToHSV(baseColor, hsv) - - for (i in 0 until size) { - val newHue = (hsv[0] + hueDelta * i) % 360 // Ensure hue stays within the 0-360 range - val newSaturation = (hsv[1] + saturationDelta * i).coerceIn(0f, 1f) - val newValue = (hsv[2] + valueDelta * i).coerceIn(0f, 1f) - - val newHsv = floatArrayOf(newHue, newSaturation, newValue) - palette.add(Color.HSVToColor(newHsv)) - } - - return palette } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_open_24.xml b/app/src/main/res/drawable/ic_open_24.xml new file mode 100644 index 00000000..52b21320 --- /dev/null +++ b/app/src/main/res/drawable/ic_open_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_statistics.xml b/app/src/main/res/layout/fragment_statistics.xml index 581961d2..4c9cb775 100644 --- a/app/src/main/res/layout/fragment_statistics.xml +++ b/app/src/main/res/layout/fragment_statistics.xml @@ -97,385 +97,9 @@ android:layout_height="wrap_content" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + tools:listitem="@layout/item_chart" /> diff --git a/app/src/main/res/layout/item_chart.xml b/app/src/main/res/layout/item_chart.xml new file mode 100644 index 00000000..0595ce50 --- /dev/null +++ b/app/src/main/res/layout/item_chart.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + \ No newline at end of file