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,9 +144,15 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
) )
} }
if (type == ChartType.STACKED) {
for (serie in series.reversed()) {
serie.onDraw(canvas, rect)
}
} else {
for (serie in series) { for (serie in series) {
serie.onDraw(canvas, rect) serie.onDraw(canvas, rect)
} }
}
super.onDraw(canvas) super.onDraw(canvas)
} }

View File

@ -5,9 +5,12 @@ import android.graphics.Color
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.graphics.RectF import android.graphics.RectF
import com.dzeio.charts.ChartType
import com.dzeio.charts.ChartViewInterface import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
import com.dzeio.charts.utils.drawDottedLine import com.dzeio.charts.utils.drawDottedLine
import kotlin.math.roundToInt import kotlin.math.roundToInt
import kotlin.math.sign
class YAxis( class YAxis(
private val view: ChartViewInterface private val view: ChartViewInterface
@ -37,7 +40,7 @@ class YAxis(
override var labelCount = 5 override var labelCount = 5
private var min: Float? = 0f private var min: Float? = null
private var max: Float? = null private var max: Float? = null
@Deprecated("use the new global function", replaceWith = ReplaceWith("YAxisInterface.addLine")) @Deprecated("use the new global function", replaceWith = ReplaceWith("YAxisInterface.addLine"))
@ -68,6 +71,22 @@ class YAxis(
if (view.series.isEmpty()) { if (view.series.isEmpty()) {
return this.lines.keys.maxOrNull() ?: 0f 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 val seriesMax = view.series
.maxOf { serie -> .maxOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) { if (serie.getDisplayedEntries().isEmpty()) {
@ -85,6 +104,22 @@ class YAxis(
if (view.series.isEmpty()) { if (view.series.isEmpty()) {
return this.lines.keys.minOrNull() ?: 0f 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 return view.series
.minOf { serie -> .minOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) { if (serie.getDisplayedEntries().isEmpty()) {
@ -168,4 +203,35 @@ class YAxis(
addLine(height, Line(true)) 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.Canvas
import android.graphics.Paint import android.graphics.Paint
import android.graphics.RectF import android.graphics.RectF
import com.dzeio.charts.Entry
sealed interface YAxisInterface { sealed interface YAxisInterface {
@ -110,4 +111,24 @@ sealed interface YAxisInterface {
* @param y the Y position of the line * @param y the Y position of the line
*/ */
fun removeLine(y: Float) 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) { override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val displayedEntries = getDisplayedEntries() val displayedEntries = getDisplayedEntries()
val barWidth = view.xAxis.getEntryWidth(drawableSpace).toFloat() 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( val zero = view.yAxis.getPositionOnRect(0f, drawableSpace)
drawableSpace.top, drawableSpace.bottom
)
for (entry in displayedEntries) { for (entry in displayedEntries) {
// calculated height in percent from 0 to 100 // 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)
.coerceIn(drawableSpace.top, drawableSpace.bottom)
var posX = drawableSpace.left + view.xAxis.getPositionOnRect( var posX = drawableSpace.left + view.xAxis.getPositionOnRect(
entry, entry,
drawableSpace drawableSpace

View File

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

View File

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