Dantotsu/app/src/main/java/ani/dantotsu/profile/ChartBuilder.kt
2024-03-05 00:25:40 -06:00

426 lines
No EOL
16 KiB
Kotlin

package ani.dantotsu.profile
import android.content.Context
import android.graphics.Color
import android.util.TypedValue
import ani.dantotsu.util.ColorEditor
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, AVG_SCORE
}
enum class MediaType {
ANIME, MANGA
}
data class ChartPacket(
val username: String,
val names: List<Any>,
var statData: List<Number>
)
fun buildChart(
context: Context,
passedChartType: ChartType,
passedAaChartType: AAChartType,
statType: StatType,
mediaType: MediaType,
chartPackets: List<ChartPacket>,
xAxisName: String,
xAxisTickInterval: Int? = null,
polar: Boolean = false,
passedCategories: List<String>? = null,
scrollPos: Float? = null,
normalize: Boolean = false
): AAOptions {
val typedValue = TypedValue()
context.theme.resolveAttribute(
com.google.android.material.R.attr.colorPrimary,
typedValue,
true
)
val primaryColor = typedValue.data
var chartType = passedChartType
var aaChartType = passedAaChartType
var categories = passedCategories
if (chartType == ChartType.OneDimensional && chartPackets.size != 1) {
//need to convert to 2D
chartType = ChartType.TwoDimensional
aaChartType = AAChartType.Column
categories = chartPackets[0].names.map { it.toString() }
}
if (normalize && chartPackets.size > 1) {
chartPackets.forEach {
it.statData = normalizeData(it.statData)
}
}
val namesMax = chartPackets.maxOf { it.names.size }
val palette = ColorEditor.generateColorPalette(primaryColor, namesMax)
val aaChartModel = when (chartType) {
ChartType.OneDimensional -> {
val chart = AAChartModel()
.chartType(aaChartType)
.subtitle(
getTypeName(
statType,
mediaType
) + if (normalize && chartPackets.size > 1) " (Normalized)" else ""
)
.zoomType(AAChartZoomType.None)
.dataLabelsEnabled(true)
val elements: MutableList<Any> = mutableListOf()
chartPackets.forEachIndexed { index, chartPacket ->
val element = AASeriesElement()
.name(chartPacket.username)
.data(
get1DElements(
chartPacket.names,
chartPacket.statData,
palette
)
)
if (index == 0) {
element.color(primaryColor)
} else {
element.color(ColorEditor.oppositeColor(primaryColor))
}
elements.add(element)
}
chart.series(elements.toTypedArray())
xAxisTickInterval?.let { chart.xAxisTickInterval(it) }
categories?.let { chart.categories(it.toTypedArray()) }
chart
}
ChartType.TwoDimensional -> {
val hexColorsArray: Array<Any> =
palette.map { String.format("#%06X", 0xFFFFFF and it) }.toTypedArray()
val chart = AAChartModel()
.chartType(aaChartType)
.subtitle(
getTypeName(
statType,
mediaType
) + if (normalize && chartPackets.size > 1) " (Normalized)" else ""
)
.zoomType(AAChartZoomType.None)
.dataLabelsEnabled(false)
.yAxisTitle(
getTypeName(
statType,
mediaType
) + if (normalize && chartPackets.size > 1) " (Normalized)" else ""
)
if (chartPackets.size == 1) {
chart.colorsTheme(hexColorsArray)
}
val elements: MutableList<AASeriesElement> = mutableListOf()
chartPackets.forEachIndexed { index, chartPacket ->
val element = get2DElements(
chartPacket.names,
chartPacket.statData,
chartPackets.size == 1
)
element.name(chartPacket.username)
if (index == 0) {
element.color(
AAColor.rgbaColor(
Color.red(primaryColor),
Color.green(primaryColor),
Color.blue(primaryColor),
0.9f
)
)
} else {
element.color(
AAColor.rgbaColor(
Color.red(
ColorEditor.oppositeColor(
primaryColor
)
),
Color.green(ColorEditor.oppositeColor(primaryColor)),
Color.blue(ColorEditor.oppositeColor(primaryColor)),
0.9f
)
)
}
if (chartPackets.size == 1) {
element.fillColor(
AAColor.rgbaColor(
Color.red(primaryColor),
Color.green(primaryColor),
Color.blue(primaryColor),
0.9f
)
)
}
elements.add(element)
}
chart.series(elements.toTypedArray())
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),
chartPackets.size
)
)
if (chartPackets.size > 1) {
useHTML(true)
}
}
aaOptions.legend?.apply {
enabled(true)
.labelFormat = "{name}"
}
aaOptions.plotOptions?.series?.connectNulls(false)
aaOptions.plotOptions?.series?.stacking(AAChartStackingType.False)
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) * (namesMax.toFloat() / 18.0f))
}
val allStatData = chartPackets.flatMap { it.statData }
val min = (allStatData.minOfOrNull { it.toDouble() } ?: 0.0) - 1.0
val coercedMin = min.coerceAtLeast(0.0)
val max = allStatData.maxOfOrNull { it.toDouble() } ?: 0.0
val aaYaxis = AAYAxis().min(coercedMin).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<Any>,
statData: List<Any>,
colorByPoint: Boolean
): AASeriesElement {
val statValues = mutableListOf<Array<Any>>()
for (i in statData.indices) {
statValues.add(arrayOf(names[i], statData[i], statData[i]))
}
return AASeriesElement()
.data(statValues.toTypedArray())
.dataLabels(
AADataLabels()
.enabled(false)
)
.colorByPoint(colorByPoint)
}
private fun get1DElements(
names: List<Any>,
statData: List<Number>,
colors: List<Int>
): Array<Any> {
val statDataElements = mutableListOf<AADataElement>()
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 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.AVG_SCORE -> "Mean Score"
}
}
private fun normalizeData(data: List<Number>): List<Number> {
if (data.isEmpty()) {
return data
}
val max = data.maxOf { it.toDouble() }
return data.map { (it.toDouble() / max) * 100 }
}
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),
1.0f
)
)
aaOptions.chart?.backgroundColor(backgroundStyle.color)
aaOptions.tooltip?.backgroundColor(
AAColor.rgbaColor(
Color.red(backgroundColor.data),
Color.green(backgroundColor.data),
Color.blue(backgroundColor.data),
1.0f
)
)
aaOptions.title?.style(onBackgroundStyle)
aaOptions.subtitle?.style(onBackgroundStyle)
aaOptions.tooltip?.style(onBackgroundStyle)
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,
chartSize: Int
): String {
return when (chartType) {
ChartType.OneDimensional -> {
"""
function () {
return this.point.name
+ ': <br/> '
+ '<b> '
+ this.y
+ ', '
+ (this.percentage).toFixed(2)
+ '%'
}
""".trimIndent()
}
ChartType.TwoDimensional -> {
if (chartSize == 1) {
"""
function () {
return '$type: ' +
this.x +
'<br/> ' +
' $typeName ' +
this.y
}
""".trimIndent()
} else {
"""
function() {
let wholeContentStr = '<span style=\"' + 'color:gray; font-size:13px\"' + '>◉${type}: ' + this.x + '</span><br/>';
if (this.points) {
let length = this.points.length;
for (let i = 0; i < length; i++) {
let thisPoint = this.points[i];
let yValue = thisPoint.y;
if (yValue != 0) {
let spanStyleStartStr = '<span style=\"' + 'color: ' + thisPoint.color + '; font-size:13px\"' + '>◉ ';
let spanStyleEndStr = '</span> <br/>';
wholeContentStr += spanStyleStartStr + thisPoint.series.name + ': ' + yValue + spanStyleEndStr;
}
}
} else {
let spanStyleStartStr = '<span style=\"' + 'color: ' + this.point.color + '; font-size:13px\"' + '>◉ ';
let spanStyleEndStr = '</span> <br/>';
wholeContentStr += spanStyleStartStr + this.point.series.name + ': ' + this.point.y + spanStyleEndStr;
}
return wholeContentStr;
}
""".trimIndent()
}
}
}
}
}
}