1
0
mirror of https://github.com/dzeiocom/OpenHealth.git synced 2025-07-08 20:39:18 +00:00

feat: Global update

This commit is contained in:
2022-08-22 00:20:11 +02:00
parent 17f3a850c7
commit 5049c8afa4
75 changed files with 1646 additions and 1211 deletions

View File

@ -1,64 +0,0 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 32
defaultConfig {
// Android 5 Lollipop
minSdk 21
// Android 12
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Languages
def locales = ["en", "fr"]
buildConfigField "String[]", "LOCALES", "new String[]{\""+locales.join("\",\"")+"\"}"
resConfigs locales
}
buildTypes {
release {
// Slimmer version
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable true
// make it debuggable
renderscriptDebuggable true
// Optimization Level
renderscriptOptimLevel 0
}
}
// Compile using JAVA 8
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'com.google.android.material:material:1.6.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

38
charts/build.gradle.kts Normal file
View File

@ -0,0 +1,38 @@
plugins {
id("com.android.library")
kotlin("android")
}
android {
compileSdk = 33
defaultConfig {
minSdk = 21
targetSdk = 33
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "com.dzeio.charts"
}
dependencies {
implementation("com.google.android.material:material:1.6.1")
}

View File

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.dzeio.charts">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -12,113 +12,52 @@ import android.view.View
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import com.dzeio.charts.components.ChartScroll
import com.dzeio.charts.series.SerieAbstract
import kotlin.math.max
import kotlin.math.min
import com.dzeio.charts.series.SerieInterface
class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
View(context, attrs), ChartViewInterface {
companion object {
const val TAG = "DzeioCharts/ChartView"
private companion object {
const val TAG = "Charts/ChartView"
}
var debug = false
private val rect = RectF()
val xAxis = XAxis<Float>()
val yAxis = YAxis<Float>(this)
val animation = Animation()
val scroller = ChartScroll(this).apply {
setOnChartMoved { movementX, movementY ->
// Log.d(TAG, "scrolled: $movementX")
movementOffset = movementX / 100
refresh()
}
setOnZoomChanged {
Log.d(TAG, "New Zoom: $it")
zoom = (it * 1.2).toFloat()
refresh()
}
private val debugStrokePaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 8f
color = Color.parseColor("#654321")
}
private val animator: Runnable = object : Runnable {
override fun run() {
var needNewFrame = false
for (serie in series) {
val result = serie.onUpdate()
if (result) {
needNewFrame = true
}
}
if (needNewFrame) {
postDelayed(this, animation.getDelay().toLong())
}
invalidate()
}
}
override var debug: Boolean = false
/**
* global padding
*/
var padding: Float = 8f
override val xAxis = XAxis(this)
var series: ArrayList<SerieAbstract> = arrayListOf()
set(value) {
for (serie in value) {
serie.view = this
}
field = value
}
override val yAxis = YAxis(this)
/**
* Number of entries displayed at the same time
*/
private var zoom = 100f
override var series: ArrayList<SerieInterface> = arrayListOf()
var movementOffset: Float = 0f
private val rectF = RectF()
private val otherUseRectF = RectF()
fun refresh() {
for (serie in series) {
serie.prepareData()
}
rectF.set(
padding,
padding,
measuredWidth - padding - yAxis.getWidth() - padding,
height - padding
)
removeCallbacks(animator)
post(animator)
}
private val fgPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = Color.parseColor("#123456")
override fun refresh() {
invalidate()
}
override fun onDraw(canvas: Canvas) {
if (yAxis.legendEnabled) {
yAxis.display(canvas, measuredWidth, height)
if (debug) {
// draw corners
canvas.drawRect(rect.apply {
set(8f, 8f, width - 8f, height - 8f)
}, debugStrokePaint)
}
// chart draw rectangle
rect.apply {
set(0f, 0f, width.toFloat(), height.toFloat())
}
for (serie in series) {
serie.displayData(canvas, rectF)
serie.onDraw(canvas, rect)
}
// canvas.drawRect(
// measuredWidth - padding - yAxis.getWidth(),
// 0f,
// measuredWidth - padding,
// height - padding,
// fgPaint
// )
super.onDraw(canvas)
}
@ -127,40 +66,19 @@ class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet
return scroller.onTouchEvent(event)
}
fun getXOffset(): Int {
// Log.d(
// TAG,
// "baseOffset: ${xAxis.baseOffset}, mOffset: $movementOffset = ${xAxis.baseOffset + movementOffset}"
// )
// Log.d(
// TAG,
// "longestOffset: ${longestSerie()}, displayedEntries: ${getDisplayedEntries()} = ${longestSerie() - getDisplayedEntries()}"
// )
return min(
max(0f, xAxis.baseOffset + movementOffset).toInt(),
max(0, getCalculatedMax() - getDisplayedEntries())
)
}
val scroller = ChartScroll(this).apply {
var lastMovement = 0.0
setOnChartMoved { movementX, _ ->
fun getDisplayedEntries(): Int {
// Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}")
return max(0, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt())
}
fun getCalculatedMax(): Int {
var size = 0
for (serie in series) {
if (serie.datas.size > size) size = serie.datas.size
Log.d(TAG, "scrolled: ${(movementX - lastMovement) * (xAxis.increment / 10)}")
xAxis.x = xAxis.x + (movementX - lastMovement) * (xAxis.increment / 10)
lastMovement = movementX.toDouble()
refresh()
}
return size
// setOnZoomChanged {
// Log.d(TAG, "New Zoom: $it")
// zoom = (it * 1.2).toFloat()
// refresh()
// }
}
fun getXMax(displayedOnly: Boolean = false): Float {
var max = 0f
for (serie in series) {
val res = serie.getYMax(displayedOnly)
if (max < res) max = res
}
return max
}
}
}

View File

@ -0,0 +1,166 @@
package com.dzeio.charts
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.View
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import com.dzeio.charts.components.ChartScroll
import com.dzeio.charts.series.SerieAbstract
import kotlin.math.max
import kotlin.math.min
class ChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
companion object {
const val TAG = "DzeioCharts/ChartView"
}
override var debug = false
override val xAxis = XAxis<Float>()
override val yAxis = YAxis(this)
override val animation = Animation()
override val scroller = ChartScroll(this).apply {
setOnChartMoved { movementX, _ ->
// Log.d(TAG, "scrolled: $movementX")
movementOffset = movementX / 100
refresh()
}
setOnZoomChanged {
Log.d(TAG, "New Zoom: $it")
zoom = (it * 1.2).toFloat()
refresh()
}
}
override val animator: Runnable = object : Runnable {
override fun run() {
var needNewFrame = false
for (serie in series) {
val result = serie.onUpdate()
if (result) {
needNewFrame = true
}
}
if (needNewFrame) {
postDelayed(this, animation.getDelay().toLong())
}
invalidate()
}
}
/**
* global padding
*/
override var padding: Float = 8f
override var series: ArrayList<SerieAbstract> = arrayListOf()
set(value) {
for (serie in value) {
serie.view = this
}
field = value
}
/**
* Number of entries displayed at the same time
*/
override var zoom = 100f
override var movementOffset: Float = 0f
override val rectF = RectF()
override val otherUseRectF = RectF()
override fun refresh() {
for (serie in series) {
serie.prepareData()
}
rectF.set(
padding,
padding,
measuredWidth - padding - yAxis.getWidth() - padding,
height - padding
)
removeCallbacks(animator)
post(animator)
}
override val fgPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = Color.parseColor("#123456")
}
override fun onDraw(canvas: Canvas) {
if (yAxis.legendEnabled) {
yAxis.display(canvas, measuredWidth, height)
}
for (serie in series) {
serie.displayData(canvas, rectF)
}
// canvas.drawRect(
// measuredWidth - padding - yAxis.getWidth(),
// 0f,
// measuredWidth - padding,
// height - padding,
// fgPaint
// )
super.onDraw(canvas)
}
override fun onTouchEvent(event: MotionEvent): Boolean {
performClick()
return scroller.onTouchEvent(event)
}
override fun getXOffset(): Int {
// Log.d(
// TAG,
// "baseOffset: ${xAxis.baseOffset}, mOffset: $movementOffset = ${xAxis.baseOffset + movementOffset}"
// )
// Log.d(
// TAG,
// "longestOffset: ${longestSerie()}, displayedEntries: ${getDisplayedEntries()} = ${longestSerie() - getDisplayedEntries()}"
// )
return min(
max(0f, xAxis.baseOffset + movementOffset).toInt(),
max(0, getCalculatedMax() - getDisplayedEntries())
)
}
override fun getDisplayedEntries(): Int {
// Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}")
return max(0, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt())
}
override fun getCalculatedMax(): Int {
var size = 0
for (serie in series) {
if (serie.datas.size > size) size = serie.datas.size
}
return size
}
override fun getXMax(displayedOnly: Boolean): Float {
var max = 0f
for (serie in series) {
val res = serie.getYMax(displayedOnly)
if (max < res) max = res
}
return max
}
}

View File

@ -0,0 +1,35 @@
package com.dzeio.charts
import com.dzeio.charts.axis.XAxisInterface
import com.dzeio.charts.axis.YAxisInterface
import com.dzeio.charts.series.SerieInterface
interface ChartViewInterface {
/**
* Make the whole view in debug mode
*
* add debug texts, logs, and more
*/
var debug: Boolean
/**
* Hold metadata about the X axis
*/
val xAxis: XAxisInterface
/**
* Hold informations about the Y axis
*/
val yAxis: YAxisInterface
/**
* handle the series
*/
var series: ArrayList<SerieInterface>
/**
* refresh EVERYTHING
*/
fun refresh()
}

View File

@ -1,25 +1,56 @@
package com.dzeio.charts.axis
import com.dzeio.charts.XAxisLabels
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
class XAxis<T> {
class XAxis(
private val view: ChartViewInterface
) : XAxisInterface {
override var x: Double = 0.0
set(value) {
val max = getXMax() - increment * displayCount
if (value > max) {
field = max
return
}
var max: T? = null
var min: T? = null
val min = getXMin()
if (value < min) {
field = min
return
}
val labels = XAxisLabels()
field = value
}
override var increment: Double = 1.0
override var displayCount: Int = 10
override var labelCount: Int = 3
/**
* Number of entries displayed in the chart at the same time
*/
var entriesDisplayed = 5
/**
* Offset in the list
*/
var baseOffset = 0
var onValueFormat: (it: T) -> String = onValueFormat@{
return@onValueFormat it.toString()
override fun getPositionOnRect(entry: Entry, rect: RectF): Double {
return translatePositionToRect(entry.x, rect)
}
}
fun translatePositionToRect(value: Double, rect: RectF): Double {
val rectItem = rect.width() / displayCount // item size in graph
return rectItem * value / increment
}
override fun getXOffset(rect: RectF): Double {
return translatePositionToRect(x, rect)
}
override fun getXMax(): Double {
return view.series.maxOf { serie ->
serie.entries.maxOf { entry -> entry.x }
}
}
override fun getXMin(): Double {
return view.series.minOf { serie ->
serie.entries.minOf { entry -> entry.x }
}
}
}

View File

@ -0,0 +1,25 @@
package com.dzeio.charts.axis
import com.dzeio.charts.XAxisLabels
class XAxis<T> {
var max: T? = null
var min: T? = null
val labels = XAxisLabels()
/**
* Number of entries displayed in the chart at the same time
*/
var entriesDisplayed = 5
/**
* Offset in the list
*/
var baseOffset = 0
var onValueFormat: (it: T) -> String = onValueFormat@{
return@onValueFormat it.toString()
}
}

View File

@ -0,0 +1,35 @@
package com.dzeio.charts.axis
import android.graphics.RectF
import com.dzeio.charts.Entry
sealed interface XAxisInterface {
/**
* set X position
*/
var x: Double
/**
* X increment
*/
var increment: Double
/**
* indicate the max number of entries are displayed
*/
var displayCount: Int
/**
* indicate the number of labels displayed
*/
var labelCount: Int
fun getPositionOnRect(entry: Entry, rect: RectF): Double
fun getXOffset(rect: RectF): Double
fun getXMax(): Double
fun getXMin(): Double
}

View File

@ -3,72 +3,85 @@ package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import androidx.annotation.ColorInt
import com.dzeio.charts.ChartView
import com.dzeio.charts.utils.drawDottedLine
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
class YAxis<T>(
private val chartView: ChartView
) {
var max: T? = null
var min: T? = null
class YAxis(
private val view: ChartViewInterface
) : YAxisInterface {
/**
* Number of labels displayed on the sidebar
*/
var labelCount: Int = 4
override var enabled = false
@ColorInt
var color = Color.parseColor("#FC496D")
var textPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = color
it.textSize = 30f
it.textAlign = Paint.Align.RIGHT
}
var linePaint = Paint().apply {
override val textLabel = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#FC496D")
textSize = 30f
textAlign = Paint.Align.RIGHT
}
var legendEnabled = true
override var labelCount: Int = 3
private val rect: Rect = Rect()
private var min: Float? = null
private var max: Float? = null
fun getWidth(): Int {
var maxWidth = 0
val max = chartView.getXMax(true)
val vIncrement = max / labelCount
for (i in 0 until labelCount) {
val text = onValueFormat(vIncrement * (labelCount - i), true)
textPaint.getTextBounds(text, 0, text.length, rect)
if (rect.width() > maxWidth) maxWidth = rect.width()
override fun setYMin(yMin: Float?): YAxisInterface {
min = yMin
return this
}
override fun setYMax(yMax: Float?): YAxisInterface {
max = yMax
return this
}
override fun getYMax(): Float {
if (max != null) {
return max!!
}
return maxWidth + chartView.padding.toInt()
}
/**
* Function to display the YAxis sidebar on the right
*
* it migh also display content over the Graph
*/
fun display(canvas: Canvas, width: Int, height: Int) {
val max = chartView.getXMax(true)
val increment = (height - chartView.padding * 2) / labelCount
val vIncrement = max / labelCount
for (i in 0 until labelCount) {
val text = onValueFormat(vIncrement * (labelCount - i), true)
textPaint.getTextBounds(text, 0, text.length, rect)
val posY = increment * i.toFloat()
canvas.drawDottedLine(0f, posY + chartView.padding, width - rect.width().toFloat() - chartView.padding, posY, 40f, linePaint)
canvas.drawText(text,
width.toFloat() - chartView.padding, posY + chartView.padding, textPaint)
// canvas.drawDottedLine(0f, posY, measuredWidth.toFloat(), posY, 10f, linePaint)
if (view.series.isEmpty()) {
return 100f
}
return view.series
.maxOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
return@maxOf 0f
}
return@maxOf serie.getDisplayedEntries().maxOf { entry -> entry.y }
}
}
var onValueFormat: (value: Float, shortVersion: Boolean) -> String = { it, _ -> it.toString() }
override fun getYMin(): Float {
if (min != null) {
return min!!
}
if (view.series.isEmpty()) {
return 0f
}
return view.series
.minOf { serie ->
if (serie.getDisplayedEntries().isEmpty()) {
return@minOf 0f
}
return@minOf serie.getDisplayedEntries().minOf { entry -> entry.y }
}
}
}
override fun onDraw(canvas: Canvas, drawLocation: RectF) {
val min = getYMin()
val max = getYMax() - min
val top = drawLocation.top
val bottom = drawLocation.bottom
val increment = (bottom - top) / labelCount
val valueIncrement = (max - min) / labelCount
for (index in 0 until labelCount) {
canvas.drawText(
(valueIncrement * (labelCount + 1)).toString(),
bottom - (index + 1) * increment,
drawLocation.right,
textLabel
)
}
TODO("IDK if it works tbh")
}
}

View File

@ -0,0 +1,74 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import androidx.annotation.ColorInt
import com.dzeio.charts.ChartView
import com.dzeio.charts.utils.drawDottedLine
class YAxis<T>(
private val chartView: ChartView
) {
var max: T? = null
var min: T? = null
/**
* Number of labels displayed on the sidebar
*/
var labelCount: Int = 4
@ColorInt
var color = Color.parseColor("#FC496D")
var textPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = color
it.textSize = 30f
it.textAlign = Paint.Align.RIGHT
}
var linePaint = Paint().apply {
isAntiAlias = true
}
var legendEnabled = true
private val rect: Rect = Rect()
fun getWidth(): Int {
var maxWidth = 0
val max = chartView.getXMax(true)
val vIncrement = max / labelCount
for (i in 0 until labelCount) {
val text = onValueFormat(vIncrement * (labelCount - i), true)
textPaint.getTextBounds(text, 0, text.length, rect)
if (rect.width() > maxWidth) maxWidth = rect.width()
}
return maxWidth + chartView.padding.toInt()
}
/**
* Function to display the YAxis sidebar on the right
*
* it migh also display content over the Graph
*/
fun display(canvas: Canvas, width: Int, height: Int) {
val max = chartView.getXMax(true)
val increment = (height - chartView.padding * 2) / labelCount
val vIncrement = max / labelCount
for (i in 0 until labelCount) {
val text = onValueFormat(vIncrement * (labelCount - i), true)
textPaint.getTextBounds(text, 0, text.length, rect)
val posY = increment * i.toFloat()
canvas.drawDottedLine(0f, posY + chartView.padding, width - rect.width().toFloat() - chartView.padding, posY, 40f, linePaint)
canvas.drawText(text,
width.toFloat() - chartView.padding, posY + chartView.padding, textPaint)
// canvas.drawDottedLine(0f, posY, measuredWidth.toFloat(), posY, 10f, linePaint)
}
}
var onValueFormat: (value: Float, shortVersion: Boolean) -> String = { it, _ -> it.toString() }
}

View File

@ -0,0 +1,58 @@
package com.dzeio.charts.axis
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
sealed interface YAxisInterface {
/**
* whether or not this axis is displayed
*/
var enabled: Boolean
/**
* 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
/**
* 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
/**
* function that draw our legend
*/
fun onDraw(canvas: Canvas, drawLocation: RectF)
}

View File

@ -0,0 +1,6 @@
package com.dzeio.charts.axis
enum class YAxisPosition {
LEFT,
RIGHT
}

View File

@ -58,7 +58,7 @@ class ChartScroll(view: View) {
return super.onScale(detector)
}
override fun onScaleEnd(detector: ScaleGestureDetector?) {
override fun onScaleEnd(detector: ScaleGestureDetector) {
super.onScaleEnd(detector)
lastZoom += -currentZoom + 1

View File

@ -1,163 +1,68 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import kotlin.math.max
import com.dzeio.charts.ChartView
class BarSerie : SerieAbstract() {
class BarSerie(
private val view: ChartView
) : BaseSerie(view) {
companion object {
const val TAG = "DzeioCharts/BarSerie"
private companion object {
const val TAG = "Charts/BarSerie"
}
var spacing: Float = 8f
var targetPercentList = arrayListOf<Float>()
var percentList = arrayListOf<Float>()
private var fgPaint: Paint = Paint().also {
it.isAntiAlias = true
val barPaint = Paint().apply {
isAntiAlias = true
color = Color.parseColor("#123456")
}
var previousRefresh = 0
override fun onDraw(canvas: Canvas, drawableSpace: RectF) {
val spacing = drawableSpace.width() / view.xAxis.displayCount / 10
val barWidth = drawableSpace.width() / view.xAxis.displayCount - spacing
val displayedEntries = getDisplayedEntries()
val max = view.yAxis.getYMax()
val min = view.yAxis.getYMin()
private val r = Rect()
// Log.d(TAG, "${space.left}, ${space.right}")
override fun onUpdate(): Boolean {
var needNewFrame = false
for (i in targetPercentList.indices) {
val value = view.animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.00f
for (entry in displayedEntries) {
// calculated height in percent from 0 to 100
val height = (1 - entry.y / max) * drawableSpace.height()
// -1.945763981752553E-21
// 2.103653925902835E-21
val posX = view.xAxis.getPositionOnRect(entry, drawableSpace) - view.xAxis.getXOffset(drawableSpace) - canvas.width
// Log.d(TAG, "gpor = ${view.xAxis.getPositionOnRect(entry, space)}, gxo = ${view.xAxis.getXOffset(space)}")
// Log.d(TAG, "max = $max, y = ${entry.y}, height = $height")
// Log.d(TAG, "posX: ${posX / 60 / 60 / 1000}, offsetX = ${view.xAxis.x / (1000 * 60 * 60)}, x = ${entry.x / (1000 * 60 * 60)}, pouet: ${(view.xAxis.x + view.xAxis.displayCount * view.xAxis.increment) / (1000 * 60 * 60)}")
// Log.d(
// TAG, """
// ${posX.toFloat()},
// $height,
// ${(posX + barWidth).toFloat()},
// ${space.bottom}""".trimIndent()
// )
canvas.drawRect(
posX.toFloat(),
height,
(posX + barWidth).toFloat(),
drawableSpace.bottom,
barPaint
)
if (value != percentList[i]) {
needNewFrame = true
percentList[i] = value
}
}
return needNewFrame
}
canvas.drawText(
entry.y.toString(),
(posX + barWidth / 2).toFloat(),
drawableSpace.bottom / 2,
view.yAxis.textLabel.apply {
textAlign = Paint.Align.CENTER
override fun prepareData() {
val max: Float = if (view.yAxis.max != null) view.yAxis.max!! else {
getMax()
}
targetPercentList = arrayListOf()
// Log.d(TAG, "offset: ${view.getXOffset()}, displayed: ${view.getDisplayedEntries()}")
for (item in datas.subList(
view.getXOffset(),
view.getXOffset() + view.getDisplayedEntries()
)) {
// // // Process bottom texts
// val text = view.xAxis.onValueFormat(item.x)
// bottomTexts.add(text)
//
// // get Text boundaries
// view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
//
// // get height of text
// if (bottomTextHeight < r.height()) {
// bottomTextHeight = r.height()
// }
//
// // get text descent
// val descent = abs(r.bottom)
// if (bottomTextDescent < descent) {
// bottomTextDescent = descent
// }
// // process values
// Log.d(TAG, item.y.toString())
// add to animations the values
targetPercentList.add(1 - item.y / max)
}
// post list
val offset = view.getXOffset()
val movement = offset - previousRefresh
Log.d(TAG, "$offset - $previousRefresh = $movement")
if (movement != 0) {
previousRefresh = offset
}
// if (movement != 0) {
// Log.d(TAG, movement.toString())
// }
if (movement >= 1) {
percentList = percentList.subList(1, percentList.size).toCollection(ArrayList())
percentList.add(1f)
} else if (movement <= -1) {
percentList = percentList.subList(0, percentList.size - 1).toCollection(ArrayList())
percentList.add(0, 1f)
}
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) {
percentList.add(1f)
}
} else if (percentList.size > targetPercentList.size) {
val temp = percentList.size - targetPercentList.size
for (i in 0 until temp) {
percentList.removeAt(percentList.size - 1)
}
}
fgPaint.color = view.yAxis.color
}
override fun displayData(canvas: Canvas, rect: RectF) {
val barWidth = (rect.width() - view.padding * 2) / view.getDisplayedEntries() - spacing
if (percentList.isNotEmpty()) {
// draw each rectangles
for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString())
val left = rect.left + spacing * i + barWidth * (i - 1).toFloat() + view.padding
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = rect.left + (spacing + barWidth) * i.toFloat()
val bottom = rect.top + rect.height() - view.padding
val top = (bottom - rect.top) * percentList[i - 1] + view.padding
// create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
// remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
val targetTop = (bottom - rect.top) * targetPercentList[i - 1]
val text = view.yAxis.onValueFormat(getMax() - getMax() * targetPercentList[i - 1], true)
view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
val doDisplayIn = r.width() + 10f < barWidth && bottom - targetTop > r.height() + 40f
if (view.debug || !doDisplayIn || (doDisplayIn && bottom - top > r.height() + 40f)) {
val y = if (doDisplayIn) top + r.height() + 20f else top - r.height()
canvas.drawText(
text,
left + (right - left) / 2,
y,
view.xAxis.labels.build()
)
}
}
)
}
}
private fun getMax(): Float {
var calculatedMax = 0f
for (entry in datas.subList(
view.getXOffset(),
view.getDisplayedEntries() + view.getXOffset()
)) {
if (entry.y > calculatedMax) calculatedMax = entry.y
}
return if (calculatedMax < 0) 0f else calculatedMax
}
}
}

View File

@ -0,0 +1,160 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.util.Log
import kotlin.math.max
class BarSerie : SerieAbstract() {
companion object {
const val TAG = "DzeioCharts/BarSerie"
}
var spacing: Float = 8f
/**
* Values displayed on the grapd
*/
var displayedDatas = arrayListOf<Float>()
/**
* Target values
*/
var targetDatas = arrayListOf<Float>()
var targetPercentList = arrayListOf<Float>()
var percentList = arrayListOf<Float>()
var previousRefresh = 0
private var fgPaint: Paint = Paint().apply {
isAntiAlias = true
}
private val r = Rect()
override fun onUpdate(): Boolean {
var needNewFrame = false
for (i in targetPercentList.indices) {
val value = view.animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.00f
)
if (value != percentList[i]) {
needNewFrame = true
percentList[i] = value
}
}
return needNewFrame
}
override fun prepareData() {
val max: Float = if (view.yAxis.max != null) view.yAxis.max!! else {
getYMax(true)
}
targetPercentList = arrayListOf()
// Log.d(TAG, "offset: ${view.getXOffset()}, displayed: ${view.getDisplayedEntries()}")
for (item in getDisplayedEntries()) {
// // // Process bottom texts
// val text = view.xAxis.onValueFormat(item.x)
// bottomTexts.add(text)
//
// // get Text boundaries
// view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
//
// // get height of text
// if (bottomTextHeight < r.height()) {
// bottomTextHeight = r.height()
// }
//
// // get text descent
// val descent = abs(r.bottom)
// if (bottomTextDescent < descent) {
// bottomTextDescent = descent
// }
// process values
// Log.d(TAG, item.y.toString())
// add to animations the values
targetPercentList.add(1 - item.y / max)
}
// post list
val offset = view.getXOffset()
val movement = offset - previousRefresh
Log.d(TAG, "$offset - $previousRefresh = $movement")
if (movement != 0) {
previousRefresh = offset
}
// if (movement != 0) {
// Log.d(TAG, movement.toString())
// }
if (movement >= 1) {
percentList = percentList.subList(1, percentList.size).toCollection(ArrayList())
percentList.add(1f)
} else if (movement <= -1) {
percentList = percentList.subList(0, percentList.size - 1).toCollection(ArrayList())
percentList.add(0, 1f)
}
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) {
percentList.add(1f)
}
} else if (percentList.size > targetPercentList.size) {
val temp = percentList.size - targetPercentList.size
for (i in 0 until temp) {
percentList.removeAt(percentList.size - 1)
}
}
fgPaint.color = view.yAxis.color
}
override fun displayData(canvas: Canvas, rect: RectF) {
val barWidth = (rect.width() - view.padding * 2) / view.getDisplayedEntries() - spacing
if (percentList.isNotEmpty()) {
// draw each rectangles
for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString())
val left = rect.left + spacing * i + barWidth * (i - 1).toFloat() + view.padding
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = rect.left + (spacing + barWidth) * i.toFloat()
val bottom = rect.top + rect.height() - view.padding
val top = (bottom - rect.top) * percentList[i - 1] + view.padding
// create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
// remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
val targetTop = (bottom - rect.top) * targetPercentList[i - 1]
val text = view.yAxis.onValueFormat(getYMax(true) - getYMax(true) * targetPercentList[i - 1], true)
view.xAxis.labels.build().getTextBounds(text, 0, text.length, r)
val doDisplayIn =
r.width() + 10f < barWidth && bottom - targetTop > r.height() + 40f
if (view.debug || !doDisplayIn || (doDisplayIn && bottom - top > r.height() + 40f)) {
val y = if (doDisplayIn) top + r.height() + 20f else top - r.height()
canvas.drawText(
text,
left + (right - left) / 2,
y,
view.xAxis.labels.build()
)
}
}
}
}
}

View File

@ -0,0 +1,33 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.RectF
import com.dzeio.charts.ChartViewInterface
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.YAxisPosition
sealed class BaseSerie(
private val view: ChartViewInterface
) : SerieInterface {
private companion object {
const val TAG = "Charts/BaseSerie"
}
override var yAxisPosition: YAxisPosition = YAxisPosition.RIGHT
override var entries: ArrayList<Entry> = arrayListOf()
override fun getDisplayedEntries(): ArrayList<Entry> {
// -+ view.xAxis.increment = one out of display
val minX = view.xAxis.x - view.xAxis.increment
val maxX =
view.xAxis.x + view.xAxis.displayCount * 2 * view.xAxis.increment + view.xAxis.increment
return entries.filter {
return@filter it.x in minX..maxX
} as ArrayList<Entry>
}
abstract override fun onDraw(canvas: Canvas, drawableSpace: RectF)
}

View File

@ -12,18 +12,31 @@ abstract class SerieAbstract {
lateinit var view: ChartView
/**
* get Serie Y max
*/
fun getYMax(displayedOnly: Boolean = false): Float {
var max = 0f
var localDatas = if (displayedOnly) datas.subList(
view.getXOffset(),
min(view.getXOffset() + view.getDisplayedEntries(), datas.size)
) else datas
var max = Float.MIN_VALUE
val localDatas = if (displayedOnly) getDisplayedEntries() else datas
for (data in localDatas) {
if (max < data.y) max= data.y
if (max < data.y) max = data.y
}
return max
}
/**
* get Serie Y min
*/
fun getYMin(displayedOnly: Boolean = false): Float {
var min = Float.MAX_VALUE
val localDatas = if (displayedOnly) getDisplayedEntries() else datas
for (data in localDatas) {
if (min > data.y) min = data.y
}
return min
}
/**
* Animation updates
*/
@ -41,4 +54,11 @@ abstract class SerieAbstract {
* @param rect the rectangle in which you have to draw data
*/
abstract fun displayData(canvas: Canvas, rect: RectF)
protected fun getDisplayedEntries(): MutableList<Entry> {
return datas.subList(
view.getXOffset(),
min(datas.size, view.getDisplayedEntries() + view.getXOffset())
)
}
}

View File

@ -0,0 +1,28 @@
package com.dzeio.charts.series
import android.graphics.Canvas
import android.graphics.RectF
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.YAxisPosition
sealed interface SerieInterface {
/**
* location of the Y axis
*/
var yAxisPosition: YAxisPosition
/**
* filter out out of display entries
*
* @return the list of entries displayed
*/
fun getDisplayedEntries(): ArrayList<Entry>
/**
* set the entries for the list
*/
var entries: ArrayList<Entry>
fun onDraw(canvas: Canvas, drawableSpace: RectF)
}

View File

@ -1,324 +0,0 @@
package com.dzeio.charts.views
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import com.dzeio.charts.Animation
import com.dzeio.charts.Entry
import com.dzeio.charts.axis.XAxis
import com.dzeio.charts.axis.YAxis
import com.dzeio.charts.utils.drawDottedLine
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class BarChartView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null) :
View(context, attrs) {
companion object {
const val TAG = "DzeioCharts/BarView"
}
var debug: Boolean = false
/**
* Number of entries displayed at the same time
*/
private var zoom = 100f
/**
* Number of labels displayed at the same time
*/
var numberOfLabels = 3
/**
* Spacing between entries
*/
var spacing = 22
val xAxis: XAxis<Double> = XAxis()
val yAxis: YAxis<Float> = YAxis()
val animation = Animation()
private val textTopMargin = 5
private var barWidth: Int = 0
private var percentList: ArrayList<Float> = ArrayList()
/**
* value goes from 1 to 0 (1 at bottom, 0 at top)
*/
private var targetPercentList: ArrayList<Float> = ArrayList()
private var fgPaint: Paint = Paint().also {
it.isAntiAlias = true
it.color = yAxis.color
}
private var linePaint = Paint().apply {
color = Color.parseColor("#123456")
strokeWidth = 6f
}
private val rect: Rect = Rect()
private var bottomTextDescent = 0
private var bottomTextHeight = 0
private var movementOffset: Int = 0
private val animator: Runnable = object : Runnable {
override fun run() {
var needNewFrame = false
// var txt = ""
// for (tpl in targetPercentList) {
// txt += "$tpl, "
// }
// Log.d(TAG, txt)
for (i in targetPercentList.indices) {
val value = animation.updateValue(
1f,
targetPercentList[i],
percentList[i],
0f,
0.01f
)
if (value != percentList[i]) {
// if (!needNewFrame) {
// Log.d(TAG, "$i, $value, ${percentList[i]}")
// }
needNewFrame = true
percentList[i] = value
}
}
if (needNewFrame) {
postDelayed(this, animation.getDelay().toLong())
}
invalidate()
}
}
var list: ArrayList<Entry> = arrayListOf()
private var fiftyPercent = 0f
private var bottomTexts: ArrayList<String> = arrayListOf()
var previousOffset = getXOffset()
fun refresh() {
val r = Rect()
// // prepare bottom texts
bottomTextDescent = 0
bottomTextHeight = 0
bottomTexts = arrayListOf()
// // prepare values
// set the bar Width (also handle div by 0)
barWidth = measuredWidth / max(min(list.size, getDisplayedEntries()), 1) - spacing
// calculate max depending on the maximum value displayed or set in the yAxis params
val max: Float = if (yAxis.max != null) yAxis.max!! else {
var calculatedMax = 0f
for (entry in list.subList(
this.getXOffset(),
getDisplayedEntries() + this.getXOffset()
)) {
if (entry.y > calculatedMax) calculatedMax = entry.y
}
if (calculatedMax < 0) 0f else calculatedMax
}
fiftyPercent = max / 2
// make sure the target list
// Log.d(TAG, list.size.toString())
targetPercentList = arrayListOf()
// Log.d(TAG, "List selected: ${getXOffset()} to ${getXOffset() + getDisplayedEntries() - 1}")
for (item in list.subList(getXOffset(), getXOffset() + getDisplayedEntries())) {
// // Process bottom texts
val text = xAxis.onValueFormat(item.x)
bottomTexts.add(text)
// get Text boundaries
xAxis.labels.build().getTextBounds(text, 0, text.length, r)
// get height of text
if (bottomTextHeight < r.height()) {
bottomTextHeight = r.height()
}
// get text descent
val descent = abs(r.bottom)
if (bottomTextDescent < descent) {
bottomTextDescent = descent
}
// // process values
// add to animations the values
targetPercentList.add(1 - item.y / max)
}
// post list
val movement = getXOffset() - previousOffset
previousOffset = getXOffset()
// Log.d(TAG, movement.toString())
if (movement >= 1) {
percentList = percentList.subList(min(movement, percentList.size), percentList.size).toCollection(
ArrayList()
)
for (i in 0 until movement) {
percentList.add(1f)
}
} else if (movement <= -1) {
percentList = percentList.subList(0, percentList.size + movement).toCollection(
ArrayList()
)
for (i in 0 until abs(movement)) {
percentList.add(0, 1f)
}
}
if (percentList.isEmpty() || percentList.size < targetPercentList.size) {
val temp = targetPercentList.size - percentList.size
for (i in 0 until temp) {
percentList.add(1f)
}
} else if (percentList.size > targetPercentList.size) {
val temp = percentList.size - targetPercentList.size
for (i in 0 until temp) {
percentList.removeAt(percentList.size - 1)
}
}
// Misc operations
fgPaint = Paint().apply {
isAntiAlias = true
color = yAxis.color
}
linePaint = Paint().apply {
color = yAxis.lineColor
strokeWidth = 4f
}
removeCallbacks(animator)
post(animator)
}
override fun onDraw(canvas: Canvas) {
val bottom = height - bottomTextHeight - textTopMargin.toFloat()
// draw sidebar and lines
val increment = bottom / yAxis.labelCount
val vIncrement = fiftyPercent * 2 / yAxis.labelCount
for (i in 0 until yAxis.labelCount) {
val text = (vIncrement * (yAxis.labelCount - i)).toString()
xAxis.labels.build().getTextBounds(text, 0, text.length, rect)
val posY = increment * i
canvas.drawDottedLine(0f, posY, measuredWidth.toFloat(), posY, 40f, linePaint)
canvas.drawText(text,
(measuredWidth - rect.width()).toFloat(), posY + rect.height() + 20f, xAxis.labels.build())
// canvas.drawDottedLine(0f, posY, measuredWidth.toFloat(), posY, 10f, linePaint)
}
if (percentList.isNotEmpty()) {
// draw each rectangles
for (i in 1..percentList.size) {
// Log.d(TAG, percentList[i - 1].toString())
val left = spacing * i + barWidth * (i - 1).toFloat()
// Log.d(TAG, "$spacing, $i, $barWidth = $left")
val right = (spacing + barWidth) * i.toFloat()
val top = bottom * percentList[i - 1]
// create rounded rect
canvas.drawRoundRect(left, top, right, bottom, 8f, 8f, fgPaint)
// remove the bottom corners DUH
canvas.drawRect(left, max(top, bottom - 8f), right, bottom, fgPaint)
if (debug) {
canvas.drawText(
bottomTexts[i - 1].toString(),
left + (right - left) / 2,
top + (bottom - top) / 2,
xAxis.labels.build()
)
}
}
}
if (bottomTexts.isNotEmpty() && numberOfLabels > 0) {
val size = bottomTexts.size
var i = 1
var items = size / max(2, (numberOfLabels - 2))
// handle cases where size is even and numberOfLabels is 3
if (size % 2 != 0) {
items += 1
}
// Log.i(TAG, "$size / max($numberOfLabels - 2, 2) = $items")
for (s in bottomTexts) {
if ((numberOfLabels <= 2 || i % items != 0) && i != 1 && i != size) {
i++
continue
}
// Log.i(TAG, "Drawing $i")
xAxis.labels.build().getTextBounds(s, 0, s.length, rect)
canvas.drawText(
s,
// handle last entry overflowing
min(
// handle first entry overflowing
max(
(spacing * i + barWidth * (i - 1) + barWidth / 2).toFloat(),
rect.width() / 2f
),
measuredWidth - rect.width() / 2f
),
(height - bottomTextDescent).toFloat(),
xAxis.labels.build()
)
i++
if (numberOfLabels == 1) {
break
}
}
}
}
// override fun onChartMoved(movementX: Float, movementY: Float) {
// movementOffset = (movementX / 100).toInt()
// refresh()
// }
// override fun onZoomChanged(scale: Float) {
// Log.d(TAG, "New Zoom: $scale")
// zoom = (scale * 1.2).toFloat()
// refresh()
// }
private fun getXOffset(): Int {
return min(max(0, xAxis.baseOffset + movementOffset), list.size - getDisplayedEntries())
}
private fun getDisplayedEntries(): Int {
// Log.d(TAG, "Number of entries displayed ${list.size}, ${xAxis.entriesDisplayed} + (($zoom - 100) * 10) = ${xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()}")
return max(1, min(list.size, xAxis.entriesDisplayed + ((zoom - 100) * 10).toInt()))
}
}