426 lines
No EOL
16 KiB
Kotlin
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()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
} |