mirror of
https://github.com/dzeiocom/charts.git
synced 2025-04-23 02:52:10 +00:00
feat: Basic idea is present
This commit is contained in:
parent
e1946e98d0
commit
3965ac2f81
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
#Sun Aug 07 22:38:24 CEST 2022
|
#Sun Aug 07 22:38:24 CEST 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-rc-1-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
@ -9,6 +9,7 @@ import android.graphics.Paint
|
|||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import com.dzeio.charts.axis.XAxis
|
import com.dzeio.charts.axis.XAxis
|
||||||
@ -82,16 +83,21 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
return@setOnChartMoved
|
return@setOnChartMoved
|
||||||
}
|
}
|
||||||
if (xAxis.scrollEnabled) {
|
if (xAxis.scrollEnabled) {
|
||||||
xAxis.x += (movementX - lastMovementX) * xAxis.getDataWidth() / width
|
val currentXMax = xAxis.getCurrentMax()
|
||||||
|
val currentXMin = xAxis.getCurrentMin()
|
||||||
|
val change = (movementX.toDouble() - lastMovementX) * (currentXMax - currentXMin) / width
|
||||||
|
xAxis.setCurrent(
|
||||||
|
currentXMin + change,
|
||||||
|
currentXMax + change
|
||||||
|
)
|
||||||
lastMovementX = movementX.toDouble()
|
lastMovementX = movementX.toDouble()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (yAxis.scrollEnabled) {
|
if (yAxis.scrollEnabled) {
|
||||||
val currentYMax = yAxis.getYMax()
|
val currentYMax = yAxis.getCurrentMax()
|
||||||
val currentYMin = yAxis.getYMin()
|
val currentYMin = yAxis.getCurrentMin()
|
||||||
val change = (movementY - lastMovementY) * (currentYMax - currentYMin) / height
|
val change = (movementY - lastMovementY) * (currentYMax - currentYMin) / height
|
||||||
yAxis.setYMax(currentYMax + change)
|
yAxis.setCurrent(currentYMin + change, currentYMax + change)
|
||||||
yAxis.setYMin(currentYMin + change)
|
|
||||||
lastMovementY = movementY
|
lastMovementY = movementY
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,7 +105,9 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
}
|
}
|
||||||
setOnToggleScroll {
|
setOnToggleScroll {
|
||||||
// note: true == no scroll
|
// note: true == no scroll
|
||||||
parent?.requestDisallowInterceptTouchEvent(!it && (yAxis.scrollEnabled || xAxis.scrollEnabled))
|
parent?.requestDisallowInterceptTouchEvent(
|
||||||
|
!it && (yAxis.scrollEnabled || xAxis.scrollEnabled || xAxis.zoomEnabled || yAxis.zoomEnabled)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
setOnChartClick { x, y ->
|
setOnChartClick { x, y ->
|
||||||
if (getDataset().isEmpty()) {
|
if (getDataset().isEmpty()) {
|
||||||
@ -132,11 +140,53 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
|
|||||||
}
|
}
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
// setOnZoomChanged {
|
setOnZoomChanged { scaleX, scaleY ->
|
||||||
// Log.d(TAG, "New Zoom: $it")
|
val animationEnabled = animator.enabled
|
||||||
|
if (animationEnabled) animator.enabled = false
|
||||||
|
Log.d(TAG, "Zoom: Start")
|
||||||
|
val factor = 0.05f
|
||||||
|
if (scaleX != 0f && xAxis.zoomEnabled && xAxis.scrollEnabled) {
|
||||||
|
Log.d(TAG, "ScaleX: scaleX")
|
||||||
|
Log.d(TAG, "ScaleX: $scaleX")
|
||||||
|
val xDistance = xAxis.getCurrentMax() - xAxis.getCurrentMin()
|
||||||
|
Log.d(TAG, "ScaleX: xDistance * (scaleX * factor)")
|
||||||
|
Log.d(TAG, "ScaleX: $xDistance * ${(scaleX * factor)}")
|
||||||
|
val xTransformer = xDistance * (scaleX * factor)
|
||||||
|
Log.d(TAG, "ScaleX: xTransformer")
|
||||||
|
Log.d(TAG, "ScaleX: $xTransformer")
|
||||||
|
val xMin = xAxis.getCurrentMin() - xTransformer
|
||||||
|
val dataWidth = xAxis.getDataWidth() + xTransformer
|
||||||
|
Log.d(TAG, "ScaleX: previousXMin, previousDataWidth")
|
||||||
|
Log.d(TAG, "ScaleX: ${xAxis.getCurrentMin()}, ${xAxis.getDataWidth()}")
|
||||||
|
Log.d(TAG, "ScaleX: xMin, dataWidth")
|
||||||
|
Log.d(TAG, "ScaleX: $xMin, $dataWidth")
|
||||||
|
xAxis.setCurrentMin(xMin)
|
||||||
|
xAxis.dataWidth = dataWidth
|
||||||
|
}
|
||||||
|
if (scaleY != 0f && yAxis.zoomEnabled && yAxis.scrollEnabled) {
|
||||||
|
Log.d(TAG, "ScaleY: scaleY")
|
||||||
|
Log.d(TAG, "ScaleY: $scaleY")
|
||||||
|
val yDistance = yAxis.getCurrentMax() - yAxis.getCurrentMin()
|
||||||
|
Log.d(TAG, "ScaleY: yDistance * (scaleY * factor)")
|
||||||
|
Log.d(TAG, "ScaleY: $yDistance * ${(scaleY * factor)}")
|
||||||
|
val yTransformer = yDistance * (scaleY * factor)
|
||||||
|
Log.d(TAG, "ScaleY: yTransformer")
|
||||||
|
Log.d(TAG, "ScaleY: $yTransformer")
|
||||||
|
val yMin = yAxis.getCurrentMin() - yTransformer
|
||||||
|
val yMax = yAxis.getCurrentMax() + yTransformer
|
||||||
|
Log.d(TAG, "ScaleY: previousYMin, previousYMax")
|
||||||
|
Log.d(TAG, "ScaleY: ${yAxis.getCurrentMin()}, ${yAxis.getCurrentMax()}")
|
||||||
|
Log.d(TAG, "ScaleY: yMin, yMax")
|
||||||
|
Log.d(TAG, "ScaleY: $yMin, $yMax")
|
||||||
|
yAxis.setCurrentMin(yMin)
|
||||||
|
yAxis.setCurrentMax(yMax.coerceAtLeast(yMin + 1f))
|
||||||
|
}
|
||||||
|
// Log.d(TAG, "Zoom: Done")
|
||||||
|
// Log.d(TAG, "[after] : tr: $scaleX, $scaleY, xMin: $xMin, xMax: $xMax, yMin: $yMin, yMax: $yMax")
|
||||||
// zoom = (it * 1.2).toFloat()
|
// zoom = (it * 1.2).toFloat()
|
||||||
// refresh()
|
refresh()
|
||||||
// }
|
if (animationEnabled) animator.enabled = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rect used for calculations
|
// rect used for calculations
|
||||||
|
119
library/src/main/java/com/dzeio/charts/axis/AxisInterface.kt
Normal file
119
library/src/main/java/com/dzeio/charts/axis/AxisInterface.kt
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
package com.dzeio.charts.axis
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import com.dzeio.charts.Entry
|
||||||
|
|
||||||
|
interface AxisInterface<T> {
|
||||||
|
/**
|
||||||
|
* enable/disable the display of the xAxis
|
||||||
|
*/
|
||||||
|
var enabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* paint for the lines
|
||||||
|
*/
|
||||||
|
val linePaint: Paint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* text Paint
|
||||||
|
*/
|
||||||
|
val textPaint: Paint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if global limits are sets to true [getCurrentMin] while never be `<` to [getMin] and same for max
|
||||||
|
*/
|
||||||
|
var keepGlobalLimits: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* indicate the number of labels displayed
|
||||||
|
*/
|
||||||
|
var labelCount: Int
|
||||||
|
|
||||||
|
var onValueFormat: (value: T) -> String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is Horizontal Scrolling enabled
|
||||||
|
*/
|
||||||
|
var scrollEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* is horizontal zooming enabled
|
||||||
|
*/
|
||||||
|
var zoomEnabled: Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the entry position on the rect
|
||||||
|
*
|
||||||
|
* @param entry the entry to place
|
||||||
|
* @param drawableSpace the space it should go into
|
||||||
|
*
|
||||||
|
* @return the position of the entry on the defined axis
|
||||||
|
*/
|
||||||
|
fun getPositionOnRect(entry: Entry, drawableSpace: RectF): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the entry position on the rect
|
||||||
|
*
|
||||||
|
* @param position the position on your point
|
||||||
|
* @param drawableSpace the space it should go into
|
||||||
|
*
|
||||||
|
* @return the position of the entry on the defined axis
|
||||||
|
*/
|
||||||
|
fun getPositionOnRect(position: T, drawableSpace: RectF): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the current/displayed minimum (inclusive)
|
||||||
|
*/
|
||||||
|
fun getCurrentMin(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the new min/max of the element
|
||||||
|
*
|
||||||
|
* if one or both can't be set to the value both won't be set
|
||||||
|
*/
|
||||||
|
fun setCurrent(min: T?, max: T?): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the new minimum displayed value of the axis (inclusive)
|
||||||
|
*/
|
||||||
|
fun setCurrentMin(value: T?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set the new maximum displayed value of the axis (inclusive)
|
||||||
|
*/
|
||||||
|
fun setCurrentMax(value: T?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the current/displayed maximum (inclusive)
|
||||||
|
*/
|
||||||
|
fun getCurrentMax(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the maximum the axis can get to
|
||||||
|
*/
|
||||||
|
fun getMax(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the minimum value the axis can get to
|
||||||
|
*/
|
||||||
|
fun getMin(): T
|
||||||
|
|
||||||
|
/**
|
||||||
|
* run when manually refreshing the system
|
||||||
|
*
|
||||||
|
* this is where the pre-logic is handled to make [onDraw] quicker
|
||||||
|
*/
|
||||||
|
fun refresh()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* function that draw our legend
|
||||||
|
*
|
||||||
|
* @param canvas the canvas to draw on
|
||||||
|
* @param space the space where it is allowed to draw on
|
||||||
|
*
|
||||||
|
* @return the the width or height of the axis
|
||||||
|
*/
|
||||||
|
fun onDraw(canvas: Canvas, space: RectF): Float
|
||||||
|
}
|
@ -2,7 +2,7 @@ package com.dzeio.charts.axis
|
|||||||
|
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
|
|
||||||
data class Line (
|
data class Line(
|
||||||
/**
|
/**
|
||||||
* is the bar dotted
|
* is the bar dotted
|
||||||
*/
|
*/
|
||||||
|
@ -15,24 +15,24 @@ class XAxis(
|
|||||||
) : XAxisInterface {
|
) : XAxisInterface {
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "Charts/XAxis"
|
const val TAG = "XAxis"
|
||||||
}
|
}
|
||||||
|
|
||||||
override var x: Double = 0.0
|
private var x: Double = 0.0
|
||||||
set(value) {
|
|
||||||
val max = getXMax()
|
|
||||||
val min = getXMin()
|
|
||||||
|
|
||||||
field = value.coerceIn(min, max.coerceAtLeast(min))
|
|
||||||
}
|
|
||||||
|
|
||||||
override var enabled = true
|
override var enabled = true
|
||||||
|
|
||||||
|
override val linePaint = Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
color = Color.BLACK
|
||||||
|
}
|
||||||
|
|
||||||
override var dataWidth: Double? = null
|
override var dataWidth: Double? = null
|
||||||
|
|
||||||
override var labelCount: Int = 2
|
override var labelCount: Int = 2
|
||||||
|
|
||||||
override var scrollEnabled: Boolean = false
|
override var scrollEnabled: Boolean = false
|
||||||
|
override var zoomEnabled: Boolean = false
|
||||||
|
|
||||||
var spacing = 16.0
|
var spacing = 16.0
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ class XAxis(
|
|||||||
private val rect = Rect()
|
private val rect = Rect()
|
||||||
|
|
||||||
private var height: Float? = null
|
private var height: Float? = null
|
||||||
|
override var keepGlobalLimits = false
|
||||||
|
|
||||||
override fun getHeight(): Float? {
|
override fun getHeight(): Float? {
|
||||||
return height
|
return height
|
||||||
@ -65,21 +66,68 @@ class XAxis(
|
|||||||
return drawableSpace.left + drawableSpace.width() * (position - x) / getDataWidth()
|
return drawableSpace.left + drawableSpace.width() * (position - x) / getDataWidth()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getXMax(): Double {
|
override fun getCurrentMin(): Double {
|
||||||
return view.series.maxOf { serie ->
|
return this.x
|
||||||
if (serie.entries.isEmpty()) {
|
|
||||||
return 0.0
|
|
||||||
}
|
}
|
||||||
serie.entries.maxOf { entry -> entry.x }
|
|
||||||
|
override fun setCurrent(min: Double?, max: Double?): Boolean {
|
||||||
|
val previousMin = getCurrentMin()
|
||||||
|
val previousMax = getCurrentMax()
|
||||||
|
setCurrentMin(min)
|
||||||
|
if (previousMin != getCurrentMin()) {
|
||||||
|
setCurrentMin(previousMin)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setCurrentMax(max)
|
||||||
|
if (previousMax != getCurrentMax()) {
|
||||||
|
setCurrentMax(previousMax)
|
||||||
|
setCurrentMin(previousMin)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCurrentMin(value: Double?) {
|
||||||
|
if (keepGlobalLimits) {
|
||||||
|
this.x = (value ?: 0.0).coerceIn(getMin(), getMax())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.x = value ?: 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setCurrentMax(value: Double?) {
|
||||||
|
if (value == null) {
|
||||||
|
dataWidth = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var real = value
|
||||||
|
if (keepGlobalLimits) {
|
||||||
|
real = value.coerceIn(getMin(), getMax())
|
||||||
|
}
|
||||||
|
dataWidth = real.coerceAtLeast(this.x + 1) - this.x
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentMax(): Double {
|
||||||
|
return this.x + getDataWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMax(): Double {
|
||||||
|
return view.series
|
||||||
|
.maxOf { serie ->
|
||||||
|
if (serie.entries.isEmpty()) {
|
||||||
|
return@maxOf 0.0
|
||||||
|
}
|
||||||
|
return@maxOf serie.entries.maxOf { entry -> entry.x }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getXMin(): Double {
|
override fun getMin(): Double {
|
||||||
return view.series.minOf { serie ->
|
return view.series
|
||||||
|
.minOf { serie ->
|
||||||
if (serie.entries.isEmpty()) {
|
if (serie.entries.isEmpty()) {
|
||||||
return 0.0
|
return@minOf 0.0
|
||||||
}
|
}
|
||||||
serie.entries.minOf { entry -> entry.x }
|
return@minOf serie.entries.minOf { entry -> entry.x }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +186,7 @@ class XAxis(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getDataWidth(): Double {
|
override fun getDataWidth(): Double {
|
||||||
// TODO: handle the auto dataWidth better (still not sure it is good enough)
|
// TODO: handle the auto dataWidth better
|
||||||
return dataWidth ?: (getXMax() - getXMin() + 1)
|
return dataWidth ?: (getMax() - getMin() + 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,8 @@
|
|||||||
package com.dzeio.charts.axis
|
package com.dzeio.charts.axis
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import com.dzeio.charts.Entry
|
|
||||||
|
|
||||||
sealed interface XAxisInterface {
|
sealed interface XAxisInterface : AxisInterface<Double> {
|
||||||
|
|
||||||
/**
|
|
||||||
* enable/disable the display of the xAxis
|
|
||||||
*/
|
|
||||||
var enabled: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* set X position
|
|
||||||
*/
|
|
||||||
var x: Double
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the "width" of the graph
|
* the "width" of the graph
|
||||||
@ -26,60 +13,6 @@ sealed interface XAxisInterface {
|
|||||||
*/
|
*/
|
||||||
var dataWidth: Double?
|
var dataWidth: Double?
|
||||||
|
|
||||||
/**
|
|
||||||
* text Paint
|
|
||||||
*/
|
|
||||||
val textPaint: Paint
|
|
||||||
|
|
||||||
/**
|
|
||||||
* indicate the number of labels displayed
|
|
||||||
*/
|
|
||||||
var labelCount: Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* is Horizontal Scrolling enabled
|
|
||||||
*/
|
|
||||||
var scrollEnabled: Boolean
|
|
||||||
|
|
||||||
var onValueFormat: (value: Double) -> String
|
|
||||||
|
|
||||||
/**
|
|
||||||
* run when manually refreshing the system
|
|
||||||
*
|
|
||||||
* this is where the pre-logic is handled to make [onDraw] quicker
|
|
||||||
*/
|
|
||||||
fun refresh()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the entry position on the rect
|
|
||||||
*
|
|
||||||
* @param entry the entry to place
|
|
||||||
* @param drawableSpace the space it should go into
|
|
||||||
*
|
|
||||||
* @return the left side of the position of the entry
|
|
||||||
*/
|
|
||||||
fun getPositionOnRect(entry: Entry, drawableSpace: RectF): Double
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the entry position on the rect
|
|
||||||
*
|
|
||||||
* @param position the position of your point
|
|
||||||
* @param drawableSpace the space it should go into
|
|
||||||
*
|
|
||||||
* @return the left side of the position of the entry
|
|
||||||
*/
|
|
||||||
fun getPositionOnRect(position: Double, drawableSpace: RectF): Double
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the maximum the X can get to
|
|
||||||
*/
|
|
||||||
fun getXMax(): Double
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get the minimum the X can get to
|
|
||||||
*/
|
|
||||||
fun getXMin(): Double
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the size of an entry in the graph
|
* get the size of an entry in the graph
|
||||||
*
|
*
|
||||||
@ -92,16 +25,6 @@ sealed interface XAxisInterface {
|
|||||||
*/
|
*/
|
||||||
fun getDataWidth(): Double
|
fun getDataWidth(): Double
|
||||||
|
|
||||||
/**
|
|
||||||
* onDraw event that will draw the XAxis
|
|
||||||
*
|
|
||||||
* @param canvas the canvas to draw on
|
|
||||||
* @param space the space where it is allowed to draw
|
|
||||||
*
|
|
||||||
* @return the final height of the XAxis
|
|
||||||
*/
|
|
||||||
fun onDraw(canvas: Canvas, space: RectF): Float
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return the height of the XAxis (available after first draw)
|
* return the height of the XAxis (available after first draw)
|
||||||
*/
|
*/
|
||||||
|
@ -18,7 +18,7 @@ class YAxis(
|
|||||||
|
|
||||||
override var enabled = true
|
override var enabled = true
|
||||||
|
|
||||||
override val textLabel = Paint().apply {
|
override val textPaint = Paint().apply {
|
||||||
isAntiAlias = true
|
isAntiAlias = true
|
||||||
color = Color.BLACK
|
color = Color.BLACK
|
||||||
textSize = 30f
|
textSize = 30f
|
||||||
@ -36,6 +36,8 @@ class YAxis(
|
|||||||
strokeWidth = 4f
|
strokeWidth = 4f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var keepGlobalLimits = true
|
||||||
|
|
||||||
override var onValueFormat: (value: Float) -> String = { it -> it.roundToInt().toString() }
|
override var onValueFormat: (value: Float) -> String = { it -> it.roundToInt().toString() }
|
||||||
|
|
||||||
override var labelCount = 5
|
override var labelCount = 5
|
||||||
@ -51,20 +53,125 @@ class YAxis(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override var scrollEnabled: Boolean = false
|
override var scrollEnabled: Boolean = false
|
||||||
|
override var zoomEnabled: Boolean = false
|
||||||
|
|
||||||
private val rect = Rect()
|
private val rect = Rect()
|
||||||
|
|
||||||
override fun setYMin(yMin: Float?): YAxisInterface {
|
override fun setCurrent(min: Float?, max: Float?): Boolean {
|
||||||
min = yMin
|
val previousMin = getCurrentMin()
|
||||||
return this
|
val previousMax = getCurrentMax()
|
||||||
|
setCurrentMin(min)
|
||||||
|
if (previousMin != getCurrentMin()) {
|
||||||
|
setCurrentMin(previousMin)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
setCurrentMax(max)
|
||||||
|
if (previousMax != getCurrentMax()) {
|
||||||
|
setCurrentMax(previousMax)
|
||||||
|
setCurrentMin(previousMin)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setYMax(yMax: Float?): YAxisInterface {
|
override fun setCurrentMin(value: Float?) {
|
||||||
max = yMax
|
if (keepGlobalLimits) {
|
||||||
return this
|
min = (value ?: 0f).coerceIn(getMin(), getMax())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
min = value
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getYMax(): Float {
|
override fun setCurrentMax(value: Float?) {
|
||||||
|
if (keepGlobalLimits) {
|
||||||
|
max = (value ?: 0f).coerceIn(getMin(), getMax())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
max = value
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMax(): Float {
|
||||||
|
val max = this.lines.keys.maxOrNull() ?: 0f
|
||||||
|
|
||||||
|
if (view.series.isEmpty()) {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
if (sign(entry.y) <= 0f && nList[index] > 0f) {
|
||||||
|
continue
|
||||||
|
} else if (nList[index] < 0f && entry.y > 0f) {
|
||||||
|
nList[index] = entry.y
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nList[index] += entry.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val localMax = nList.maxOf { it }
|
||||||
|
return if (localMax > max) localMax else max
|
||||||
|
}
|
||||||
|
val seriesMax = view.series
|
||||||
|
.maxOf { serie ->
|
||||||
|
if (serie.entries.isEmpty()) {
|
||||||
|
return@maxOf 0f
|
||||||
|
}
|
||||||
|
return@maxOf serie.entries.maxOf { entry -> entry.y }
|
||||||
|
}
|
||||||
|
return if (seriesMax > max) seriesMax else max
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMin(): Float {
|
||||||
|
val min = this.lines.keys.minOrNull() ?: 0f
|
||||||
|
|
||||||
|
if (view.series.isEmpty()) {
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
if (sign(entry.y) >= 0f && nList[index] < 0f) {
|
||||||
|
continue
|
||||||
|
} else if (nList[index] > 0f && entry.y < 0f) {
|
||||||
|
nList[index] = entry.y
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nList[index] += entry.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val localMin = nList.minOf { it }
|
||||||
|
|
||||||
|
return if (localMin < min) localMin else min
|
||||||
|
}
|
||||||
|
val localMin = view.series
|
||||||
|
.minOf { serie ->
|
||||||
|
if (serie.entries.isEmpty()) {
|
||||||
|
return@minOf 0f
|
||||||
|
}
|
||||||
|
return@minOf serie.entries.minOf { entry -> entry.y }
|
||||||
|
}
|
||||||
|
return if (localMin < min) localMin else min
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentMax(): Float {
|
||||||
if (max != null) {
|
if (max != null) {
|
||||||
return max!!
|
return max!!
|
||||||
}
|
}
|
||||||
@ -110,7 +217,7 @@ class YAxis(
|
|||||||
return if (seriesMax > max) seriesMax else max
|
return if (seriesMax > max) seriesMax else max
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getYMin(): Float {
|
override fun getCurrentMin(): Float {
|
||||||
if (min != null) {
|
if (min != null) {
|
||||||
return min!!
|
return min!!
|
||||||
}
|
}
|
||||||
@ -162,8 +269,8 @@ class YAxis(
|
|||||||
return 0f
|
return 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
val min = getYMin()
|
val min = getCurrentMin()
|
||||||
val max = getYMax() - min
|
val max = getCurrentMax() - min
|
||||||
var maxWidth = 0f
|
var maxWidth = 0f
|
||||||
|
|
||||||
val valueIncrement = max / (labelCount - 1).coerceAtLeast(1)
|
val valueIncrement = max / (labelCount - 1).coerceAtLeast(1)
|
||||||
@ -176,7 +283,7 @@ class YAxis(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val text = onValueFormat(min + (valueIncrement * index))
|
val text = onValueFormat(min + (valueIncrement * index))
|
||||||
textLabel.getTextBounds(text, 0, text.length, rect)
|
textPaint.getTextBounds(text, 0, text.length, rect)
|
||||||
maxWidth = maxWidth.coerceAtLeast(rect.width().toFloat())
|
maxWidth = maxWidth.coerceAtLeast(rect.width().toFloat())
|
||||||
|
|
||||||
val posY = getPositionOnRect(value, space)
|
val posY = getPositionOnRect(value, space)
|
||||||
@ -185,7 +292,7 @@ class YAxis(
|
|||||||
text,
|
text,
|
||||||
space.width() - rect.width().toFloat(),
|
space.width() - rect.width().toFloat(),
|
||||||
(posY + rect.height() / 2).coerceAtLeast(rect.height().toFloat()),
|
(posY + rect.height() / 2).coerceAtLeast(rect.height().toFloat()),
|
||||||
textLabel
|
textPaint
|
||||||
)
|
)
|
||||||
canvas.drawLine(space.left, posY, space.right - maxWidth - 32f, posY, linePaint)
|
canvas.drawLine(space.left, posY, space.right - maxWidth - 32f, posY, linePaint)
|
||||||
}
|
}
|
||||||
@ -193,7 +300,7 @@ class YAxis(
|
|||||||
for ((y, settings) in lines) {
|
for ((y, settings) in lines) {
|
||||||
val pos = getPositionOnRect(y, space)
|
val pos = getPositionOnRect(y, space)
|
||||||
val text = onValueFormat(y)
|
val text = onValueFormat(y)
|
||||||
textLabel.getTextBounds(text, 0, text.length, rect)
|
textPaint.getTextBounds(text, 0, text.length, rect)
|
||||||
if (settings.dotted) {
|
if (settings.dotted) {
|
||||||
canvas.drawDottedLine(
|
canvas.drawDottedLine(
|
||||||
0f,
|
0f,
|
||||||
@ -216,7 +323,7 @@ class YAxis(
|
|||||||
text,
|
text,
|
||||||
space.width() - rect.width().toFloat(),
|
space.width() - rect.width().toFloat(),
|
||||||
(pos + rect.height() / 2).coerceAtLeast(rect.height().toFloat()),
|
(pos + rect.height() / 2).coerceAtLeast(rect.height().toFloat()),
|
||||||
textLabel
|
textPaint
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,11 +381,11 @@ class YAxis(
|
|||||||
return getPositionOnRect(entry.y, drawableSpace)
|
return getPositionOnRect(entry.y, drawableSpace)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPositionOnRect(point: Float, drawableSpace: RectF): Float {
|
override fun getPositionOnRect(position: Float, drawableSpace: RectF): Float {
|
||||||
val min = getYMin()
|
val min = getCurrentMin()
|
||||||
val max = getYMax()
|
val max = getCurrentMax()
|
||||||
|
|
||||||
return (1 - (point - min) / (max - min)) *
|
return (1 - (position - min) / (max - min)) *
|
||||||
drawableSpace.height() +
|
drawableSpace.height() +
|
||||||
drawableSpace.top
|
drawableSpace.top
|
||||||
}
|
}
|
||||||
|
@ -1,97 +1,20 @@
|
|||||||
package com.dzeio.charts.axis
|
package com.dzeio.charts.axis
|
||||||
|
|
||||||
import android.graphics.Canvas
|
|
||||||
import android.graphics.Paint
|
import android.graphics.Paint
|
||||||
import android.graphics.RectF
|
|
||||||
import com.dzeio.charts.Entry
|
|
||||||
|
|
||||||
sealed interface YAxisInterface {
|
sealed interface YAxisInterface : AxisInterface<Float> {
|
||||||
|
|
||||||
/**
|
|
||||||
* whether or not this axis is displayed
|
|
||||||
*/
|
|
||||||
var enabled: Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get/set the number of label of this Y axis
|
|
||||||
*
|
|
||||||
* the first/last labels are at the bottom/top of the chart
|
|
||||||
*/
|
|
||||||
var labelCount: Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* text label paint
|
|
||||||
*/
|
|
||||||
val textLabel: Paint
|
|
||||||
|
|
||||||
/**
|
|
||||||
* paint for the lines
|
|
||||||
*/
|
|
||||||
val linePaint: Paint
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Goal line paint
|
* Goal line paint
|
||||||
*/
|
*/
|
||||||
val goalLinePaint: Paint
|
val goalLinePaint: Paint
|
||||||
|
|
||||||
/**
|
|
||||||
* is vertical scrolling enabled
|
|
||||||
*/
|
|
||||||
var scrollEnabled: Boolean
|
|
||||||
|
|
||||||
var onValueFormat: (value: Float) -> String
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* do the Zero line gets drawn?
|
* do the Zero line gets drawn?
|
||||||
*/
|
*/
|
||||||
@Deprecated("use the new global function", ReplaceWith("YAxisInterface.addLine"))
|
@Deprecated("use the new global function", ReplaceWith("YAxisInterface.addLine"))
|
||||||
var drawZeroLine: Boolean
|
var drawZeroLine: Boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* run when manually refreshing the system
|
|
||||||
*
|
|
||||||
* this is where the pre-logic is handled to make [onDraw] quicker
|
|
||||||
*/
|
|
||||||
fun refresh()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* override Y minimum
|
|
||||||
*
|
|
||||||
* @param yMin is set the min will ba at the value, if null it is calculated
|
|
||||||
*/
|
|
||||||
fun setYMin(yMin: Float?): YAxisInterface
|
|
||||||
|
|
||||||
/**
|
|
||||||
* override Y maximum
|
|
||||||
*
|
|
||||||
* @param yMax is set the max will ba at the value, if null it is calculated
|
|
||||||
*/
|
|
||||||
fun setYMax(yMax: Float?): YAxisInterface
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get Y maximum
|
|
||||||
*
|
|
||||||
* @return the maximum value Y can get (for displayed values)
|
|
||||||
*/
|
|
||||||
fun getYMax(): Float
|
|
||||||
|
|
||||||
/**
|
|
||||||
* get Y minimum
|
|
||||||
*
|
|
||||||
* @return the minimum value Y can get (for displayed values)
|
|
||||||
*/
|
|
||||||
fun getYMin(): Float
|
|
||||||
|
|
||||||
/**
|
|
||||||
* function that draw our legend
|
|
||||||
*
|
|
||||||
* @param canvas the canvas to draw on
|
|
||||||
* @param space the space where it is allowed to draw on
|
|
||||||
*
|
|
||||||
* @return the width of the sidebar
|
|
||||||
*/
|
|
||||||
fun onDraw(canvas: Canvas, space: RectF): Float
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a Goal line
|
* Add a Goal line
|
||||||
*
|
*
|
||||||
@ -125,24 +48,4 @@ sealed interface YAxisInterface {
|
|||||||
* Remove every lines
|
* Remove every lines
|
||||||
*/
|
*/
|
||||||
fun clearLines()
|
fun clearLines()
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.dzeio.charts.components
|
package com.dzeio.charts.components
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import android.view.MotionEvent.INVALID_POINTER_ID
|
import android.view.MotionEvent.INVALID_POINTER_ID
|
||||||
import android.view.ScaleGestureDetector
|
import android.view.ScaleGestureDetector
|
||||||
@ -9,7 +10,7 @@ import kotlin.math.abs
|
|||||||
/**
|
/**
|
||||||
* Class handling the scroll/zoom for the library
|
* Class handling the scroll/zoom for the library
|
||||||
*/
|
*/
|
||||||
class ChartScroll(view: View) {
|
class ChartScroll(private val view: View) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enabled the zoom/unzoom of datas
|
* Enabled the zoom/unzoom of datas
|
||||||
@ -43,7 +44,7 @@ class ChartScroll(view: View) {
|
|||||||
onChartMoved = fn
|
onChartMoved = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
private var onZoomChanged: ((scale: Float) -> Unit)? = null
|
private var onZoomChanged: ((scaleX: Float, scaleY: Float) -> Unit)? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param fn.scale Float starting from 100%
|
* @param fn.scale Float starting from 100%
|
||||||
@ -51,7 +52,7 @@ class ChartScroll(view: View) {
|
|||||||
* 99-% zoom out,
|
* 99-% zoom out,
|
||||||
* 101+% zoom in
|
* 101+% zoom in
|
||||||
*/
|
*/
|
||||||
fun setOnZoomChanged(fn: (scale: Float) -> Unit) {
|
fun setOnZoomChanged(fn: (scaleX: Float, scaleY: Float) -> Unit) {
|
||||||
onZoomChanged = fn
|
onZoomChanged = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,29 +60,29 @@ class ChartScroll(view: View) {
|
|||||||
view.context,
|
view.context,
|
||||||
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||||
if (currentZoom != detector.scaleFactor) {
|
Log.d("ChartScroll", detector.scaleFactor.toString())
|
||||||
currentZoom = detector.scaleFactor
|
val scaleX = processScaling(detector.currentSpanX, detector.previousSpanX)
|
||||||
onZoomChanged?.invoke(lastZoom + -currentZoom + 1)
|
val scaleY = processScaling(detector.currentSpanY, detector.previousSpanY)
|
||||||
}
|
onZoomChanged?.invoke(
|
||||||
|
1 - scaleY,
|
||||||
|
1 - scaleX,
|
||||||
|
)
|
||||||
|
|
||||||
return super.onScale(detector)
|
return super.onScale(detector)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
|
||||||
super.onScaleEnd(detector)
|
|
||||||
|
|
||||||
lastZoom += -currentZoom + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private fun processScaling(current: Float, previous: Float): Float {
|
||||||
|
return if (previous > 0f) current / previous else 1f
|
||||||
|
}
|
||||||
|
|
||||||
private var hasMoved = false
|
private var hasMoved = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
|
* Code mostly stolen from https://developer.android.com/training/gestures/scale#drag
|
||||||
*/
|
*/
|
||||||
fun onTouchEvent(ev: MotionEvent): Boolean {
|
fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||||
|
|
||||||
if (zoomEnabled) {
|
if (zoomEnabled) {
|
||||||
scaleGestureDetector.onTouchEvent(ev)
|
scaleGestureDetector.onTouchEvent(ev)
|
||||||
}
|
}
|
||||||
@ -117,7 +118,6 @@ class ChartScroll(view: View) {
|
|||||||
posX += moveX
|
posX += moveX
|
||||||
posY += moveY
|
posY += moveY
|
||||||
|
|
||||||
|
|
||||||
if (scrollEnabled) {
|
if (scrollEnabled) {
|
||||||
onChartMoved?.invoke(-posX, posY)
|
onChartMoved?.invoke(-posX, posY)
|
||||||
}
|
}
|
||||||
|
@ -33,8 +33,8 @@ sealed class BaseSerie(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getDisplayedEntries(): ArrayList<Entry> {
|
override fun getDisplayedEntries(): ArrayList<Entry> {
|
||||||
val minX = view.xAxis.x
|
val minX = view.xAxis.getCurrentMin()
|
||||||
val maxX = minX + view.xAxis.getDataWidth()
|
val maxX = view.xAxis.getCurrentMax()
|
||||||
|
|
||||||
val result: ArrayList<Entry> = arrayListOf()
|
val result: ArrayList<Entry> = arrayListOf()
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class ChartFragment : Fragment() {
|
|||||||
binding.sliderXAxisScroll.visibility = View.VISIBLE
|
binding.sliderXAxisScroll.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
chart.xAxis.dataWidth = null
|
chart.xAxis.dataWidth = null
|
||||||
chart.xAxis.x = 0.0
|
chart.xAxis.setCurrentMin(0.0)
|
||||||
binding.sliderXAxisScroll.visibility = View.GONE
|
binding.sliderXAxisScroll.visibility = View.GONE
|
||||||
}
|
}
|
||||||
chart.refresh()
|
chart.refresh()
|
||||||
|
@ -72,6 +72,11 @@ class MainFragment : Fragment() {
|
|||||||
LineSerie(this).apply {
|
LineSerie(this).apply {
|
||||||
entries = Utils.generateRandomDataset(5)
|
entries = Utils.generateRandomDataset(5)
|
||||||
}
|
}
|
||||||
|
this.xAxis.keepGlobalLimits = true
|
||||||
|
this.xAxis.zoomEnabled = true
|
||||||
|
this.xAxis.scrollEnabled = true
|
||||||
|
this.yAxis.zoomEnabled = true
|
||||||
|
this.yAxis.scrollEnabled = true
|
||||||
MaterialUtils.materielTheme(this, requireView())
|
MaterialUtils.materielTheme(this, requireView())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import com.google.android.material.color.MaterialColors
|
|||||||
object MaterialUtils {
|
object MaterialUtils {
|
||||||
fun materielTheme(chart: ChartViewInterface, view: View) {
|
fun materielTheme(chart: ChartViewInterface, view: View) {
|
||||||
chart.yAxis.apply {
|
chart.yAxis.apply {
|
||||||
textLabel.color =
|
textPaint.color =
|
||||||
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
||||||
linePaint.color =
|
linePaint.color =
|
||||||
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
||||||
@ -17,8 +17,13 @@ object MaterialUtils {
|
|||||||
MaterialColors.getColor(view, com.google.android.material.R.attr.colorError)
|
MaterialColors.getColor(view, com.google.android.material.R.attr.colorError)
|
||||||
}
|
}
|
||||||
|
|
||||||
chart.xAxis.textPaint.color =
|
chart.xAxis.apply {
|
||||||
|
textPaint.color =
|
||||||
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
||||||
|
linePaint.color =
|
||||||
|
MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnPrimaryContainer)
|
||||||
|
}
|
||||||
|
|
||||||
chart.annotator.apply {
|
chart.annotator.apply {
|
||||||
backgroundPaint.color = MaterialColors.getColor(
|
backgroundPaint.color = MaterialColors.getColor(
|
||||||
view,
|
view,
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
<com.dzeio.charts.ChartView
|
<com.dzeio.charts.ChartView
|
||||||
android:id="@+id/linechart"
|
android:id="@+id/linechart"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="100dp" />
|
android:layout_height="500dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user