feat: Add basic Stacked charts support (#22)

This commit is contained in:
Florian Bouillon 2023-01-12 21:56:29 +01:00 committed by GitHub
parent 6afe0a938e
commit ed06aa697e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 18 deletions

View File

@ -144,8 +144,14 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
)
}
for (serie in series) {
serie.onDraw(canvas, rect)
if (type == ChartType.STACKED) {
for (serie in series.reversed()) {
serie.onDraw(canvas, rect)
}
} else {
for (serie in series) {
serie.onDraw(canvas, rect)
}
}
super.onDraw(canvas)
}

View File

@ -5,9 +5,12 @@ import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import com.dzeio.charts.ChartType
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
import com.dzeio.charts.utils.drawDottedLine
import kotlin.math.roundToInt
import kotlin.math.sign
class YAxis(
private val view: ChartViewInterface
@ -37,7 +40,7 @@ class YAxis(
override var labelCount = 5
private var min: Float? = 0f
private var min: Float? = null
private var max: Float? = null
@Deprecated("use the new global function", replaceWith = ReplaceWith("YAxisInterface.addLine"))
@ -68,6 +71,22 @@ class YAxis(
if (view.series.isEmpty()) {
return this.lines.keys.maxOrNull() ?: 0f
}
if (view.type == ChartType.STACKED) {
val nList: ArrayList<Float> = arrayListOf()
for (serie in view.series) {
val size = serie.entries.size
while (nList.size < size) {
nList.add(0f)
}
for (index in 0 until serie.entries.size) {
val entry = serie.entries[index]
nList[index] += entry.y
}
}
return nList.maxOf { it }
}
val seriesMax = view.series
.maxOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
@ -85,6 +104,22 @@ class YAxis(
if (view.series.isEmpty()) {
return this.lines.keys.minOrNull() ?: 0f
}
if (view.type == ChartType.STACKED) {
val nList: ArrayList<Float> = arrayListOf()
for (serie in view.series) {
val size = serie.entries.size
while (nList.size < size) {
nList.add(0f)
}
for (index in 0 until serie.entries.size) {
val entry = serie.entries[index]
nList[index] += entry.y
}
}
return nList.minOf { it }
}
return view.series
.minOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
@ -168,4 +203,35 @@ class YAxis(
addLine(height, Line(true))
}
}
override fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Float {
if (view.type == ChartType.STACKED) {
val serie = view.series.find { it.entries.contains(entry) }
val index = view.series.indexOf(serie)
return getPositionOnRect(entry, drawableSpace, index)
}
return getPositionOnRect(entry.y, drawableSpace)
}
private fun getPositionOnRect(entry: Entry, drawableSpace: RectF, index: Int): Float {
if (index > 0) {
val entry2 = view.series[index - 1].entries.find { it.x == entry.x }
if (entry2 != null) {
// make a new """Entry""" containing the new Y
val isReverse = sign(entry2.y) != sign(entry.y)
val tmp = Entry(entry.x, if (isReverse) entry.y else entry.y + entry2.y)
return getPositionOnRect(tmp, drawableSpace, index - 1)
}
}
return getPositionOnRect(entry.y, drawableSpace)
}
override fun getPositionOnRect(point: Float, drawableSpace: RectF): Float {
val min = getYMin()
val max = getYMax()
return (1 - (point - min) / (max - min)) *
drawableSpace.height() +
drawableSpace.top
}
}

View File

@ -3,6 +3,7 @@ package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import com.dzeio.charts.Entry
sealed interface YAxisInterface {
@ -110,4 +111,24 @@ sealed interface YAxisInterface {
* @param y the Y position of the line
*/
fun removeLine(y: Float)
/**
* get the position of an [entry] Y position in the [drawableSpace]
*
* if the chart type is stacked it will automatically calculate the position depending on it
*
* @param entry the entry to search to position
* @param drawableSpace the space in which it should appear
* @return the float position (can be out of the [drawableSpace])
*/
fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Float
/**
* get the position of a [point] in the [drawableSpace]
*
* @param point the point to search to position
* @param drawableSpace the space in which it should appear
* @return the float position (can be out of the [drawableSpace])
*/
fun getPositionOnRect(point: Float, drawableSpace: RectF): Float
}

View File

@ -44,17 +44,12 @@ class BarSerie(
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries()
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat()
val max = view.yAxis.getYMax()
val min = view.yAxis.getYMin()
val zero = ((1 - -min / (max - min)) * drawableSpace.height() + drawableSpace.top).coerceIn(
drawableSpace.top, drawableSpace.bottom
)
val zero = view.yAxis.getPositionOnRect(0f, drawableSpace)
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = ((1 - (entry.y - min) / (max - min)) * drawableSpace.height() + drawableSpace.top)
.coerceIn(drawableSpace.top, drawableSpace.bottom)
val top = view.yAxis.getPositionOnRect(entry, drawableSpace)
var posX = drawableSpace.left + view.xAxis.getPositionOnRect(
entry,
drawableSpace

View File

@ -34,15 +34,13 @@ class LineSerie(
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries()
displayedEntries.sortBy { it.x }
val max = view.yAxis.getYMax()
val min = view.yAxis.getYMin()
var previousPosX: Float? = null
var previousPosY: Float? = null
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val top = (1 - (entry.y - min) / (max - min)) * drawableSpace.height() + drawableSpace.top
val top = view.yAxis.getPositionOnRect(entry, drawableSpace)
val posX = (drawableSpace.left +
view.xAxis.getPositionOnRect(entry, drawableSpace) +
view.xAxis.getEntryWidth(drawableSpace) / 2f).toFloat()

View File

@ -37,15 +37,15 @@ class MainFragment : Fragment() {
val serie2 = BarSerie(this)
// transform the chart into a grouped chart
type = ChartType.GROUPED
type = ChartType.STACKED
// utils function to use Material3 auto colors
materielTheme(this, requireView())
serie2.barPaint.color = Color.RED
// give the serie it's entries
serie1.entries = generateRandomDataset(10)
serie2.entries = generateRandomDataset(10)
serie1.entries = generateRandomDataset(1)
serie2.entries = generateRandomDataset(1)
// refresh the Chart
refresh()
@ -164,7 +164,7 @@ class MainFragment : Fragment() {
private fun generateRandomDataset(size: Int = 100, min: Int = 0, max: Int = 100): ArrayList<Entry> {
val dataset: ArrayList<Entry> = arrayListOf()
for (i in 0 .. size) {
for (i in 0 until size) {
dataset.add(Entry(
i.toDouble(),
Random.nextInt(min, max).toFloat()
@ -233,4 +233,4 @@ class MainFragment : Fragment() {
}
}
}
}
}